From efb6f8b64fe061426278bd28b27ac38a2b5e09bc Mon Sep 17 00:00:00 2001 From: vlad zverzhkhovskiy Date: Tue, 16 Sep 2025 17:59:23 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D1=80=D0=B8=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=81=D0=BE=D0=BE=D1=82=D0=BD=D0=BE=D1=88?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D1=80=D0=BE=D0=B4=D0=B0=D0=B6?= =?UTF-8?q?=20=D0=B8=20=D0=BF=D0=BE=D0=BA=D1=83=D0=BF=D0=BE=D0=BA=20=D1=81?= =?UTF-8?q?=D0=B1=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dtos/Enums/TimeWindowCacheType.cs | 2 +- .../Dtos/FFT/Enums/ValueAmplitudePosition.cs | 2 +- .../Declisions/Dtos/FFT/FFTAnalyzeResult.cs | 2 +- .../Declisions/Dtos/TimeWindowCacheItem.cs | 4 +- .../Services/Cache/PriceHistoryCacheUnit2.cs | 6 +- KLHZ.Trader.Core.Math/Declisions/Utils/FFT.cs | 6 +- KLHZ.Trader.Core/Exchange/Constants.cs | 2 + KLHZ.Trader.Core/Exchange/Services/Trader.cs | 133 ++++++++++++------ .../Exchange/Services/TraderDataProvider.cs | 19 +++ .../Controllers/PlayController.cs | 10 +- 10 files changed, 130 insertions(+), 56 deletions(-) diff --git a/KLHZ.Trader.Core.Contracts/Declisions/Dtos/Enums/TimeWindowCacheType.cs b/KLHZ.Trader.Core.Contracts/Declisions/Dtos/Enums/TimeWindowCacheType.cs index 1a77eea..5a78531 100644 --- a/KLHZ.Trader.Core.Contracts/Declisions/Dtos/Enums/TimeWindowCacheType.cs +++ b/KLHZ.Trader.Core.Contracts/Declisions/Dtos/Enums/TimeWindowCacheType.cs @@ -4,7 +4,7 @@ { None = 0, _1_Minute = 1, - _2_Minutes = 2, + _5_Minutes = 2, _15_Minutes = 15, } } diff --git a/KLHZ.Trader.Core.Math/Declisions/Dtos/FFT/Enums/ValueAmplitudePosition.cs b/KLHZ.Trader.Core.Math/Declisions/Dtos/FFT/Enums/ValueAmplitudePosition.cs index 5bd2ac3..279b5b1 100644 --- a/KLHZ.Trader.Core.Math/Declisions/Dtos/FFT/Enums/ValueAmplitudePosition.cs +++ b/KLHZ.Trader.Core.Math/Declisions/Dtos/FFT/Enums/ValueAmplitudePosition.cs @@ -10,7 +10,7 @@ namespace KLHZ.Trader.Core.Math.Declisions.Dtos.FFT.Enums { None = 0, Middle = 1, - UpperThen20Decil = 10, + UpperThen30Decil = 10, LowerThenMediana = -50, } } diff --git a/KLHZ.Trader.Core.Math/Declisions/Dtos/FFT/FFTAnalyzeResult.cs b/KLHZ.Trader.Core.Math/Declisions/Dtos/FFT/FFTAnalyzeResult.cs index ef715f1..7f2d270 100644 --- a/KLHZ.Trader.Core.Math/Declisions/Dtos/FFT/FFTAnalyzeResult.cs +++ b/KLHZ.Trader.Core.Math/Declisions/Dtos/FFT/FFTAnalyzeResult.cs @@ -13,7 +13,7 @@ namespace KLHZ.Trader.Core.Math.Declisions.Dtos.FFT public decimal Max { get; init; } public decimal Min { get; init; } public decimal Mediana { get; init; } - public decimal Upper20Decil { get; init; } + public decimal Upper30Decil { get; init; } public decimal Lower20Decil { get; init; } public DateTime LastTime { get; init; } public DateTime StartTime { get; init; } diff --git a/KLHZ.Trader.Core.Math/Declisions/Dtos/TimeWindowCacheItem.cs b/KLHZ.Trader.Core.Math/Declisions/Dtos/TimeWindowCacheItem.cs index 8cbf129..c1c50a7 100644 --- a/KLHZ.Trader.Core.Math/Declisions/Dtos/TimeWindowCacheItem.cs +++ b/KLHZ.Trader.Core.Math/Declisions/Dtos/TimeWindowCacheItem.cs @@ -43,9 +43,9 @@ namespace KLHZ.Trader.Core.Math.Declisions.Dtos { switch (type) { - case TimeWindowCacheType._2_Minutes: + case TimeWindowCacheType._5_Minutes: { - return TimeSpan.FromMinutes(2); + return TimeSpan.FromMinutes(5); } case TimeWindowCacheType._15_Minutes: { diff --git a/KLHZ.Trader.Core.Math/Declisions/Services/Cache/PriceHistoryCacheUnit2.cs b/KLHZ.Trader.Core.Math/Declisions/Services/Cache/PriceHistoryCacheUnit2.cs index 3fe3372..e9523df 100644 --- a/KLHZ.Trader.Core.Math/Declisions/Services/Cache/PriceHistoryCacheUnit2.cs +++ b/KLHZ.Trader.Core.Math/Declisions/Services/Cache/PriceHistoryCacheUnit2.cs @@ -51,7 +51,7 @@ namespace KLHZ.Trader.Core.Math.Declisions.Services.Cache private readonly decimal[] Prices = new decimal[_arrayMaxLength]; private readonly DateTime[] Timestamps = new DateTime[_arrayMaxLength]; private readonly ConcurrentDictionary _1_minTimeWindows = new(); - private readonly ConcurrentDictionary _2_minTimeWindows = new(); + private readonly ConcurrentDictionary _5_minTimeWindows = new(); private readonly ConcurrentDictionary _15_minTimeWindows = new(); private int _length = 0; @@ -85,9 +85,9 @@ namespace KLHZ.Trader.Core.Math.Declisions.Services.Cache { switch (timeWindowCacheType) { - case TimeWindowCacheType._2_Minutes: + case TimeWindowCacheType._5_Minutes: { - return _2_minTimeWindows; + return _5_minTimeWindows; } case TimeWindowCacheType._15_Minutes: { diff --git a/KLHZ.Trader.Core.Math/Declisions/Utils/FFT.cs b/KLHZ.Trader.Core.Math/Declisions/Utils/FFT.cs index 2566a2f..3c68256 100644 --- a/KLHZ.Trader.Core.Math/Declisions/Utils/FFT.cs +++ b/KLHZ.Trader.Core.Math/Declisions/Utils/FFT.cs @@ -11,9 +11,9 @@ namespace KLHZ.Trader.Core.Math.Declisions.Utils { var value = (decimal)CalcAmplitude(fftData.Harmonics, fftData.StartTime, timestamp); var value2 = (decimal)CalcExtremum(fftData.Harmonics, fftData.StartTime, timestamp); - if (value > fftData.Upper20Decil) + if (value > fftData.Upper30Decil) { - return ValueAmplitudePosition.UpperThen20Decil; + return ValueAmplitudePosition.UpperThen30Decil; } else if (value < fftData.Mediana) { @@ -47,7 +47,7 @@ namespace KLHZ.Trader.Core.Math.Declisions.Utils LastTime = timestamps[timestamps.Length - 1], StartTime = startTime, Mediana = newValues[newValues.Length / 2], - Upper20Decil = newValues[(int)(newValues.Length * 0.8)], + Upper30Decil = newValues[(int)(newValues.Length * 0.7)], Lower20Decil = newValues[(int)(newValues.Length * 0.2)], Max = newValues.Max(), Min = newValues.Min(), diff --git a/KLHZ.Trader.Core/Exchange/Constants.cs b/KLHZ.Trader.Core/Exchange/Constants.cs index 14632ef..820facb 100644 --- a/KLHZ.Trader.Core/Exchange/Constants.cs +++ b/KLHZ.Trader.Core/Exchange/Constants.cs @@ -3,6 +3,8 @@ internal static class Constants { internal const string _1minCacheKey = "1min"; + internal const string _1minSellCacheKey = "1min_sell"; + internal const string _1minBuyCacheKey = "1min_buy"; internal const string BigWindowCrossingAverageProcessor = "Trader_big"; internal const string SmallWindowCrossingAverageProcessor = "Trader_small"; internal const string AreasRelationProcessor = "balancescalc30min"; diff --git a/KLHZ.Trader.Core/Exchange/Services/Trader.cs b/KLHZ.Trader.Core/Exchange/Services/Trader.cs index 44102f1..c16b5cc 100644 --- a/KLHZ.Trader.Core/Exchange/Services/Trader.cs +++ b/KLHZ.Trader.Core/Exchange/Services/Trader.cs @@ -32,6 +32,7 @@ namespace KLHZ.Trader.Core.Exchange.Services private readonly TraderDataProvider _tradeDataProvider; private readonly ILogger _logger; private readonly ConcurrentDictionary LongOpeningStops = new(); + private readonly ConcurrentDictionary LongClosingStops = new(); private readonly ConcurrentDictionary ShortClosingStops = new(); private readonly ConcurrentDictionary Leverages = new(); @@ -105,9 +106,8 @@ namespace KLHZ.Trader.Core.Exchange.Services else { position = FFT.Check(fft, message.Time); - if (position == Math.Declisions.Dtos.FFT.Enums.ValueAmplitudePosition.UpperThen20Decil) + if (position == Math.Declisions.Dtos.FFT.Enums.ValueAmplitudePosition.UpperThen30Decil) { - await LogPrice(message, "upper10percent", message.Value); await LogPrice(message, "upper30percent", message.Value); } if (position == Math.Declisions.Dtos.FFT.Enums.ValueAmplitudePosition.LowerThenMediana) @@ -155,6 +155,36 @@ namespace KLHZ.Trader.Core.Exchange.Services list.Clear(); } } + if (message.Figi == "BBG004730N88") + { + if (message.Direction == 1) + { + await _tradeDataProvider.AddDataTo5MinuteWindowCache(message.Figi, Constants._1minBuyCacheKey, new Contracts.Declisions.Dtos.CachedValue() + { + Time = message.Time, + Value = (decimal)message.Count + }); + } + if (message.Direction == 2) + { + await _tradeDataProvider.AddDataTo5MinuteWindowCache(message.Figi, Constants._1minSellCacheKey, new Contracts.Declisions.Dtos.CachedValue() + { + Time = message.Time, + Value = (decimal)message.Count + }); + + } + var sberSells = await _tradeDataProvider.GetDataFrom5MinuteWindowCache("BBG004730N88", Constants._1minSellCacheKey); + var sberBuys = await _tradeDataProvider.GetDataFrom5MinuteWindowCache("BBG004730N88", Constants._1minBuyCacheKey); + var sells = sberSells.Sum(s => s.Value); + var buys = sberBuys.Sum(s => s.Value); + var su = sells + buys; + if (su != 0) + { + await LogPrice(message, "sellsbuysbalance", (sells / su - 0.5m)*2); + } + } + if (_tradingInstrumentsFigis.Contains(message.Figi)) { var currentTime = message.IsHistoricalData ? message.Time : DateTime.UtcNow; @@ -314,6 +344,14 @@ namespace KLHZ.Trader.Core.Exchange.Services return; } + var sberSells = await _tradeDataProvider.GetDataFrom5MinuteWindowCache("BBG004730N88", Constants._1minSellCacheKey); + var sberBuys = await _tradeDataProvider.GetDataFrom5MinuteWindowCache("BBG004730N88", Constants._1minBuyCacheKey); + var sells = sberSells.Sum(s => s.Value); + var buys = sberBuys.Sum(s => s.Value); + var su = sells + buys; + var dsell = (sells / su - 0.5m) * 2; + + var mavTask = CheckByWindowAverageMean(data, message, windowMaxSize); var mavTaskShorts = CheckByWindowAverageMeanForShotrs(data, message, windowMaxSize); var ltTask = CheckByLocalTrends(data, message, windowMaxSize); @@ -328,7 +366,7 @@ namespace KLHZ.Trader.Core.Exchange.Services if ((res & TradingEvent.UptrendStart) == TradingEvent.UptrendStart && !LongOpeningStops.ContainsKey(message.Figi) && trendTask.Result.HasValue - && System.Math.Abs(trendTask.Result.Value)<6 + && trendTask.Result.Value > -5 && state == ExchangeState.Open && areasTask.Result.HasValue && (areasTask.Result.Value >= 20 && areasTask.Result.Value < 75) @@ -371,59 +409,63 @@ namespace KLHZ.Trader.Core.Exchange.Services await LogDeclision(DeclisionTradeAction.OpenLong, message); } - if ((res & TradingEvent.UptrendEnd) == TradingEvent.UptrendEnd && positionTask.Result != ValueAmplitudePosition.LowerThenMediana) + if ((res & TradingEvent.UptrendEnd) == TradingEvent.UptrendEnd) { - if (!message.IsHistoricalData && BotModeSwitcher.CanSell()) + if (dsell < 0.1m) { - var loggedDeclisions = 0; - var assetsForClose = _tradeDataProvider.Accounts - .SelectMany(a => a.Value.Assets.Values) - .Where(a => a.Figi == message.Figi && a.Count > 0) - .ToArray(); - foreach (var asset in assetsForClose) + if (!message.IsHistoricalData && BotModeSwitcher.CanSell()) { - var profit = 0m; + var loggedDeclisions = 0; + var assetsForClose = _tradeDataProvider.Accounts + .SelectMany(a => a.Value.Assets.Values) + .Where(a => a.Figi == message.Figi && a.Count > 0) + .ToArray(); + foreach (var asset in assetsForClose) + { + var profit = 0m; - if (assetType == AssetType.Common && asset.Count > 0) - { - profit = TradingCalculator.CaclProfit(asset.BoughtPrice, message.Value, - GetComission(assetType), 1, false); - } - if (assetType == AssetType.Futures) - { - profit = TradingCalculator.CaclProfit(asset.BoughtPrice, message.Value, - GetComission(assetType), GetLeverage(message.Figi, asset.Count < 0), asset.Count < 0); - } - var stoppingKey = message.Figi + asset.AccountId; - if (profit > 0) - { - //ClosingStops[stoppingKey] = DateTime.UtcNow.AddSeconds(30); - var command = new TradeCommand() + if (assetType == AssetType.Common && asset.Count > 0) { - AccountId = asset.AccountId, - Figi = message.Figi, - CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketSell, - Count = (long)asset.Count, - RecomendPrice = null, - EnableMargin = false, - }; - await _dataBus.Broadcast(command); - _logger.LogWarning("Продажа актива {figi}! id команды {commandId}. Направление сделки: {dir}; Количество активов: {count}; Разрешена ли маржиналка: {margin}", - message.Figi, command.CommandId, command.CommandType, command.Count, command.EnableMargin); - if (loggedDeclisions == 0) + profit = TradingCalculator.CaclProfit(asset.BoughtPrice, message.Value, + GetComission(assetType), 1, false); + } + if (assetType == AssetType.Futures) { - loggedDeclisions++; - await LogDeclision(DeclisionTradeAction.CloseLongReal, message, profit); + profit = TradingCalculator.CaclProfit(asset.BoughtPrice, message.Value, + GetComission(assetType), GetLeverage(message.Figi, asset.Count < 0), asset.Count < 0); + } + + if (profit > 0) + { + LongClosingStops[message.Figi] = message.Time.AddSeconds(30); + var command = new TradeCommand() + { + AccountId = asset.AccountId, + Figi = message.Figi, + CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketSell, + Count = (long)asset.Count, + RecomendPrice = null, + EnableMargin = false, + }; + await _dataBus.Broadcast(command); + _logger.LogWarning("Продажа актива {figi}! id команды {commandId}. Направление сделки: {dir}; Количество активов: {count}; Разрешена ли маржиналка: {margin}", + message.Figi, command.CommandId, command.CommandType, command.Count, command.EnableMargin); + if (loggedDeclisions == 0) + { + loggedDeclisions++; + await LogDeclision(DeclisionTradeAction.CloseLongReal, message, profit); + } } } } + await LogDeclision(DeclisionTradeAction.CloseLong, message); } - await LogDeclision(DeclisionTradeAction.CloseLong, message); + } if ((mavTaskShorts.Result & TradingEvent.UptrendEnd) == TradingEvent.UptrendEnd) { - if (trendTask.Result.HasValue && trendTask.Result.Value < -3) + if (trendTask.Result.HasValue && trendTask.Result.Value < -4) { if (!message.IsHistoricalData) { @@ -541,6 +583,13 @@ namespace KLHZ.Trader.Core.Exchange.Services ShortClosingStops.TryRemove(message.Figi, out _); } } + if (LongClosingStops.TryGetValue(message.Figi, out var dt3)) + { + if (dt3 < currentTime) + { + LongClosingStops.TryRemove(message.Figi, out _); + } + } } private async Task LogPrice(INewPrice message, string processor, decimal value) diff --git a/KLHZ.Trader.Core/Exchange/Services/TraderDataProvider.cs b/KLHZ.Trader.Core/Exchange/Services/TraderDataProvider.cs index 87a56db..0df2d65 100644 --- a/KLHZ.Trader.Core/Exchange/Services/TraderDataProvider.cs +++ b/KLHZ.Trader.Core/Exchange/Services/TraderDataProvider.cs @@ -126,6 +126,16 @@ namespace KLHZ.Trader.Core.Exchange.Services await _historyCash[figi].AddDataToTimeWindowCache(key, data, TimeWindowCacheType._1_Minute); } + public async ValueTask AddDataTo5MinuteWindowCache(string figi, string key, CachedValue data) + { + if (!_historyCash.TryGetValue(figi, out var unit)) + { + unit = new PriceHistoryCacheUnit2(figi); + _historyCash.TryAdd(figi, unit); + } + await _historyCash[figi].AddDataToTimeWindowCache(key, data, TimeWindowCacheType._5_Minutes); + } + public ValueTask GetDataFrom1MinuteWindowCache(string figi, string key) { if (_historyCash.TryGetValue(figi, out var cahcheItem)) @@ -135,6 +145,15 @@ namespace KLHZ.Trader.Core.Exchange.Services return ValueTask.FromResult(Array.Empty()); } + public ValueTask GetDataFrom5MinuteWindowCache(string figi, string key) + { + if (_historyCash.TryGetValue(figi, out var cahcheItem)) + { + return cahcheItem.GetDataFromTimeWindowCache(key, TimeWindowCacheType._5_Minutes); + } + return ValueTask.FromResult(Array.Empty()); + } + public async ValueTask AddOrderbook(IOrderbook orderbook) { if (!_historyCash.TryGetValue(orderbook.Figi, out var unit)) diff --git a/KLHZ.Trader.Service/Controllers/PlayController.cs b/KLHZ.Trader.Service/Controllers/PlayController.cs index 0645238..3a2713b 100644 --- a/KLHZ.Trader.Service/Controllers/PlayController.cs +++ b/KLHZ.Trader.Service/Controllers/PlayController.cs @@ -26,20 +26,24 @@ namespace KLHZ.Trader.Service.Controllers } [HttpGet] - public async Task Run(string figi) + public async Task Run() { try { + var figi1 = "FUTIMOEXF000"; + //var figi1 = "BBG004730N88"; + var figi2 = "BBG004730N88"; + //var figi2 = "FUTIMOEXF000"; var time1 = DateTime.UtcNow.AddDays(-16.5); //var time2 = DateTime.UtcNow.AddMinutes(18); using var context1 = await _dbContextFactory.CreateDbContextAsync(); context1.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; var data = await context1.PriceChanges - .Where(c => c.Figi == figi && c.Time >= time1) + .Where(c => (c.Figi == figi1 || c.Figi == figi2) && c.Time >= time1) .OrderBy(c => c.Time) .Select(c => new NewPriceMessage() { - Figi = figi, + Figi = c.Figi, Ticker = c.Ticker, Time = c.Time, Value = c.Value,