From c3c6b52b7b10611550f18410843bb9dd3b6eb6bc Mon Sep 17 00:00:00 2001 From: vlad zverzhkhovskiy Date: Thu, 18 Sep 2025 12:16:41 +0300 Subject: [PATCH] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=20=D1=80=D0=B5=D0=B6?= =?UTF-8?q?=D0=B8=D0=BC=D0=B0=20=D1=82=D0=BE=D1=80=D0=B3=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Declisions/Utils/MovingAverage.cs | 80 +++++++ KLHZ.Trader.Core/Exchange/Services/Trader.cs | 205 +++++++++++++----- .../Exchange/Services/TraderDataProvider.cs | 5 +- .../Services/TradingCommandsExecutor.cs | 2 +- 4 files changed, 237 insertions(+), 55 deletions(-) diff --git a/KLHZ.Trader.Core.Math/Declisions/Utils/MovingAverage.cs b/KLHZ.Trader.Core.Math/Declisions/Utils/MovingAverage.cs index 0112677..a4731c6 100644 --- a/KLHZ.Trader.Core.Math/Declisions/Utils/MovingAverage.cs +++ b/KLHZ.Trader.Core.Math/Declisions/Utils/MovingAverage.cs @@ -125,5 +125,85 @@ namespace KLHZ.Trader.Core.Math.Declisions.Utils } return (res, bigWindowAv, smallWindowAv); } + + public static (TradingEvent events, decimal bigWindowAv, decimal smallWindowAv) CheckByWindowAverageMean2(DateTime[] timestamps, + decimal[] prices, int size, int smallWindow, int bigWindow, TimeSpan timeForUptreandStart, + decimal uptrendStartingDetectionMeanfullStep = 0m, decimal uptrendEndingDetectionMeanfullStep = 3m) + { + var res = TradingEvent.None; + var bigWindowAv = 0m; + var smallWindowAv = 0m; + var s = 0; + var pricesForFinalComparison = new decimal[size]; + var twavss = new decimal[size]; + var twavbs = new decimal[size]; + var times = new DateTime[size]; + var crossings = new List(); + for (int shift = 0; shift < size - 1 && shift < prices.Length - 1; shift++) + { + s = shift; + var i2 = size - 1 - shift; + var i1 = size - 2 - shift; + + var twavs = CalcTimeWindowAverageValue(timestamps, prices, smallWindow, shift); + var twavb = CalcTimeWindowAverageValue(timestamps, prices, bigWindow, shift); + pricesForFinalComparison[i2] = prices[prices.Length - 1 - shift]; + + if (shift == 0) + { + bigWindowAv = twavb.value; + smallWindowAv = twavs.value; + } + twavss[i2] = twavs.value; + twavbs[i2] = twavb.value; + times[i2] = twavb.time; + + if (shift > 0) + { + var isCrossing = Lines.IsLinesCrossing( + times[i1 + 1], + times[i2 + 1], + twavss[i1 + 1], + twavss[i2 + 1], + twavbs[i1 + 1], + twavbs[i2 + 1]); + + if (shift == 1 && !isCrossing.res) //если нет пересечения скользящих средний с окном 120 и 15 секунд между + //текущей и предыдущей точкой - можно не продолжать выполнение. + { + break; + } + + if (isCrossing.res) + { + crossings.Add(i2); + if (crossings.Count == 2) + { + // если фильтрация окном 15 наползает на окно 120 сверху, потенциальное время закрытия лонга и возможно открытия шорта + if (twavss[size - 1] <= twavbs[size - 1] && twavss[size - 2] > twavbs[size - 2]) + { + if (pricesForFinalComparison[crossings[0]] - pricesForFinalComparison[crossings[1]] >= uptrendEndingDetectionMeanfullStep + && times[crossings[0]] - times[crossings[1]] >= timeForUptreandStart) + { + res |= TradingEvent.UptrendEnd; + } + break; + } + // если фильтрация окном 120 наползает на окно 15 сверху, потенциальное время открытия лонга и закрытия шорта + if (twavss[size - 1] >= twavbs[size - 1] && twavss[size - 2] < twavbs[size - 2]) + { + if (pricesForFinalComparison[crossings[0]] - pricesForFinalComparison[crossings[1]] <= uptrendStartingDetectionMeanfullStep + && times[crossings[0]] - times[crossings[1]] >= timeForUptreandStart) + { + res |= TradingEvent.UptrendStart; + } + break; + } + } + } + } + } + return (res, bigWindowAv, smallWindowAv); + } } } diff --git a/KLHZ.Trader.Core/Exchange/Services/Trader.cs b/KLHZ.Trader.Core/Exchange/Services/Trader.cs index 8ea6fdd..834131c 100644 --- a/KLHZ.Trader.Core/Exchange/Services/Trader.cs +++ b/KLHZ.Trader.Core/Exchange/Services/Trader.cs @@ -42,7 +42,6 @@ namespace KLHZ.Trader.Core.Exchange.Services private readonly decimal _accountCashPart; private readonly decimal _accountCashPartFutures; private readonly string[] _tradingInstrumentsFigis = []; - private readonly bool _isDebug = false; private readonly Channel _pricesChannel = Channel.CreateUnbounded(); private readonly Channel _ordersbookChannel = Channel.CreateUnbounded(); @@ -61,7 +60,6 @@ namespace KLHZ.Trader.Core.Exchange.Services _accountCashPart = options.Value.AccountCashPart; _accountCashPartFutures = options.Value.AccountCashPartFutures; _tradingInstrumentsFigis = options.Value.TradingInstrumentsFigis; - _isDebug = !options.Value.ExchangeDataRecievingEnabled; foreach (var f in _tradingInstrumentsFigis) { TradingModes[f] = TradingMode.None; @@ -80,10 +78,6 @@ namespace KLHZ.Trader.Core.Exchange.Services _dataBus.AddChannel(nameof(Trader), _ordersbookChannel); _ = ProcessPrices(); _ = ProcessOrders(); - if (!_isDebug) - { - _ = TradingModeUpdatingWorker(); - } } public async ValueTask<(DateTime[] timestamps, decimal[] prices, bool isFullIntervalExists)> GetData(INewPrice message) @@ -171,27 +165,27 @@ namespace KLHZ.Trader.Core.Exchange.Services }; list.Clear(); } + } - try + try + { + if (timesCache.TryGetValue(message.Figi, out var dt)) { - if (timesCache.TryGetValue(message.Figi, out var dt)) - { - if ((message.Time - dt).TotalSeconds > 120) - { - timesCache[message.Figi] = message.Time; - - TradingModes[message.Figi] = await CalcTradingMode(message); - } - } - else + if ((message.Time - dt).TotalSeconds > 10) { timesCache[message.Figi] = message.Time; + + TradingModes[message.Figi] = await CalcTradingMode(message); } } - catch (Exception ex) + else { - + timesCache[message.Figi] = message.Time; } + } + catch (Exception ex) + { + } if (TradingModes.TryGetValue(message.Figi, out var mode)) { @@ -249,12 +243,16 @@ namespace KLHZ.Trader.Core.Exchange.Services } else if (TradingModes[message.Figi] == TradingMode.SlowDropping) { - await ProcessNewPriceIMOEXF_Dropping(data, state, message, windowMaxSize, 3); + await ProcessNewPriceIMOEXF_Dropping(data, state, message, windowMaxSize, 2); } else if (TradingModes[message.Figi] == TradingMode.Dropping) { await ProcessNewPriceIMOEXF_Dropping(data, state, message, windowMaxSize, 6); } + else if (TradingModes[message.Figi] == TradingMode.Growing) + { + await ProcessNewPriceIMOEXF_Growing(data, state, message, windowMaxSize); + } else { await ProcessNewPriceIMOEXF2(data, state, message, windowMaxSize); @@ -366,6 +364,19 @@ namespace KLHZ.Trader.Core.Exchange.Services return resultMoveAvFull.events; } + private async Task CheckByWindowAverageMean2((DateTime[] timestamps, decimal[] prices) data, int smallWindow, int bigWindow, + INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullStep = 0m, decimal uptrendEndingDetectionMeanfullStep = 3m) + { + var resultMoveAvFull = MovingAverage.CheckByWindowAverageMean2(data.timestamps, data.prices, + windowMaxSize, smallWindow, bigWindow, TimeSpan.FromSeconds(20), uptrendStartingDetectionMeanfullStep, uptrendEndingDetectionMeanfullStep); + if (resultMoveAvFull.bigWindowAv != 0) + { + await LogPrice(message, Constants.BigWindowCrossingAverageProcessor, resultMoveAvFull.bigWindowAv); + await LogPrice(message, Constants.SmallWindowCrossingAverageProcessor, resultMoveAvFull.smallWindowAv); + } + return resultMoveAvFull.events; + } + private Task CheckByWindowAverageMeanNolog((DateTime[] timestamps, decimal[] prices) data, INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullStep = 0m, decimal uptrendEndingDetectionMeanfullStep = 3m) { @@ -471,10 +482,8 @@ namespace KLHZ.Trader.Core.Exchange.Services if ((res & TradingEvent.UptrendStart) == TradingEvent.UptrendStart && !LongOpeningStops.ContainsKey(message.Figi) && trendTask.Result.HasValue - && trendTask.Result.Value > -5 && state == ExchangeState.Open && areasTask.Result.HasValue - && (areasTask.Result.Value >= 20 && areasTask.Result.Value < 75) && (positionTask.Result == ValueAmplitudePosition.LowerThenMediana) ) { @@ -633,15 +642,13 @@ namespace KLHZ.Trader.Core.Exchange.Services return; } - var mavTask = CheckByWindowAverageMean(data, message, windowMaxSize, -1, 1m); - var ltTask = CheckByLocalTrends(data, message, windowMaxSize); - var positionTask = CheckPosition(message); + var mavTask = CheckByWindowAverageMean2(data, 30, 180, message, windowMaxSize, 0, 0); - await Task.WhenAll(mavTask, ltTask, positionTask); + await Task.WhenAll(mavTask); var assetType = _tradeDataProvider.GetAssetTypeByFigi(message.Figi); - var res = mavTask.Result | ltTask.Result; + var res = mavTask.Result; - if ((res & TradingEvent.UptrendStart) == TradingEvent.UptrendStart && (positionTask.Result == ValueAmplitudePosition.None || positionTask.Result == ValueAmplitudePosition.LowerThenMediana)) + if ((res & TradingEvent.UptrendStart) == TradingEvent.UptrendStart) { if (!message.IsHistoricalData && BotModeSwitcher.CanPurchase()) { @@ -653,7 +660,7 @@ namespace KLHZ.Trader.Core.Exchange.Services { if (IsBuyAllowed(acc.Value, message.Value, 1, _accountCashPartFutures, _accountCashPart)) { - if (RandomNumberGenerator.GetInt32(100) > 50 && await acc.Value.Lock(TimeSpan.FromSeconds(12))) + if (await acc.Value.Lock(TimeSpan.FromSeconds(30))) { var command = new TradeCommand() { @@ -697,7 +704,7 @@ namespace KLHZ.Trader.Core.Exchange.Services Figi = message.Figi, CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.LimitSell, Count = (long)asset.Count, - RecomendPrice = asset.BoughtPrice + 3, + RecomendPrice = asset.BoughtPrice + 1.5m, EnableMargin = false, }; await _dataBus.Broadcast(command); @@ -707,6 +714,117 @@ namespace KLHZ.Trader.Core.Exchange.Services } } + private async Task ProcessNewPriceIMOEXF_Growing( + (DateTime[] timestamps, decimal[] prices) data, + ExchangeState state, + INewPrice message, int windowMaxSize) + { + if (data.timestamps.Length <= 4 || state != ExchangeState.Open) + { + return; + } + + var mavTask = CheckByWindowAverageMean2(data, 30, 180, message, windowMaxSize, 0, 0); + + await Task.WhenAll(mavTask); + var assetType = _tradeDataProvider.GetAssetTypeByFigi(message.Figi); + var res = mavTask.Result; + + if ((res & TradingEvent.UptrendStart) == TradingEvent.UptrendStart) + { + if (!message.IsHistoricalData && BotModeSwitcher.CanPurchase()) + { + var accounts = _tradeDataProvider.Accounts + .Where(a => !a.Value.Assets.ContainsKey(message.Figi)) + .ToArray(); + var loggedDeclisions = 0; + foreach (var acc in accounts) + { + if (IsBuyAllowed(acc.Value, message.Value, 1, _accountCashPartFutures, _accountCashPart)) + { + if (RandomNumberGenerator.GetInt32(100) > 50 && await acc.Value.Lock(TimeSpan.FromSeconds(12))) + { + var command = new TradeCommand() + { + AccountId = acc.Value.AccountId, + Figi = message.Figi, + CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.LimitBuy, + Count = 1, + RecomendPrice = message.Value - 0.5m, + ExchangeObject = acc.Value, + }; + + 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) + { + await LogDeclision(DeclisionTradeAction.OpenLongReal, message); + LongOpeningStops[message.Figi] = message.Time.AddMinutes(1); + loggedDeclisions++; + } + } + } + } + } + + await LogDeclision(DeclisionTradeAction.OpenLong, message); + } + + if ((res & TradingEvent.UptrendEnd) == TradingEvent.UptrendEnd) + { + if (!message.IsHistoricalData && BotModeSwitcher.CanSell()) + { + 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 (await asset.Lock(TimeSpan.FromSeconds(60))) + { + 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); + } + + 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); + } + } + private async Task ProcessNewPriceIMOEXF_Dropping( (DateTime[] timestamps, decimal[] prices) data, ExchangeState state, @@ -717,13 +835,12 @@ namespace KLHZ.Trader.Core.Exchange.Services return; } - var mavTask = CheckByWindowAverageMean(data, message, windowMaxSize, -1, 1m); - var ltTask = CheckByLocalTrends(data, message, windowMaxSize); + var mavTask = CheckByWindowAverageMean2(data, 30, 180, message, windowMaxSize, 0, 0); var positionTask = CheckPosition(message); - await Task.WhenAll(mavTask, ltTask, positionTask); + await Task.WhenAll(mavTask, positionTask); var assetType = _tradeDataProvider.GetAssetTypeByFigi(message.Figi); - var res = mavTask.Result | ltTask.Result; + var res = mavTask.Result; if ((res & TradingEvent.UptrendEnd) == TradingEvent.UptrendEnd && (positionTask.Result != ValueAmplitudePosition.LowerThenMediana)) { @@ -745,7 +862,7 @@ namespace KLHZ.Trader.Core.Exchange.Services Figi = message.Figi, CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketSell, Count = 1, - RecomendPrice = message.Value, + RecomendPrice = message.Value - 0.5m, ExchangeObject = acc.Value, }; @@ -977,24 +1094,6 @@ namespace KLHZ.Trader.Core.Exchange.Services return res; } - private async Task TradingModeUpdatingWorker() - { - while (true) - { - try - { - foreach (var figi in _tradingInstrumentsFigis) - { - TradingModes[figi] = await CalcTradingMode(figi); - } - await Task.Delay(120000); - } - catch (Exception ex) - { - _logger.LogError(ex, "Ошибка при вычислении режима торговли."); - } - } - } internal static bool IsBuyAllowed(ManagedAccount account, decimal boutPrice, decimal count, decimal accountCashPartFutures, decimal accountCashPart) { diff --git a/KLHZ.Trader.Core/Exchange/Services/TraderDataProvider.cs b/KLHZ.Trader.Core/Exchange/Services/TraderDataProvider.cs index 92370f2..846e626 100644 --- a/KLHZ.Trader.Core/Exchange/Services/TraderDataProvider.cs +++ b/KLHZ.Trader.Core/Exchange/Services/TraderDataProvider.cs @@ -98,6 +98,7 @@ namespace KLHZ.Trader.Core.Exchange.Services public async ValueTask AddData(INewPrice message, TimeSpan? clearingInterval = null) { + if (message.Direction != 1) return; if (_historyCash.TryGetValue(message.Figi, out var unit)) { if (clearingInterval.HasValue) @@ -215,7 +216,9 @@ namespace KLHZ.Trader.Core.Exchange.Services Ticker = c.Ticker, Time = c.Time, Value = c.Value, - IsHistoricalData = true + IsHistoricalData = true, + Direction = c.Direction, + Count = c.Count, }) .ToArrayAsync(); diff --git a/KLHZ.Trader.Core/Exchange/Services/TradingCommandsExecutor.cs b/KLHZ.Trader.Core/Exchange/Services/TradingCommandsExecutor.cs index a4c0a49..f90d410 100644 --- a/KLHZ.Trader.Core/Exchange/Services/TradingCommandsExecutor.cs +++ b/KLHZ.Trader.Core/Exchange/Services/TradingCommandsExecutor.cs @@ -103,7 +103,7 @@ namespace KLHZ.Trader.Core.Exchange.Services Ticker = _tradeDataProvider.GetTickerByFigi(tradeCommand.Figi), Count = res.LotsRequested, Direction = (DealDirection)(int)dir, - ExpirationTime = DateTime.UtcNow.AddMinutes(2), + ExpirationTime = DateTime.UtcNow.AddMinutes(10), OpenDate = DateTime.UtcNow, Price = tradeCommand.RecomendPrice.Value, };