diff --git a/KLHZ.Trader.Core.Contracts/Messaging/Dtos/Enums/TradeCommandType.cs b/KLHZ.Trader.Core.Contracts/Messaging/Dtos/Enums/TradeCommandType.cs index 9ecada1..abf2c90 100644 --- a/KLHZ.Trader.Core.Contracts/Messaging/Dtos/Enums/TradeCommandType.cs +++ b/KLHZ.Trader.Core.Contracts/Messaging/Dtos/Enums/TradeCommandType.cs @@ -7,5 +7,7 @@ MarketBuy = 1, MarketSell = 101, LimitBuy = 200, + LimitSell = 300, + CancelOrder = 400, } } diff --git a/KLHZ.Trader.Core.Contracts/Messaging/Dtos/Interfaces/ILockableObject.cs b/KLHZ.Trader.Core.Contracts/Messaging/Dtos/Interfaces/ILockableObject.cs index 690241a..987503d 100644 --- a/KLHZ.Trader.Core.Contracts/Messaging/Dtos/Interfaces/ILockableObject.cs +++ b/KLHZ.Trader.Core.Contracts/Messaging/Dtos/Interfaces/ILockableObject.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces +namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces { public interface ILockableObject { diff --git a/KLHZ.Trader.Core.Contracts/Messaging/Dtos/Interfaces/ITradeCommand.cs b/KLHZ.Trader.Core.Contracts/Messaging/Dtos/Interfaces/ITradeCommand.cs index e3e8494..7d36b98 100644 --- a/KLHZ.Trader.Core.Contracts/Messaging/Dtos/Interfaces/ITradeCommand.cs +++ b/KLHZ.Trader.Core.Contracts/Messaging/Dtos/Interfaces/ITradeCommand.cs @@ -10,7 +10,8 @@ namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces public decimal? RecomendPrice { get; } public long Count { get; } public string AccountId { get; } + public string? OrderId { get; } public bool EnableMargin { get; } - public ILockableObject? ExchangeObject { get; } + public ILockableObject? ExchangeObject { get; } } } diff --git a/KLHZ.Trader.Core.Contracts/Messaging/Dtos/TradeCommand.cs b/KLHZ.Trader.Core.Contracts/Messaging/Dtos/TradeCommand.cs index 5da9d04..146aa1a 100644 --- a/KLHZ.Trader.Core.Contracts/Messaging/Dtos/TradeCommand.cs +++ b/KLHZ.Trader.Core.Contracts/Messaging/Dtos/TradeCommand.cs @@ -13,5 +13,6 @@ namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos public required string AccountId { get; init; } public bool EnableMargin { get; init; } = true; public ILockableObject? ExchangeObject { get; init; } + public string? OrderId { get; init; } } } diff --git a/KLHZ.Trader.Core.Tests/TraderTests.cs b/KLHZ.Trader.Core.Tests/TraderTests.cs index 8dd80c4..0476505 100644 --- a/KLHZ.Trader.Core.Tests/TraderTests.cs +++ b/KLHZ.Trader.Core.Tests/TraderTests.cs @@ -94,7 +94,7 @@ namespace KLHZ.Trader.Core.Tests [Test] public void CalcProfitTest() { - var profit = TradingCalculator.CaclProfit(2990, 2985m, 0.0025m, 10.3m, false); + var profit = TradingCalculator.CaclProfit(2990, 2987m, 0.0025m, 7.9m, true); } } } diff --git a/KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/ManagedAccount.cs b/KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/ManagedAccount.cs index 7132619..b1ffafb 100644 --- a/KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/ManagedAccount.cs +++ b/KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/ManagedAccount.cs @@ -37,6 +37,7 @@ namespace KLHZ.Trader.Core.Exchange.Models.AssetsAccounting } internal readonly ConcurrentDictionary Assets = new(); + internal readonly ConcurrentDictionary Orders = new(); public ManagedAccount(string accountId) { diff --git a/KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/Order.cs b/KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/Order.cs new file mode 100644 index 0000000..4beef7c --- /dev/null +++ b/KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/Order.cs @@ -0,0 +1,15 @@ +namespace KLHZ.Trader.Core.Exchange.Models.AssetsAccounting +{ + public class Order + { + public required string AccountId { get; init; } + public required string Figi { get; init; } + public required string Ticker { get; init; } + public required string OrderId { get; init; } + public decimal Price { get; init; } + public long Count { get; init; } + public DateTime ExpirationTime { get; init; } + public DateTime OpenDate { get; init; } + public DealDirection Direction { get; init; } + } +} diff --git a/KLHZ.Trader.Core/Exchange/Models/Trading/DeferredTrade.cs b/KLHZ.Trader.Core/Exchange/Models/Trading/DeferredTrade.cs deleted file mode 100644 index ec9aabf..0000000 --- a/KLHZ.Trader.Core/Exchange/Models/Trading/DeferredTrade.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace KLHZ.Trader.Core.Exchange.Models.Trading -{ - internal class DeferredTrade - { - public required string Figi { get; set; } - public decimal Price { get; set; } - public DateTime Time { get; set; } - } -} diff --git a/KLHZ.Trader.Core/Exchange/Models/Trading/TradingMode.cs b/KLHZ.Trader.Core/Exchange/Models/Trading/TradingMode.cs new file mode 100644 index 0000000..e9afa8a --- /dev/null +++ b/KLHZ.Trader.Core/Exchange/Models/Trading/TradingMode.cs @@ -0,0 +1,11 @@ +namespace KLHZ.Trader.Core.Exchange.Models.Trading +{ + public enum TradingMode + { + None = 0, + Stable = 1, + SlowDropping = -1, + Growing = 2, + Dropping = -2, + } +} diff --git a/KLHZ.Trader.Core/Exchange/Services/Trader.cs b/KLHZ.Trader.Core/Exchange/Services/Trader.cs index 2305923..9b27f9e 100644 --- a/KLHZ.Trader.Core/Exchange/Services/Trader.cs +++ b/KLHZ.Trader.Core/Exchange/Services/Trader.cs @@ -30,6 +30,9 @@ namespace KLHZ.Trader.Core.Exchange.Services private readonly IDataBus _dataBus; private readonly TraderDataProvider _tradeDataProvider; private readonly ILogger _logger; + + private readonly ConcurrentDictionary TradingModes = new(); + private readonly ConcurrentDictionary LongOpeningStops = new(); private readonly ConcurrentDictionary LongClosingStops = new(); private readonly ConcurrentDictionary ShortClosingStops = new(); @@ -40,7 +43,7 @@ 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(); @@ -59,6 +62,11 @@ 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; + } foreach (var lev in options.Value.InstrumentsSettings) { @@ -72,6 +80,11 @@ namespace KLHZ.Trader.Core.Exchange.Services _dataBus.AddChannel(nameof(Trader), _pricesChannel); _dataBus.AddChannel(nameof(Trader), _ordersbookChannel); _ = ProcessPrices(); + _ = ProcessOrders(); + if (!_isDebug) + { + _ = TradingModeUpdatingWorker(); + } } public async ValueTask<(DateTime[] timestamps, decimal[] prices, bool isFullIntervalExists)> GetData(INewPrice message) @@ -122,9 +135,12 @@ namespace KLHZ.Trader.Core.Exchange.Services private async Task ProcessPrices() { var pricesCache = new Dictionary>(); + var timesCache = new Dictionary(); while (await _pricesChannel.Reader.WaitToReadAsync()) { var message = await _pricesChannel.Reader.ReadAsync(); + + #region Ускорение обработки исторических данных при отладке if (message.IsHistoricalData) { await _tradeDataProvider.AddData(message, TimeSpan.FromHours(6)); @@ -154,7 +170,32 @@ namespace KLHZ.Trader.Core.Exchange.Services }; list.Clear(); } + + try + { + 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 + { + timesCache[message.Figi] = message.Time; + } + } + catch(Exception ex) + { + + } } + + await LogPrice(message, "trading_mode", (int)TradingModes[message.Figi]); + //continue; + #endregion if (message.Figi == "BBG004730N88") { if (message.Direction == 1) @@ -191,15 +232,27 @@ namespace KLHZ.Trader.Core.Exchange.Services try { - if (message.Figi == "FUTIMOEXF000" && message.Direction == 1) + ProcessStops(message, currentTime); + var windowMaxSize = 2000; + await SellAssetsIfNeed(message); + var data = await _tradeDataProvider.GetData(message.Figi, windowMaxSize); + var state = ExchangeScheduler.GetCurrentState(message.Time); + await ProcessClearing(data, state, message); + + if (TradingModes[message.Figi] == TradingMode.Stable) + { + await ProcessNewPriceIMOEXF_Stable(data, state, message, windowMaxSize); + } + else if (TradingModes[message.Figi] == TradingMode.SlowDropping) + { + await ProcessNewPriceIMOEXF_Dropping(data,state,message,windowMaxSize,3); + } + else if (TradingModes[message.Figi] == TradingMode.Dropping) + { + await ProcessNewPriceIMOEXF_Dropping(data, state, message, windowMaxSize, 6); + } + else { - ProcessStops(message, currentTime); - var windowMaxSize = 2000; - await SellAssetsIfNeed(message); - var data = await _tradeDataProvider.GetData(message.Figi, windowMaxSize); - var state = ExchangeScheduler.GetCurrentState(message.Time); - await ProcessClearing(data, state, message); - await ProcessNewPriceIMOEXF2(data, state, message, windowMaxSize); } } @@ -211,6 +264,43 @@ namespace KLHZ.Trader.Core.Exchange.Services } } + private async Task ProcessOrders() + { + while (true) + { + await ProcessOrdersAction(); + await Task.Delay(5000); + } + } + + private async Task ProcessOrdersAction(bool cancellAll = false, string? figi = null) + { + var accounts = _tradeDataProvider.Accounts.Values.ToArray(); + foreach (var account in accounts) + { + foreach (var order in account.Orders) + { + if (!string.IsNullOrEmpty(figi)) + { + if (order.Value.Figi != figi) + { + continue; + } + } + if (cancellAll || order.Value.ExpirationTime < DateTime.UtcNow) + { + await _dataBus.Broadcast(new TradeCommand() + { + AccountId = account.AccountId, + Figi = "", + OrderId = order.Key, + CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.CancelOrder, + }); + } + } + } + } + private async Task SellAssetsIfNeed(INewPrice message) { if (!BotModeSwitcher.CanSell()) @@ -266,29 +356,24 @@ namespace KLHZ.Trader.Core.Exchange.Services return resultMoveAvFull.events; } - private async Task CheckByWindowAverageMeanNolog((DateTime[] timestamps, decimal[] prices) data, + private Task CheckByWindowAverageMeanNolog((DateTime[] timestamps, decimal[] prices) data, INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullStep = 0m, decimal uptrendEndingDetectionMeanfullStep = 3m) { var resultMoveAvFull = MovingAverage.CheckByWindowAverageMean(data.timestamps, data.prices, windowMaxSize, 30, 180, TimeSpan.FromSeconds(20), uptrendStartingDetectionMeanfullStep, uptrendEndingDetectionMeanfullStep); - return resultMoveAvFull.events; + return Task.FromResult(resultMoveAvFull.events); } - private async Task CheckByWindowAverageMeanForShotrs((DateTime[] timestamps, decimal[] prices) data, + private Task CheckByWindowAverageMeanForShotrs((DateTime[] timestamps, decimal[] prices) data, INewPrice message, int windowMaxSize) { var resultMoveAvFull = MovingAverage.CheckByWindowAverageMean(data.timestamps, data.prices, windowMaxSize, 30, 240, TimeSpan.FromSeconds(20), -1m, 1m); - if (resultMoveAvFull.bigWindowAv != 0) - { - //await LogPrice(message, Constants.BigWindowCrossingAverageProcessor, resultMoveAvFull.bigWindowAv); - //await LogPrice(message, Constants.SmallWindowCrossingAverageProcessor, resultMoveAvFull.smallWindowAv); - } - return resultMoveAvFull.events; + return Task.FromResult(resultMoveAvFull.events); } private Task CheckByLocalTrends((DateTime[] timestamps, decimal[] prices) data, - INewPrice message, int windowMaxSize) + INewPrice message, int windowMaxSize) { var res = TradingEvent.None; if (LocalTrends.TryGetLocalTrends(data.timestamps, data.prices, TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(20), 1, out var resLocalTrends)) @@ -299,14 +384,6 @@ namespace KLHZ.Trader.Core.Exchange.Services res |= TradingEvent.DowntrendEnd; } } - //if (LocalTrends.TryGetLocalTrends(data.timestamps, data.prices, TimeSpan.FromSeconds(90), TimeSpan.FromSeconds(30), 2, out var resLocalTrends2)) - //{ - // res |= (resLocalTrends & TradingEvent.DowntrendEnd); - //} - //if (LocalTrends.TryGetLocalTrends(data.timestamps, data.prices, TimeSpan.FromSeconds(90), TimeSpan.FromSeconds(20), 2.5, out var resLocalTrends3)) - //{ - // res |= (resLocalTrends & TradingEvent.DowntrendStart); - //} return Task.FromResult(res); } @@ -575,6 +652,221 @@ namespace KLHZ.Trader.Core.Exchange.Services } } + private async Task ProcessNewPriceIMOEXF_Stable( + (DateTime[] timestamps, decimal[] prices) data, + ExchangeState state, + INewPrice message, int windowMaxSize) + { + if (data.timestamps.Length <= 4 || state!=ExchangeState.Open) + { + return; + } + + var mavTask = CheckByWindowAverageMean(data, message, windowMaxSize, -1, 1m); + var ltTask = CheckByLocalTrends(data, message, windowMaxSize); + var positionTask = CheckPosition(message); + + await Task.WhenAll(mavTask, ltTask, positionTask); + var assetType = _tradeDataProvider.GetAssetTypeByFigi(message.Figi); + var res = mavTask.Result | ltTask.Result; + + if ((res & TradingEvent.UptrendStart) == TradingEvent.UptrendStart && (positionTask.Result == ValueAmplitudePosition.None || positionTask.Result == ValueAmplitudePosition.LowerThenMediana)) + { + 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); + } + + foreach (var acc in _tradeDataProvider.Accounts) + { + if (acc.Value.Assets.TryGetValue(message.Figi, out var asset)) + { + var order = acc.Value.Orders.Values.FirstOrDefault(o => o.Figi == message.Figi && o.Direction == DealDirection.Sell); + if (order == null && asset.Count>0) + { + var command = new TradeCommand() + { + AccountId = asset.AccountId, + Figi = message.Figi, + CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.LimitSell, + Count = (long)asset.Count, + RecomendPrice = asset.BoughtPrice + 3, + EnableMargin = false, + }; + await _dataBus.Broadcast(command); + } + } + } + } + + private async Task ProcessNewPriceIMOEXF_Dropping( + (DateTime[] timestamps, decimal[] prices) data, + ExchangeState state, + INewPrice message, int windowMaxSize, decimal step) + { + if (data.timestamps.Length <= 4 && state !=ExchangeState.Open) + { + return; + } + + var mavTask = CheckByWindowAverageMean(data, message, windowMaxSize, -1, 1m); + var ltTask = CheckByLocalTrends(data, message, windowMaxSize); + var positionTask = CheckPosition(message); + + await Task.WhenAll(mavTask, ltTask, positionTask); + var assetType = _tradeDataProvider.GetAssetTypeByFigi(message.Figi); + var res = mavTask.Result | ltTask.Result; + + if ((res & TradingEvent.UptrendEnd) == TradingEvent.UptrendEnd && (positionTask.Result != ValueAmplitudePosition.LowerThenMediana)) + { + if (!message.IsHistoricalData && BotModeSwitcher.CanSell()) + { + 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.MarketSell, + Count = 1, + RecomendPrice = message.Value, + 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.OpenShort, message); + } + + if ((res & TradingEvent.UptrendStart) == TradingEvent.UptrendStart) + { + if (!ShortClosingStops.ContainsKey(message.Figi)) + { + if (!message.IsHistoricalData && BotModeSwitcher.CanPurchase()) + { + 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.Futures) + { + profit = TradingCalculator.CaclProfit(asset.BoughtPrice, message.Value, + GetComission(assetType), GetLeverage(message.Figi, asset.Count < 0), asset.Count < 0); + } + if (profit > 0) + { + var command = new TradeCommand() + { + AccountId = asset.AccountId, + Figi = message.Figi, + CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketBuy, + Count = System.Math.Abs((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.CloseShortReal, message, profit); + } + } + } + } + } + + if (message.IsHistoricalData) + { + ShortClosingStops[message.Figi] = message.Time.AddSeconds(30); + } + await LogDeclision(DeclisionTradeAction.CloseShort, message); + } + } + foreach (var acc in _tradeDataProvider.Accounts) + { + if (acc.Value.Assets.TryGetValue(message.Figi, out var asset)) + { + var order = acc.Value.Orders.Values.FirstOrDefault(o => o.Figi == message.Figi && o.Direction == DealDirection.Buy); + if (order == null && asset.Count<0) + { + var command = new TradeCommand() + { + AccountId = asset.AccountId, + Figi = message.Figi, + CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.LimitBuy, + Count = System.Math.Abs((long)asset.Count), + RecomendPrice = asset.BoughtPrice - step, + EnableMargin = false, + }; + await _dataBus.Broadcast(command); + } + } + } + } + private async Task ProcessClearing((DateTime[] timestamps, decimal[] prices) data, ExchangeState state, INewPrice message) { if (state == ExchangeState.ClearingTime @@ -668,6 +960,63 @@ namespace KLHZ.Trader.Core.Exchange.Services return res; } + private async Task CalcTradingMode(string figi) + { + var res = TradingMode.None; + var largeData = await _tradeDataProvider.GetData(figi, TimeSpan.FromMinutes(90)); + var smallData = await _tradeDataProvider.GetData(figi, TimeSpan.FromMinutes(15)); + + if (largeData.isFullIntervalExists && smallData.isFullIntervalExists) + { + if (LocalTrends.TryCalcTrendDiff(largeData.timestamps, largeData.prices, out var largeDataRes) + && LocalTrends.TryCalcTrendDiff(smallData.timestamps, smallData.prices, out var smallDataRes)) + { + if (largeDataRes>0 && largeDataRes <= 4 && System.Math.Abs(smallDataRes)<3) + { + res = TradingMode.Stable; + } + if (largeDataRes < 0 && largeDataRes >= -5 && smallDataRes < 1) + { + res = TradingMode.SlowDropping; + } + if (largeDataRes>5 && smallDataRes > 0) + { + res = TradingMode.Growing; + } + if (largeDataRes < -5 && smallDataRes < 0) + { + res = TradingMode.Dropping; + } + } + } + return res; + } + + private async Task CalcTradingMode(INewPrice message) + { + var res = await CalcTradingMode(message.Figi); + //await LogPrice(message, "trading_mode", (int)res); + 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 0df2d65..f225a29 100644 --- a/KLHZ.Trader.Core/Exchange/Services/TraderDataProvider.cs +++ b/KLHZ.Trader.Core/Exchange/Services/TraderDataProvider.cs @@ -21,6 +21,7 @@ using Tinkoff.InvestApi; using Tinkoff.InvestApi.V1; using Asset = KLHZ.Trader.Core.Exchange.Models.AssetsAccounting.Asset; using AssetType = KLHZ.Trader.Core.Exchange.Models.AssetsAccounting.AssetType; +using Order = KLHZ.Trader.Core.Exchange.Models.AssetsAccounting.Order; using PositionType = KLHZ.Trader.Core.Exchange.Models.AssetsAccounting.PositionType; namespace KLHZ.Trader.Core.Exchange.Services @@ -167,9 +168,9 @@ namespace KLHZ.Trader.Core.Exchange.Services public async Task Init() { - await _initSemaphore.WaitAsync(TimeSpan.FromSeconds(3)); try { + await _initSemaphore.WaitAsync(TimeSpan.FromSeconds(3)); var shares = await _investApiClient.Instruments.SharesAsync(); foreach (var share in shares.Instruments) { @@ -325,11 +326,31 @@ namespace KLHZ.Trader.Core.Exchange.Services await context.Trades .Where(t => ids.Contains(t.Id)) .ExecuteUpdateAsync(t => t.SetProperty(tr => tr.ArchiveStatus, 1)); + + var orders = await _investApiClient.Orders.GetOrdersAsync(new GetOrdersRequest() { AccountId = account.AccountId }); + var actualOrders = orders.Orders.Select(o => new Order() + { + AccountId = account.AccountId, + Figi = o.Figi, + OrderId = o.OrderId, + Ticker = GetTickerByFigi(o.Figi), + Count = o.LotsRequested, + ExpirationTime = DateTime.UtcNow.AddMinutes(10), + OpenDate = DateTime.UtcNow, + Price = o.AveragePositionPrice, + Direction = (DealDirection)(int)o.Direction + }).ToArray(); + + foreach (var order in actualOrders) + { + account.Orders[order.OrderId] = order; + } } catch (Exception ex) { _logger.LogError(ex, "Ошибка при синхранизации портфеля счёта {accountId}", account.AccountId); } + _initSemaphore.Release(); } diff --git a/KLHZ.Trader.Core/Exchange/Services/TradingCommandsExecutor.cs b/KLHZ.Trader.Core/Exchange/Services/TradingCommandsExecutor.cs index afeb558..9d7ff27 100644 --- a/KLHZ.Trader.Core/Exchange/Services/TradingCommandsExecutor.cs +++ b/KLHZ.Trader.Core/Exchange/Services/TradingCommandsExecutor.cs @@ -1,5 +1,6 @@ using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces; using KLHZ.Trader.Core.Contracts.Messaging.Interfaces; +using KLHZ.Trader.Core.Exchange.Models.AssetsAccounting; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Threading.Channels; @@ -40,52 +41,76 @@ namespace KLHZ.Trader.Core.Exchange.Services { try { - var dir = OrderDirection.Unspecified; - var orderType = OrderType.Unspecified; - if (tradeCommand.CommandType == Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketBuy) + if (tradeCommand.CommandType == Contracts.Messaging.Dtos.Enums.TradeCommandType.CancelOrder && !string.IsNullOrEmpty(tradeCommand.OrderId)) { - dir = OrderDirection.Buy; - orderType = OrderType.Market; + var res = await _investApiClient.Orders.CancelOrderAsync(new CancelOrderRequest() { AccountId = tradeCommand.AccountId, OrderId = tradeCommand.OrderId }); } - else if (tradeCommand.CommandType == Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketSell) + else { - dir = OrderDirection.Sell; - orderType = OrderType.Market; + var dir = OrderDirection.Unspecified; + var orderType = OrderType.Unspecified; + if (tradeCommand.CommandType == Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketBuy) + { + dir = OrderDirection.Buy; + orderType = OrderType.Market; + } + else if (tradeCommand.CommandType == Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketSell) + { + dir = OrderDirection.Sell; + orderType = OrderType.Market; + } + else if (tradeCommand.CommandType == Contracts.Messaging.Dtos.Enums.TradeCommandType.LimitBuy && tradeCommand.RecomendPrice.HasValue) + { + dir = OrderDirection.Buy; + orderType = OrderType.Limit; + } + else if (tradeCommand.CommandType == Contracts.Messaging.Dtos.Enums.TradeCommandType.LimitSell && tradeCommand.RecomendPrice.HasValue) + { + dir = OrderDirection.Sell; + orderType = OrderType.Limit; + } + + if (orderType == OrderType.Unspecified) + { + return; + } + + var req = new PostOrderRequest() + { + AccountId = tradeCommand.AccountId, + InstrumentId = tradeCommand.Figi, + Direction = dir, + Price = tradeCommand.RecomendPrice ?? 0, + OrderType = orderType, + Quantity = tradeCommand.Count, + ConfirmMarginTrade = tradeCommand.EnableMargin, + }; + + _logger.LogWarning("Получена команда c id {commandId} на операцию с активом {figi}! Тип заявки сделки: {dir}; Количество активов: {count}; Разрешена ли маржиналка: {margin}", + tradeCommand.CommandId, req.InstrumentId, req.OrderType, req.Quantity, req.ConfirmMarginTrade); + + var res = await _investApiClient.Orders.PostOrderAsync(req); + + if ((tradeCommand.CommandType == Contracts.Messaging.Dtos.Enums.TradeCommandType.LimitBuy + || tradeCommand.CommandType == Contracts.Messaging.Dtos.Enums.TradeCommandType.LimitSell) + && tradeCommand.RecomendPrice.HasValue) + { + _tradeDataProvider.Accounts[tradeCommand.AccountId].Orders[res.OrderId] = new Models.AssetsAccounting.Order() + { + AccountId = tradeCommand.AccountId, + Figi = tradeCommand.Figi, + OrderId = res.OrderId, + Ticker = _tradeDataProvider.GetTickerByFigi(tradeCommand.Figi), + Count = res.LotsRequested, + Direction = (DealDirection)(int)dir, + ExpirationTime = DateTime.UtcNow.AddMinutes(2), + OpenDate = DateTime.UtcNow, + Price = tradeCommand.RecomendPrice.Value, + }; + } + _logger.LogWarning("Исполнена команда c id {commandId} на операцию с активом {figi}! Направление: {dir}; Число лотов: {lots};", tradeCommand.CommandId, res.Figi, + res.Direction, res.LotsExecuted); } - else if (tradeCommand.CommandType == Contracts.Messaging.Dtos.Enums.TradeCommandType.LimitBuy && tradeCommand.RecomendPrice.HasValue) - { - dir = OrderDirection.Buy; - orderType = OrderType.Limit; - } - - var req = new PostOrderRequest() - { - AccountId = tradeCommand.AccountId, - InstrumentId = tradeCommand.Figi, - Direction = dir, - Price = tradeCommand.RecomendPrice ?? 0, - OrderType = orderType, - Quantity = tradeCommand.Count, - ConfirmMarginTrade = tradeCommand.EnableMargin, - }; - - _logger.LogWarning("Получена команда c id {commandId} на операцию с активом {figi}! Тип заявки сделки: {dir}; Количество активов: {count}; Разрешена ли маржиналка: {margin}", - tradeCommand.CommandId, req.InstrumentId, req.OrderType, req.Quantity, req.ConfirmMarginTrade); - - var res = await _investApiClient.Orders.PostOrderAsync(req); - - _logger.LogWarning("Исполнена команда c id {commandId} на операцию с активом {figi}! Направление: {dir}; Число лотов: {lots}; цена: {price}", tradeCommand.CommandId, res.Figi, - res.Direction, res.LotsExecuted, (decimal)res.ExecutedOrderPrice); - //var result = new DealResult - //{ - // Count = sign * res.LotsExecuted, - // Price = res.ExecutedOrderPrice, - // Success = true, - // Direction = dealDirection, - // AccountId = tradeCommand.AccountId, - // Figi = tradeCommand.Figi, - //}; - //await _tradeDataProvider.LogDeal(result); } catch (Exception ex) { diff --git a/KLHZ.Trader.Service/Controllers/PlayController.cs b/KLHZ.Trader.Service/Controllers/PlayController.cs index 595a509..ebd89da 100644 --- a/KLHZ.Trader.Service/Controllers/PlayController.cs +++ b/KLHZ.Trader.Service/Controllers/PlayController.cs @@ -26,7 +26,7 @@ namespace KLHZ.Trader.Service.Controllers } [HttpGet] - public async Task Run() + public async Task Run(DateTime? startDate = null) { try { @@ -34,7 +34,8 @@ namespace KLHZ.Trader.Service.Controllers //var figi1 = "BBG004730N88"; var figi2 = "BBG004730N88"; //var figi2 = "FUTIMOEXF000"; - var time1 = DateTime.UtcNow.AddDays(-17); + var time1 = startDate?? DateTime.UtcNow.AddDays(-17); + //var time1 = new DateTime(2025, 9, 4, 14, 0, 0, DateTimeKind.Utc); //var time2 = DateTime.UtcNow.AddMinutes(18); using var context1 = await _dbContextFactory.CreateDbContextAsync(); context1.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;