|
| 1 | +using Canvas.Core.Shapes; |
| 2 | +using Core.Enums; |
| 3 | +using Core.Indicators; |
| 4 | +using Core.Models; |
| 5 | +using Core.Services; |
| 6 | +using Dashboard.Components; |
| 7 | +using Simulation; |
| 8 | +using SkiaSharp; |
| 9 | +using System; |
| 10 | +using System.Collections.Generic; |
| 11 | +using System.Linq; |
| 12 | +using System.Threading.Tasks; |
| 13 | + |
| 14 | +namespace Dashboard.Pages.Futures |
| 15 | +{ |
| 16 | + public partial class Covariance |
| 17 | + { |
| 18 | + ControlsComponent View { get; set; } |
| 19 | + ChartsComponent DataView { get; set; } |
| 20 | + ChartsComponent ScoreView { get; set; } |
| 21 | + ChartsComponent IndicatorsView { get; set; } |
| 22 | + ChartsComponent PerformanceView { get; set; } |
| 23 | + TransactionsComponent TransactionsView { get; set; } |
| 24 | + OrdersComponent OrdersView { get; set; } |
| 25 | + PositionsComponent PositionsView { get; set; } |
| 26 | + StatementsComponent StatementsView { get; set; } |
| 27 | + PerformanceIndicator Performance { get; set; } |
| 28 | + Dictionary<string, ScaleIndicator> Scales { get; set; } |
| 29 | + |
| 30 | + double Deviation { get; set; } = 2; |
| 31 | + AverageService AverageService { get; set; } = new(); |
| 32 | + |
| 33 | + Dictionary<string, Instrument> Instruments = new() |
| 34 | + { |
| 35 | + ["ESU25"] = new() { Name = "ESU25", StepValue = 12.50, StepSize = 0.25, Leverage = 50, Commission = 3.65, TimeFrame = TimeSpan.FromSeconds(1) }, |
| 36 | + ["NQU25"] = new() { Name = "NQU25", StepValue = 5, StepSize = 0.25, Leverage = 20, Commission = 3.65, TimeFrame = TimeSpan.FromSeconds(1) }, |
| 37 | + }; |
| 38 | + |
| 39 | + protected override async Task OnView() |
| 40 | + { |
| 41 | + await DataView.Create(nameof(DataView)); |
| 42 | + await ScoreView.Create(nameof(ScoreView)); |
| 43 | + await IndicatorsView.Create(nameof(IndicatorsView)); |
| 44 | + await PerformanceView.Create(nameof(PerformanceView)); |
| 45 | + |
| 46 | + DataView.Composers.ForEach(o => o.ShowIndex = i => GetDate(o.Items, (int)i)); |
| 47 | + ScoreView.Composers.ForEach(o => o.ShowIndex = i => GetDate(o.Items, (int)i)); |
| 48 | + IndicatorsView.Composers.ForEach(o => o.ShowIndex = i => GetDate(o.Items, (int)i)); |
| 49 | + PerformanceView.Composers.ForEach(o => o.ShowIndex = i => GetDate(o.Items, (int)i)); |
| 50 | + } |
| 51 | + |
| 52 | + protected override Task OnTrade() |
| 53 | + { |
| 54 | + var adapter = Adapter = new SimGateway |
| 55 | + { |
| 56 | + Connector = Connector, |
| 57 | + Source = Configuration["Documents:Resources"] + "/FUTS/2025-06-17", |
| 58 | + Account = new() |
| 59 | + { |
| 60 | + Descriptor = "Demo", |
| 61 | + Balance = 25000, |
| 62 | + Instruments = Instruments |
| 63 | + } |
| 64 | + }; |
| 65 | + |
| 66 | + Performance = new PerformanceIndicator { Name = "Balance" }; |
| 67 | + Scales = adapter.Account.Instruments.Keys.ToDictionary(o => o, name => new ScaleIndicator |
| 68 | + { |
| 69 | + Name = name, |
| 70 | + Min = -1, |
| 71 | + Max = 1 |
| 72 | + }); |
| 73 | + |
| 74 | + return base.OnTrade(); |
| 75 | + } |
| 76 | + |
| 77 | + protected override async void OnViewUpdate(Instrument instrument) |
| 78 | + { |
| 79 | + var adapter = Adapter; |
| 80 | + var account = adapter.Account; |
| 81 | + var price = instrument.Price; |
| 82 | + var index = price.Bar.Time.Value; |
| 83 | + var assetX = account.Instruments["ESU25"]; |
| 84 | + var assetY = account.Instruments["NQU25"]; |
| 85 | + var seriesX = (await adapter.GetPriceGroups(new Criteria { Count = 100, Instrument = assetX })).Data; |
| 86 | + var seriesY = (await adapter.GetPriceGroups(new Criteria { Count = 100, Instrument = assetY })).Data; |
| 87 | + |
| 88 | + if (seriesX.Count is 0 || seriesY.Count is 0) |
| 89 | + { |
| 90 | + return; |
| 91 | + } |
| 92 | + |
| 93 | + var performance = await Performance.Update([adapter]); |
| 94 | + var scaleX = await Scales[assetX.Name].Update(seriesX); |
| 95 | + var scaleY = await Scales[assetY.Name].Update(seriesY); |
| 96 | + var priceX = seriesX.Last(); |
| 97 | + var priceY = seriesY.Last(); |
| 98 | + var retSeriesX = seriesX.Select(o => o.Last.Value * assetX.Leverage.Value).ToArray(); |
| 99 | + var retSeriesY = seriesY.Select(o => o.Last.Value * assetY.Leverage.Value).ToArray(); |
| 100 | + var beta = CalculateHedgeRatio(retSeriesX, retSeriesY); |
| 101 | + var score = CalculateZScore(retSeriesX, retSeriesY, beta, (priceX.Last * assetX.Leverage.Value - beta * priceY.Last * assetY.Leverage.Value).Value); |
| 102 | + |
| 103 | + OrdersView.Update(Adapters.Values); |
| 104 | + PositionsView.Update(Adapters.Values); |
| 105 | + TransactionsView.Update(Adapters.Values); |
| 106 | + ScoreView.Update(index, nameof(ScoreView), "Score", new AreaShape { Y = score, Component = ComUp }); |
| 107 | + DataView.Update(index, nameof(DataView), "Leader", new AreaShape { Y = priceX.Last, Component = ComUp }); |
| 108 | + IndicatorsView.Update(index, nameof(IndicatorsView), "X", new LineShape { Y = scaleX.Response.Last, Component = ComUp }); |
| 109 | + IndicatorsView.Update(index, nameof(IndicatorsView), "Y", new LineShape { Y = scaleY.Response.Last, Component = ComDown }); |
| 110 | + PerformanceView.Update(index, nameof(PerformanceView), "Balance", new AreaShape { Y = account.Balance + account.Performance }); |
| 111 | + PerformanceView.Update(index, nameof(PerformanceView), "PnL", PerformanceView.GetShape<LineShape>(performance.Response, SKColors.OrangeRed)); |
| 112 | + } |
| 113 | + |
| 114 | + protected override async Task OnTradeUpdate(Instrument instrument) |
| 115 | + { |
| 116 | + if (Equals(instrument.Name, "ESU25") is false) |
| 117 | + { |
| 118 | + return; |
| 119 | + } |
| 120 | + |
| 121 | + var price = instrument.Price; |
| 122 | + var adapter = Adapter; |
| 123 | + var account = adapter.Account; |
| 124 | + var assetX = account.Instruments["ESU25"]; |
| 125 | + var assetY = account.Instruments["NQU25"]; |
| 126 | + var seriesX = (await adapter.GetPrices(new Criteria { Count = 100, Instrument = assetX })).Data; |
| 127 | + var seriesY = (await adapter.GetPrices(new Criteria { Count = 100, Instrument = assetY })).Data; |
| 128 | + |
| 129 | + if (seriesX.Count is 0 || seriesY.Count is 0) |
| 130 | + { |
| 131 | + return; |
| 132 | + } |
| 133 | + |
| 134 | + var orders = (await adapter.GetOrders(default)).Data; |
| 135 | + var positions = (await adapter.GetPositions(default)).Data; |
| 136 | + var priceX = seriesX.Last(); |
| 137 | + var priceY = seriesY.Last(); |
| 138 | + |
| 139 | + if (orders.Count is 0) |
| 140 | + { |
| 141 | + var retSeriesX = seriesX.Select(o => o.Last.Value * assetX.Leverage.Value).ToArray(); |
| 142 | + var retSeriesY = seriesY.Select(o => o.Last.Value * assetY.Leverage.Value).ToArray(); |
| 143 | + var beta = CalculateHedgeRatio(retSeriesX, retSeriesY); |
| 144 | + var score = CalculateZScore(retSeriesX, retSeriesY, beta, (priceX.Last * assetX.Leverage.Value - beta * priceY.Last * assetY.Leverage.Value).Value); |
| 145 | + var isLong = score < -Deviation; |
| 146 | + var isShort = score > Deviation; |
| 147 | + |
| 148 | + if (Equals(price.Bar.Time, scores.LastOrDefault().Item1)) |
| 149 | + { |
| 150 | + scores[scores.Count - 1] = (price.Bar.Time.Value, score); |
| 151 | + } |
| 152 | + else |
| 153 | + { |
| 154 | + scores.Add((price.Bar.Time.Value, score)); |
| 155 | + } |
| 156 | + |
| 157 | + var prevScore = scores.ElementAtOrDefault(scores.Count - 2).Item2; |
| 158 | + |
| 159 | + if (positions.Count is 0) |
| 160 | + { |
| 161 | + switch (true) |
| 162 | + { |
| 163 | + case true when isLong: |
| 164 | + await OpenPosition(adapter, assetX, OrderSideEnum.Long); |
| 165 | + await OpenPosition(adapter, assetY, OrderSideEnum.Short); |
| 166 | + break; |
| 167 | + |
| 168 | + case true when isShort: |
| 169 | + await OpenPosition(adapter, assetX, OrderSideEnum.Short); |
| 170 | + await OpenPosition(adapter, assetY, OrderSideEnum.Long); |
| 171 | + break; |
| 172 | + } |
| 173 | + } |
| 174 | + |
| 175 | + if (positions.Count is not 0) |
| 176 | + { |
| 177 | + var pos = positions.First(); |
| 178 | + var closeLong = pos.Side is OrderSideEnum.Long && prevScore > 0; |
| 179 | + var closeShort = pos.Side is OrderSideEnum.Short && prevScore < 0; |
| 180 | + |
| 181 | + if (closeLong || closeShort) |
| 182 | + { |
| 183 | + await ClosePosition(adapter); |
| 184 | + } |
| 185 | + } |
| 186 | + } |
| 187 | + } |
| 188 | + |
| 189 | + List<(long, double)> scores = new(); |
| 190 | + |
| 191 | + /// <summary> |
| 192 | + /// Calculates the Hedge Ratio (Beta) between two time series (Y vs X) using simple linear regression. |
| 193 | + /// This should be calculated over a long look-back window (e.g., 252 daily bars). |
| 194 | + /// </summary> |
| 195 | + /// <param name="y">The time series of the dependent asset (Asset 1, the one being hedged).</param> |
| 196 | + /// <param name="x">The time series of the independent asset (Asset 2, the hedging instrument).</param> |
| 197 | + /// <returns>The Hedge Ratio (Beta) - the number of units of X required to hedge 1 unit of Y.</returns> |
| 198 | + public static double CalculateHedgeRatio(double[] y, double[] x) |
| 199 | + { |
| 200 | + if (y == null || x == null || y.Length != x.Length || y.Length < 2) |
| 201 | + { |
| 202 | + return 0; |
| 203 | + } |
| 204 | + |
| 205 | + int n = y.Length; |
| 206 | + |
| 207 | + // 1. Calculate Averages |
| 208 | + double avgY = y.Average(); |
| 209 | + double avgX = x.Average(); |
| 210 | + |
| 211 | + // 2. Calculate Numerator (Covariance equivalent: Sum of (xi - avgX) * (yi - avgY)) |
| 212 | + double numerator = 0; |
| 213 | + for (int i = 0; i < n; i++) |
| 214 | + { |
| 215 | + numerator += (x[i] - avgX) * (y[i] - avgY); |
| 216 | + } |
| 217 | + |
| 218 | + // 3. Calculate Denominator (Variance equivalent: Sum of (xi - avgX)^2) |
| 219 | + double denominator = 0; |
| 220 | + for (int i = 0; i < n; i++) |
| 221 | + { |
| 222 | + denominator += Math.Pow(x[i] - avgX, 2); |
| 223 | + } |
| 224 | + |
| 225 | + // 4. Calculate Beta (Hedge Ratio) = Covariance(X, Y) / Variance(X) |
| 226 | + if (denominator == 0) |
| 227 | + { |
| 228 | + // Avoid division by zero, which implies no variation in the hedging asset price. |
| 229 | + return 0.0; |
| 230 | + } |
| 231 | + |
| 232 | + return numerator / denominator; |
| 233 | + } |
| 234 | + |
| 235 | + /// <summary> |
| 236 | + /// Calculates the Z-Score for the current spread relative to its historical mean and standard deviation. |
| 237 | + /// This should be calculated over a shorter, rolling look-back window (e.g., 60-90 bars) |
| 238 | + /// using the spread history that was already calculated using the Hedge Ratio. |
| 239 | + /// </summary> |
| 240 | + /// <param name="spreadHistory">Historical values of the calculated spread (Spread = Y - Beta * X).</param> |
| 241 | + /// <param name="currentSpread">The current calculated value of the spread.</param> |
| 242 | + /// <returns>The Z-score: the number of standard deviations the current spread is from the mean.</returns> |
| 243 | + public static double CalculateZScore(double[] yHistory, double[] xHistory, double beta, double currentSpread) |
| 244 | + { |
| 245 | + if (yHistory == null || xHistory == null || yHistory.Length != xHistory.Length || yHistory.Length < 2) |
| 246 | + { |
| 247 | + return 0; |
| 248 | + } |
| 249 | + |
| 250 | + // 1. Calculate the historical spread internally: Spread = Y - Beta * X |
| 251 | + List<double> spreadHistory = new List<double>(); |
| 252 | + for (int i = 0; i < yHistory.Length; i++) |
| 253 | + { |
| 254 | + spreadHistory.Add(yHistory[i] - beta * xHistory[i]); |
| 255 | + } |
| 256 | + |
| 257 | + // Now, proceed with mean and standard deviation calculation based on the generated spreadHistory |
| 258 | + int n = spreadHistory.Count; |
| 259 | + |
| 260 | + // 1. Calculate the Mean (Average) of the historical spread |
| 261 | + double mean = spreadHistory.Average(); |
| 262 | + |
| 263 | + // 2. Calculate the Standard Deviation of the historical spread |
| 264 | + double sumOfSquares = 0; |
| 265 | + |
| 266 | + foreach (var spread in spreadHistory) |
| 267 | + { |
| 268 | + sumOfSquares += Math.Pow(spread - mean, 2); |
| 269 | + } |
| 270 | + |
| 271 | + // Use N-1 for sample standard deviation, though N is also common in finance. |
| 272 | + double variance = sumOfSquares / (n - 1); |
| 273 | + double standardDeviation = Math.Sqrt(variance); |
| 274 | + |
| 275 | + // 3. Calculate the Z-Score |
| 276 | + if (standardDeviation == 0) |
| 277 | + { |
| 278 | + // Spread is perfectly flat, Z-score is undefined or 0. |
| 279 | + return 0.0; |
| 280 | + } |
| 281 | + |
| 282 | + return (currentSpread - mean) / standardDeviation; |
| 283 | + } |
| 284 | + } |
| 285 | +} |
0 commit comments