From 270e8075914373f7aeb00076e3dba216b40b8474 Mon Sep 17 00:00:00 2001 From: vlad zverzhkhovskiy Date: Tue, 2 Sep 2025 17:10:46 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B2=D0=BE=D1=81=D1=81=D1=82=D0=B0=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B4=D0=B0=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D1=85=20=D0=BE=20=D1=86=D0=B5=D0=BD=D0=B0=D1=85=20=D0=B8?= =?UTF-8?q?=20=D1=81=D0=B4=D0=B5=D0=BB=D0=BA=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Declisions/Dtos/TradingEventsDto.cs | 16 -- .../Interfaces/IPriceHistoryCacheUnit.cs | 4 +- .../Interfaces/ITradingEventsDetector.cs | 9 - .../Services/Cache/PriceHistoryCacheUnit.cs | 4 +- .../Services/Cache/PriceHistoryCacheUnit2.cs | 4 +- .../IntervalsTradingEventsDetector.cs | 14 -- .../Declisions/Utils/MovingAverage.cs | 22 ++- .../Declisions/Enums/DeclisionTradeAction.cs | 1 + KLHZ.Trader.Core/Exchange/ExchangeConfig.cs | 1 + .../Exchange/Services/ExchangeDataReader.cs | 10 +- KLHZ.Trader.Core/Exchange/Services/Trader.cs | 172 +++++++++++++----- KLHZ.Trader.Service/Program.cs | 3 - KLHZ.Trader.Service/appsettings.json | 1 + KLHZ.Trader.sln | 1 + 14 files changed, 155 insertions(+), 107 deletions(-) delete mode 100644 KLHZ.Trader.Core.Contracts/Declisions/Dtos/TradingEventsDto.cs delete mode 100644 KLHZ.Trader.Core.Contracts/Declisions/Interfaces/ITradingEventsDetector.cs delete mode 100644 KLHZ.Trader.Core.Math/Declisions/Services/EventsDetection/IntervalsTradingEventsDetector.cs diff --git a/KLHZ.Trader.Core.Contracts/Declisions/Dtos/TradingEventsDto.cs b/KLHZ.Trader.Core.Contracts/Declisions/Dtos/TradingEventsDto.cs deleted file mode 100644 index be21e10..0000000 --- a/KLHZ.Trader.Core.Contracts/Declisions/Dtos/TradingEventsDto.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace KLHZ.Trader.Core.Contracts.Declisions.Dtos -{ - public readonly struct TradingEventsDto - { - public readonly bool LongClose; - public readonly bool LongOpen; - - public TradingEventsDto(bool longClose, bool longOpen) - { - LongClose = longClose; - LongOpen = longOpen; - } - - public readonly static TradingEventsDto Empty = new TradingEventsDto(false, false); - } -} diff --git a/KLHZ.Trader.Core.Contracts/Declisions/Interfaces/IPriceHistoryCacheUnit.cs b/KLHZ.Trader.Core.Contracts/Declisions/Interfaces/IPriceHistoryCacheUnit.cs index ad6232c..fdd930e 100644 --- a/KLHZ.Trader.Core.Contracts/Declisions/Interfaces/IPriceHistoryCacheUnit.cs +++ b/KLHZ.Trader.Core.Contracts/Declisions/Interfaces/IPriceHistoryCacheUnit.cs @@ -9,7 +9,7 @@ namespace KLHZ.Trader.Core.Contracts.Declisions.Interfaces public ValueTask AddData(INewPrice priceChange); public ValueTask<(DateTime[] timestamps, float[] prices)> GetData(); public ValueTask AddOrderbook(IOrderbook orderbook); - public long AsksCount { get; } - public long BidsCount { get; } + public decimal AsksCount { get; } + public decimal BidsCount { get; } } } diff --git a/KLHZ.Trader.Core.Contracts/Declisions/Interfaces/ITradingEventsDetector.cs b/KLHZ.Trader.Core.Contracts/Declisions/Interfaces/ITradingEventsDetector.cs deleted file mode 100644 index 47b1fd1..0000000 --- a/KLHZ.Trader.Core.Contracts/Declisions/Interfaces/ITradingEventsDetector.cs +++ /dev/null @@ -1,9 +0,0 @@ -using KLHZ.Trader.Core.Contracts.Declisions.Dtos.Enums; - -namespace KLHZ.Trader.Core.Contracts.Declisions.Interfaces -{ - public interface ITradingEventsDetector - { - public ValueTask Detect(IPriceHistoryCacheUnit unit); - } -} diff --git a/KLHZ.Trader.Core.Math/Declisions/Services/Cache/PriceHistoryCacheUnit.cs b/KLHZ.Trader.Core.Math/Declisions/Services/Cache/PriceHistoryCacheUnit.cs index aa801ba..52a30e8 100644 --- a/KLHZ.Trader.Core.Math/Declisions/Services/Cache/PriceHistoryCacheUnit.cs +++ b/KLHZ.Trader.Core.Math/Declisions/Services/Cache/PriceHistoryCacheUnit.cs @@ -20,8 +20,8 @@ namespace KLHZ.Trader.Core.Math.Declisions.Services.Cache } } - public long AsksCount => 1; - public long BidsCount => 1; + public decimal AsksCount => 1; + public decimal BidsCount => 1; private readonly object _locker = new(); private readonly float[] Prices = new float[CacheMaxLength]; diff --git a/KLHZ.Trader.Core.Math/Declisions/Services/Cache/PriceHistoryCacheUnit2.cs b/KLHZ.Trader.Core.Math/Declisions/Services/Cache/PriceHistoryCacheUnit2.cs index 9023d77..baf5a74 100644 --- a/KLHZ.Trader.Core.Math/Declisions/Services/Cache/PriceHistoryCacheUnit2.cs +++ b/KLHZ.Trader.Core.Math/Declisions/Services/Cache/PriceHistoryCacheUnit2.cs @@ -21,7 +21,7 @@ namespace KLHZ.Trader.Core.Math.Declisions.Services.Cache } } - public long AsksCount + public decimal AsksCount { get { @@ -32,7 +32,7 @@ namespace KLHZ.Trader.Core.Math.Declisions.Services.Cache } } - public long BidsCount + public decimal BidsCount { get { diff --git a/KLHZ.Trader.Core.Math/Declisions/Services/EventsDetection/IntervalsTradingEventsDetector.cs b/KLHZ.Trader.Core.Math/Declisions/Services/EventsDetection/IntervalsTradingEventsDetector.cs deleted file mode 100644 index 2d3d4ca..0000000 --- a/KLHZ.Trader.Core.Math/Declisions/Services/EventsDetection/IntervalsTradingEventsDetector.cs +++ /dev/null @@ -1,14 +0,0 @@ -using KLHZ.Trader.Core.Contracts.Declisions.Dtos.Enums; -using KLHZ.Trader.Core.Contracts.Declisions.Interfaces; -using KLHZ.Trader.Core.Math.Declisions.Utils; - -namespace KLHZ.Trader.Core.Math.Declisions.Services.EventsDetection -{ - public class IntervalsTradingEventsDetector : ITradingEventsDetector - { - public ValueTask Detect(IPriceHistoryCacheUnit unit) - { - return ValueTask.FromResult(TwoPeriods.Detect(unit)); - } - } -} diff --git a/KLHZ.Trader.Core.Math/Declisions/Utils/MovingAverage.cs b/KLHZ.Trader.Core.Math/Declisions/Utils/MovingAverage.cs index 8784a47..61c21ed 100644 --- a/KLHZ.Trader.Core.Math/Declisions/Utils/MovingAverage.cs +++ b/KLHZ.Trader.Core.Math/Declisions/Utils/MovingAverage.cs @@ -1,4 +1,4 @@ -using KLHZ.Trader.Core.Contracts.Declisions.Dtos; +using KLHZ.Trader.Core.Contracts.Declisions.Dtos.Enums; using KLHZ.Trader.Core.Math.Common; namespace KLHZ.Trader.Core.Math.Declisions.Utils @@ -18,12 +18,12 @@ namespace KLHZ.Trader.Core.Math.Declisions.Utils return (startTime, sum / count); } - public static TradingEventsDto CheckByWindowAverageMean(DateTime[] timestamps, float[] prices, int size, float meanfullStep = 3f) + public static TradingEvent CheckByWindowAverageMean(DateTime[] timestamps, float[] prices, int size, float meanfullStep = 3f) { var twav15s = new float[size]; var twav120s = new float[size]; var times = new DateTime[size]; - + var res = TradingEvent.None; for (int shift = 0; shift < size; shift++) { var twav15 = CalcTimeWindowAverageValue(timestamps, prices, 15, shift); @@ -31,7 +31,11 @@ namespace KLHZ.Trader.Core.Math.Declisions.Utils twav15s[size - 1 - shift] = twav15.value; twav120s[size - 1 - shift] = twav120.value; times[size - 1 - shift] = twav120.time; - + if (System.Math.Abs(twav120.value - prices[prices.Length - 1]) > 3 * meanfullStep) + { + res |= TradingEvent.StopBuy; + return res; + } if (shift > 0) { var isCrossing = Lines.IsLinesCrossing( @@ -53,7 +57,9 @@ namespace KLHZ.Trader.Core.Math.Declisions.Utils { if (twav15s[size - 1 - shift] - twav15s[size - 1] >= meanfullStep) { - return new TradingEventsDto(false, true); + res |= TradingEvent.LongOpen; + res |= TradingEvent.ShortClose; + break; } } @@ -62,14 +68,16 @@ namespace KLHZ.Trader.Core.Math.Declisions.Utils { if (twav15s[size - 1 - shift] - twav15s[size - 1] <= -meanfullStep) { - return new TradingEventsDto(true, false); + res |= TradingEvent.LongClose; + res |= TradingEvent.ShortOpen; + break; } } } } } - return new TradingEventsDto(false, false); + return res; } } diff --git a/KLHZ.Trader.Core/DataLayer/Entities/Declisions/Enums/DeclisionTradeAction.cs b/KLHZ.Trader.Core/DataLayer/Entities/Declisions/Enums/DeclisionTradeAction.cs index 66577f1..8aab2f2 100644 --- a/KLHZ.Trader.Core/DataLayer/Entities/Declisions/Enums/DeclisionTradeAction.cs +++ b/KLHZ.Trader.Core/DataLayer/Entities/Declisions/Enums/DeclisionTradeAction.cs @@ -3,6 +3,7 @@ public enum DeclisionTradeAction { Unknown = 0, + StopBuy = 1, OpenLong = 100, CloseLong = 200, OpenShort = 300, diff --git a/KLHZ.Trader.Core/Exchange/ExchangeConfig.cs b/KLHZ.Trader.Core/Exchange/ExchangeConfig.cs index ef535f7..35020d7 100644 --- a/KLHZ.Trader.Core/Exchange/ExchangeConfig.cs +++ b/KLHZ.Trader.Core/Exchange/ExchangeConfig.cs @@ -3,6 +3,7 @@ public class ExchangeConfig { public bool ExchangeDataRecievingEnabled { get; set; } + public decimal StopBuyLengthMinuts { get; set; } public decimal FutureComission { get; set; } public decimal ShareComission { get; set; } public decimal AccountCashPart { get; set; } diff --git a/KLHZ.Trader.Core/Exchange/Services/ExchangeDataReader.cs b/KLHZ.Trader.Core/Exchange/Services/ExchangeDataReader.cs index 526d71d..54039b4 100644 --- a/KLHZ.Trader.Core/Exchange/Services/ExchangeDataReader.cs +++ b/KLHZ.Trader.Core/Exchange/Services/ExchangeDataReader.cs @@ -132,10 +132,16 @@ namespace KLHZ.Trader.Core.Exchange.Services await stream.RequestStream.WriteAsync(new MarketDataRequest { SubscribeLastPriceRequest = request, - SubscribeTradesRequest = tradesRequest, - SubscribeOrderBookRequest = bookRequest }); + await stream.RequestStream.WriteAsync(new MarketDataRequest + { + SubscribeTradesRequest = tradesRequest, + }); + await stream.RequestStream.WriteAsync(new MarketDataRequest + { + SubscribeOrderBookRequest = bookRequest + }); using var context = await _dbContextFactory.CreateDbContextAsync(); context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; var pricesBuffer = new List(); diff --git a/KLHZ.Trader.Core/Exchange/Services/Trader.cs b/KLHZ.Trader.Core/Exchange/Services/Trader.cs index 21898f2..076da5e 100644 --- a/KLHZ.Trader.Core/Exchange/Services/Trader.cs +++ b/KLHZ.Trader.Core/Exchange/Services/Trader.cs @@ -10,6 +10,7 @@ using KLHZ.Trader.Core.DataLayer.Entities.Declisions.Enums; using KLHZ.Trader.Core.Exchange.Extentions; using KLHZ.Trader.Core.Exchange.Models; using KLHZ.Trader.Core.Math.Declisions.Services.Cache; +using KLHZ.Trader.Core.Math.Declisions.Utils; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -29,25 +30,26 @@ namespace KLHZ.Trader.Core.Exchange.Services private readonly IDataBus _dataBus; private readonly BotModeSwitcher _botModeSwitcher; private readonly IDbContextFactory _dbContextFactory; + private readonly ConcurrentDictionary BuyStops = new(); private readonly ConcurrentDictionary Accounts = new(); private readonly ConcurrentDictionary _historyCash = new(); - private readonly ITradingEventsDetector _tradingEventsDetector; private readonly ILogger _logger; + private readonly double _buyStopLength; private readonly decimal _futureComission; private readonly decimal _shareComission; private readonly decimal _accountCashPart; private readonly decimal _accountCashPartFutures; private readonly decimal _defaultBuyPartOfAccount; private readonly string[] _managedAccountsNamePatterns = []; + private readonly string[] _tradingInstrumentsFigis = []; private readonly Channel _pricesChannel = Channel.CreateUnbounded(); private readonly Channel _ordersbookChannel = Channel.CreateUnbounded(); - + private readonly CancellationTokenSource _cts = new(); public Trader( ILogger logger, - ITradingEventsDetector tradingEventsDetector, BotModeSwitcher botModeSwitcher, IServiceProvider provider, IOptions options, @@ -56,7 +58,6 @@ namespace KLHZ.Trader.Core.Exchange.Services InvestApiClient investApiClient) { _logger = logger; - _tradingEventsDetector = tradingEventsDetector; _botModeSwitcher = botModeSwitcher; _dataBus = dataBus; _provider = provider; @@ -68,10 +69,13 @@ namespace KLHZ.Trader.Core.Exchange.Services _accountCashPart = options.Value.AccountCashPart; _accountCashPartFutures = options.Value.AccountCashPartFutures; _defaultBuyPartOfAccount = options.Value.DefaultBuyPartOfAccount; + _tradingInstrumentsFigis = options.Value.TradingInstrumentsFigis; + _buyStopLength = (double)options.Value.StopBuyLengthMinuts; } public async Task StartAsync(CancellationToken cancellationToken) { + //await InitStops(); var accounts = await _investApiClient.GetAccounts(_managedAccountsNamePatterns); var accountsList = new List(); int i = 0; @@ -94,6 +98,20 @@ namespace KLHZ.Trader.Core.Exchange.Services _dataBus.AddChannel(nameof(Trader), _ordersbookChannel); _ = ProcessPrices(); _ = ProcessOrdersbooks(); + _ = BackgroundWorker(); + } + + private async Task InitStops() + { + using var context = await _dbContextFactory.CreateDbContextAsync(); + context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + var dt = DateTime.UtcNow.AddMinutes(-_buyStopLength); + var stops = await context.Declisions.Where(d => d.Time > dt && d.Action == DeclisionTradeAction.StopBuy).ToArrayAsync(); + foreach (var stop in stops) + { + var time = stop.Time.AddMinutes(_buyStopLength); + BuyStops.TryAdd(stop.Figi, time); + } } private async Task ProcessPrices() @@ -101,54 +119,86 @@ namespace KLHZ.Trader.Core.Exchange.Services while (await _pricesChannel.Reader.WaitToReadAsync()) { var message = await _pricesChannel.Reader.ReadAsync(); - if (_historyCash.TryGetValue(message.Figi, out var data)) - { - await data.AddData(message); - } - else - { - data = new PriceHistoryCacheUnit2(message.Figi, message); - _historyCash.TryAdd(message.Figi, data); - } - var result = await _tradingEventsDetector.Detect(data); + //if (_tradingInstrumentsFigis.Contains(message.Figi)) + //{ + // if (_historyCash.TryGetValue(message.Figi, out var unit)) + // { + // await unit.AddData(message); + // } + // else + // { + // unit = new PriceHistoryCacheUnit2(message.Figi, message); + // _historyCash.TryAdd(message.Figi, unit); + // } + // var data = await unit.GetData(); + // var declisionsForSave = new List(); + // if (message.Figi == "FUTIMOEXF000") + // { + // var result = MovingAverage.CheckByWindowAverageMean(data.timestamps, data.prices, 100, 3f); + // if ((result & TradingEvent.StopBuy) == TradingEvent.StopBuy) + // { + // var stopTo = DateTime.UtcNow.AddMinutes(_buyStopLength); + // BuyStops.AddOrUpdate(message.Figi, stopTo, (k, v) => stopTo); + // declisionsForSave.Add(new Declision() + // { + // AccountId = string.Empty, + // Figi = message.Figi, + // Ticker = message.Ticker, + // Price = message.Value, + // Time = message.IsHistoricalData ? message.Time : DateTime.UtcNow, + // Action = DeclisionTradeAction.StopBuy, + // }); + // } - try - { - if ((result & TradingEvent.LongOpen) == TradingEvent.LongOpen) - { - using var context = await _dbContextFactory.CreateDbContextAsync(); - context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - await context.Declisions.AddAsync(new Declision() - { - AccountId = string.Empty, - Figi = message.Figi, - Ticker = message.Ticker, - Price = message.Value, - Time = message.IsHistoricalData ? message.Time : DateTime.UtcNow, - Action = DeclisionTradeAction.OpenLong, - }); - await context.SaveChangesAsync(); - } - if ((result & TradingEvent.LongClose) == TradingEvent.LongClose) - { - using var context = await _dbContextFactory.CreateDbContextAsync(); - context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - await context.Declisions.AddAsync(new Declision() - { - AccountId = string.Empty, - Figi = message.Figi, - Ticker = message.Ticker, - Price = message.Value, - Time = message.IsHistoricalData ? message.Time : DateTime.UtcNow, - Action = DeclisionTradeAction.CloseLong, - }); - await context.SaveChangesAsync(); - } - } - catch (Exception ex) - { + // if ((result & TradingEvent.LongOpen) == TradingEvent.LongOpen + // && !BuyStops.TryGetValue(message.Figi, out _)) + // { + // var stopTo = DateTime.UtcNow.AddMinutes(_buyStopLength); + // BuyStops.AddOrUpdate(message.Figi, stopTo, (k, v) => stopTo); + // declisionsForSave.Add(new Declision() + // { + // AccountId = string.Empty, + // Figi = message.Figi, + // Ticker = message.Ticker, + // Price = message.Value, + // Time = message.IsHistoricalData ? message.Time : DateTime.UtcNow, + // Action = DeclisionTradeAction.OpenLong, + // }); + // } - } + // if ((result & TradingEvent.LongClose) == TradingEvent.LongClose) + // { + // declisionsForSave.Add(new Declision() + // { + // AccountId = string.Empty, + // Figi = message.Figi, + // Ticker = message.Ticker, + // Price = message.Value, + // Time = message.IsHistoricalData ? message.Time : DateTime.UtcNow, + // Action = DeclisionTradeAction.CloseLong, + // }); + // } + + // if ((result & TradingEvent.ShortOpen) == TradingEvent.ShortOpen && (unit.AsksCount/ unit.BidsCount>2 )) + // { + // declisionsForSave.Add(new Declision() + // { + // AccountId = string.Empty, + // Figi = message.Figi, + // Ticker = message.Ticker, + // Price = message.Value, + // Time = message.IsHistoricalData ? message.Time : DateTime.UtcNow, + // Action = DeclisionTradeAction.OpenShort, + // }); + // } + + // using var context = await _dbContextFactory.CreateDbContextAsync(); + // context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + + // await context.AddRangeAsync(declisionsForSave); + // await context.SaveChangesAsync(); + // } + //} } } @@ -166,8 +216,30 @@ namespace KLHZ.Trader.Core.Exchange.Services } } + private async Task BackgroundWorker() + { + var keysForRemove = new List(); + while (!_cts.IsCancellationRequested) + { + var time = DateTime.UtcNow; + foreach (var kvp in BuyStops) + { + if (kvp.Value > time) + { + keysForRemove.Add(kvp.Key); + } + } + foreach (var key in keysForRemove) + { + BuyStops.TryRemove(key, out _); + } + await Task.Delay(10000); + } + } + public Task StopAsync(CancellationToken cancellationToken) { + _cts.Cancel(); return Task.CompletedTask; } diff --git a/KLHZ.Trader.Service/Program.cs b/KLHZ.Trader.Service/Program.cs index de04e35..30a0081 100644 --- a/KLHZ.Trader.Service/Program.cs +++ b/KLHZ.Trader.Service/Program.cs @@ -1,11 +1,9 @@ using KLHZ.Trader.Core.Common; using KLHZ.Trader.Core.Common.Messaging.Services; -using KLHZ.Trader.Core.Contracts.Declisions.Interfaces; using KLHZ.Trader.Core.Contracts.Messaging.Interfaces; using KLHZ.Trader.Core.DataLayer; using KLHZ.Trader.Core.Exchange; using KLHZ.Trader.Core.Exchange.Services; -using KLHZ.Trader.Core.Math.Declisions.Services.EventsDetection; using KLHZ.Trader.Core.TG; using KLHZ.Trader.Core.TG.Services; using KLHZ.Trader.Service.Infrastructure; @@ -53,7 +51,6 @@ builder.Services.AddHostedService(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); -builder.Services.AddSingleton(); for (int i = 0; i < 10; i++) { diff --git a/KLHZ.Trader.Service/appsettings.json b/KLHZ.Trader.Service/appsettings.json index 33976e3..aec7101 100644 --- a/KLHZ.Trader.Service/appsettings.json +++ b/KLHZ.Trader.Service/appsettings.json @@ -8,6 +8,7 @@ }, "LokiUrl": "", "ExchangeConfig": { + "StopBuyLengthMinuts": 20, "ExchangeDataRecievingEnabled": true, "Token": "", "ManagingAccountNamePatterns": [ "автотрейд 1" ], diff --git a/KLHZ.Trader.sln b/KLHZ.Trader.sln index 8551b6e..0b05253 100644 --- a/KLHZ.Trader.sln +++ b/KLHZ.Trader.sln @@ -28,6 +28,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "postrgres", "postrgres", "{ KLHZ.Trader.Infrastructure\postgres\init.sql = KLHZ.Trader.Infrastructure\postgres\init.sql KLHZ.Trader.Infrastructure\postgres\migration1.sql = KLHZ.Trader.Infrastructure\postgres\migration1.sql KLHZ.Trader.Infrastructure\postgres\migration2.sql = KLHZ.Trader.Infrastructure\postgres\migration2.sql + KLHZ.Trader.Infrastructure\postgres\migration3.sql = KLHZ.Trader.Infrastructure\postgres\migration3.sql EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "loki", "loki", "{63D21DAF-FDF0-4F2D-A671-E9E59BB0CA5B}"