diff --git a/KLHZ.Trader.Core.Contracts/Messaging/Dtos/Enums/TradeCommandType.cs b/KLHZ.Trader.Core.Contracts/Messaging/Dtos/Enums/TradeCommandType.cs index abf2c90..7b3fd90 100644 --- a/KLHZ.Trader.Core.Contracts/Messaging/Dtos/Enums/TradeCommandType.cs +++ b/KLHZ.Trader.Core.Contracts/Messaging/Dtos/Enums/TradeCommandType.cs @@ -3,11 +3,8 @@ public enum TradeCommandType { Unknown = 0, - - MarketBuy = 1, - MarketSell = 101, - LimitBuy = 200, - LimitSell = 300, - CancelOrder = 400, + OpenLong = 1, + OpenShort = 101, + ClosePosition = 200, } } diff --git a/KLHZ.Trader.Core.Contracts/Messaging/Interfaces/IDataBus.cs b/KLHZ.Trader.Core.Contracts/Messaging/Interfaces/IDataBus.cs index ed53e7c..f00f38b 100644 --- a/KLHZ.Trader.Core.Contracts/Messaging/Interfaces/IDataBus.cs +++ b/KLHZ.Trader.Core.Contracts/Messaging/Interfaces/IDataBus.cs @@ -8,8 +8,10 @@ namespace KLHZ.Trader.Core.Contracts.Messaging.Interfaces public bool AddChannel(string key, Channel channel); public bool AddChannel(string key, Channel channel); public bool AddChannel(string key, Channel channel); + public bool AddChannel(string key, Channel channel); public Task Broadcast(INewPrice newPriceMessage); public Task Broadcast(IOrderbook orderbook); public Task Broadcast(IMessage message); + public Task Broadcast(ITradeCommand message); } } diff --git a/KLHZ.Trader.Core/Common/Messaging/Services/DataBus.cs b/KLHZ.Trader.Core/Common/Messaging/Services/DataBus.cs index 63bf7c2..63f7f23 100644 --- a/KLHZ.Trader.Core/Common/Messaging/Services/DataBus.cs +++ b/KLHZ.Trader.Core/Common/Messaging/Services/DataBus.cs @@ -9,6 +9,7 @@ namespace KLHZ.Trader.Core.Common.Messaging.Services { private readonly ConcurrentDictionary> _orderbooksChannels = new(); private readonly ConcurrentDictionary> _messagesChannels = new(); + private readonly ConcurrentDictionary> _commandsChannel = new(); private readonly ConcurrentDictionary> _priceChannels = new(); public bool AddChannel(string key, Channel channel) @@ -16,6 +17,11 @@ namespace KLHZ.Trader.Core.Common.Messaging.Services return _messagesChannels.TryAdd(key, channel); } + public bool AddChannel(string key, Channel channel) + { + return _commandsChannel.TryAdd(key, channel); + } + public bool AddChannel(string key, Channel channel) { return _priceChannels.TryAdd(key, channel); @@ -34,6 +40,13 @@ namespace KLHZ.Trader.Core.Common.Messaging.Services } } + public async Task Broadcast(ITradeCommand command) + { + foreach (var channel in _commandsChannel.Values) + { + await channel.Writer.WriteAsync(command); + } + } public async Task Broadcast(IOrderbook orderbook) { foreach (var channel in _orderbooksChannels.Values) diff --git a/KLHZ.Trader.Core/Exchange/Services/Trader.cs b/KLHZ.Trader.Core/Exchange/Services/Trader.cs index 2feb1b5..f393448 100644 --- a/KLHZ.Trader.Core/Exchange/Services/Trader.cs +++ b/KLHZ.Trader.Core/Exchange/Services/Trader.cs @@ -1,6 +1,7 @@ using KLHZ.Trader.Core.Common; using KLHZ.Trader.Core.Contracts.Declisions.Dtos.Enums; using KLHZ.Trader.Core.Contracts.Messaging.Dtos; +using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Enums; using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces; using KLHZ.Trader.Core.Contracts.Messaging.Interfaces; using KLHZ.Trader.Core.DataLayer.Entities.Declisions; @@ -20,6 +21,7 @@ using System.Collections.Concurrent; using System.Collections.Immutable; using System.Security.Cryptography; using System.Threading.Channels; +using Telegram.Bot.Types; using Tinkoff.InvestApi; using Asset = KLHZ.Trader.Core.Exchange.Models.AssetsAccounting.Asset; using AssetType = KLHZ.Trader.Core.Exchange.Models.AssetsAccounting.AssetType; @@ -43,6 +45,8 @@ namespace KLHZ.Trader.Core.Exchange.Services private readonly ConcurrentDictionary ShortClosingStops = new(); private readonly ConcurrentDictionary Leverages = new(); + + private readonly decimal _futureComission; private readonly decimal _shareComission; private readonly decimal _accountCashPart; @@ -50,8 +54,7 @@ namespace KLHZ.Trader.Core.Exchange.Services private readonly string[] _tradingInstrumentsFigis = []; private readonly Channel _pricesChannel = Channel.CreateUnbounded(); - private readonly Channel _ordersbookChannel = Channel.CreateUnbounded(); - + private readonly Channel _commands = Channel.CreateUnbounded(); public Trader( ILogger logger, IOptions options, @@ -83,8 +86,10 @@ namespace KLHZ.Trader.Core.Exchange.Services public Task StartAsync(CancellationToken cancellationToken) { _dataBus.AddChannel(nameof(Trader), _pricesChannel); - _dataBus.AddChannel(nameof(Trader), _ordersbookChannel); + //_dataBus.AddChannel(nameof(Trader), _ordersbookChannel); + _dataBus.AddChannel(nameof(Trader), _commands); _ = ProcessPrices(); + _ = ProcessCommands(); return Task.CompletedTask; } @@ -133,6 +138,43 @@ namespace KLHZ.Trader.Core.Exchange.Services return position; } + private async Task ProcessCommands() + { + while (await _commands.Reader.WaitToReadAsync()) + { + var command = await _commands.Reader.ReadAsync(); + try + { + if (command.CommandType == Contracts.Messaging.Dtos.Enums.TradeCommandType.OpenLong + || command.CommandType == Contracts.Messaging.Dtos.Enums.TradeCommandType.OpenShort) + { + var fakeMessage = new NewPriceMessage() { Figi = command.Figi, Ticker = "", Count = command.Count, Direction = 1, IsHistoricalData = false, Time = DateTime.UtcNow, Value = command.RecomendPrice ?? 0m }; + var positionType = command.CommandType == TradeCommandType.OpenLong ? PositionType.Long : PositionType.Short; + var stops = GetStops(fakeMessage, positionType); + var accounts = _portfolioWrapper.Accounts + .Where(a => !a.Value.Assets.ContainsKey(command.Figi)) + .Take(1) + .Select(a => a.Value) + .ToArray(); + await OpenPositions(accounts, fakeMessage, positionType, stops.stopLoss, stops.takeProfit, System.Math.Abs(command.Count)); + } + else + { + var fakeMessage = new NewPriceMessage() { Figi = command.Figi, Ticker = "", Count = command.Count, Direction = 1, IsHistoricalData = false, Time = DateTime.UtcNow, Value = command.RecomendPrice ?? 0m }; + var assetsForClose = _portfolioWrapper.Accounts + .SelectMany(a => a.Value.Assets.Values) + .Where(a => a.Figi == fakeMessage.Figi) + .ToArray(); + await ClosePositions(assetsForClose, fakeMessage, false); + } + } + catch(Exception ex) + { + + } + } + } + private async Task ProcessPrices() { var pricesCache1 = new Dictionary>(); @@ -141,7 +183,7 @@ namespace KLHZ.Trader.Core.Exchange.Services while (await _pricesChannel.Reader.WaitToReadAsync()) { var message = await _pricesChannel.Reader.ReadAsync(); - var changeMods = GetInitDict(1); + var changeMods = GetInitDict(0); try { if (message.IsHistoricalData) @@ -241,7 +283,7 @@ namespace KLHZ.Trader.Core.Exchange.Services } } #endregion - if (_tradingInstrumentsFigis.Contains(message.Figi) && message.Figi == "FUTIMOEXF000") + if (_tradingInstrumentsFigis.Contains(message.Figi) && message.Figi == "FUTIMOEXF000" && message.Direction==1) { var currentTime = message.IsHistoricalData ? message.Time : DateTime.UtcNow; try @@ -355,7 +397,7 @@ INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullSt res[TradingEvent.UptrendEnd] = Constants.PowerLowingCoefficient; if ((resultMoveAvFull.events & TradingEvent.UptrendStart) == TradingEvent.UptrendStart) { - res[TradingEvent.UptrendStart] = 2*initValue; + res[TradingEvent.UptrendStart] = initValue; res[TradingEvent.DowntrendEnd] = initValue; } if ((resultMoveAvFull.events & TradingEvent.UptrendEnd) == TradingEvent.UptrendEnd) @@ -397,7 +439,12 @@ INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullSt var loggedDeclisions = 0; var assetType = _tradeDataProvider.GetAssetTypeByFigi(message.Figi); var assetsForClose = new List(); - + var price = message.Value; + if (price == 0) + { + price = await _tradeDataProvider.GetLastPrice(message.Figi); + } + var messages = new List(); foreach (var asset in assets) { if (withProfitOnly) @@ -406,16 +453,13 @@ INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullSt if (assetType == AssetType.Futures) { - profit = TradingCalculator.CaclProfit(asset.BoughtPrice, message.Value, + profit = TradingCalculator.CaclProfit(asset.BoughtPrice, price, GetComission(assetType), GetLeverage(message.Figi, asset.Count < 0), asset.Count < 0); } if (profit > 0) { assetsForClose.Add(asset); - await _dataBus.Broadcast(new MessageForAdmin() - { - Text = $"Закрываю позицию {asset.Figi} ({(asset.Count > 0 ? "лонг" : "шорт")}) на счёте {_portfolioWrapper.Accounts[asset.AccountId].AccountName}. Количество {(long)asset.Count}, цена ~{message.Value}, профит {profit}" - }); + messages.Add($"Закрываю позицию {asset.Figi} ({(asset.Count > 0 ? "лонг" : "шорт")}) на счёте {_portfolioWrapper.Accounts[asset.AccountId].AccountName}. Количество {(long)asset.Count}, цена ~{price}, профит {profit}"); if (loggedDeclisions == 0) { loggedDeclisions++; @@ -425,12 +469,17 @@ INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullSt } else { + messages.Add($"Закрываю позицию {asset.Figi} ({(asset.Count > 0 ? "лонг" : "шорт")}) на счёте {_portfolioWrapper.Accounts[asset.AccountId].AccountName}. Количество {(long)asset.Count}, цена ~{price}"); assetsForClose.Add(asset); } } var tasks = assetsForClose.Select(asset => _portfolioWrapper.Accounts[asset.AccountId].ClosePosition(message.Figi)); await Task.WhenAll(tasks); + foreach (var mess in messages) + { + await _dataBus.Broadcast(new MessageForAdmin() { Text = mess }); + } } private async Task OpenPositions(IManagedAccount[] accounts, INewPrice message, PositionType positionType, decimal stopLossShift, decimal takeProfitShift, long count = 1) @@ -469,7 +518,7 @@ INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullSt return; } //var resTask1 = GetWindowAverageStartData(data, 30, 180, message, windowMaxSize, -2m, 2m,3); - var resTask1 = GetWindowAverageStartData(data, 30, 180, message, windowMaxSize, 0m, 0.5m, Constants.PowerUppingCoefficient); + var resTask1 = GetWindowAverageStartData(data, 30, 180, message, windowMaxSize, 0m, 0.5m, 2*Constants.PowerUppingCoefficient); //var resTask3 = GetWindowAverageStartData(data, 30, 180, message, windowMaxSize, 0, 0,0.7m); var getFFTModsTask = GetFFTMods(message); //var getAreasModsTask = GetAreasMods(data, message); @@ -487,11 +536,11 @@ INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullSt //result = MergeResults(result, resTask2.Result.ToImmutableDictionary()); //result = MergeResults(result, resTask3.Result.ToImmutableDictionary()); - //result = MergeResults(result, changeModeData); - result = MergeResults(result, getFFTModsTask.Result); + result = MergeResultsMax(result, changeModeData); + result = MergeResultsMult(result, getFFTModsTask.Result); //result = MergeResults(result, getAreasModsTask.Result); - result = MergeResults(result, getSellsDiffsModsTask.Result); - result = MergeResults(result, getTradingModeModsTask.Result); + result = MergeResultsMult(result, getSellsDiffsModsTask.Result); + result = MergeResultsMult(result, getTradingModeModsTask.Result); if (result[TradingEvent.UptrendStart] > Constants.UppingCoefficient && !LongOpeningStops.ContainsKey(message.Figi) @@ -684,11 +733,11 @@ INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullSt { res = TradingMode.SlowDropping; } - if (largeDataRes > 5 && smallDataRes > 0) + if ((largeDataRes > 5 && smallDataRes > 0)||smallDataRes>7) { res = TradingMode.Growing; } - if (largeDataRes < -5 && smallDataRes < 0) + if ((largeDataRes < -5 && smallDataRes < 0)|| smallDataRes<-7) { res = TradingMode.Dropping; } @@ -706,12 +755,11 @@ INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullSt private (decimal stopLoss, decimal takeProfit) GetStops(INewPrice message, PositionType type) { var mode = TradingModes[message.Figi]; - decimal stopLossShift = 4; - decimal takeProfitShift = 5; + decimal stopLossShift = 15; + decimal takeProfitShift = 6; if (mode == TradingMode.Growing && type == PositionType.Long) { - stopLossShift = 6; - takeProfitShift = 9; + takeProfitShift = 15; } if (mode == TradingMode.Growing && type == PositionType.Short) { @@ -720,28 +768,24 @@ INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullSt } if (mode == TradingMode.Stable && type == PositionType.Long) { - stopLossShift = 4; takeProfitShift = 2.5m; } if (mode == TradingMode.Stable && type == PositionType.Short) { - stopLossShift = 4; takeProfitShift = 2.5m; + stopLossShift = 5; } if (mode == TradingMode.SlowDropping && type == PositionType.Short) { - stopLossShift = 6; - takeProfitShift = 2.5m; + takeProfitShift = 4m; } if (mode == TradingMode.SlowDropping && type == PositionType.Long) { - stopLossShift = 4; takeProfitShift = 1.5m; } if (mode == TradingMode.Dropping && type == PositionType.Short) { - stopLossShift = 6; - takeProfitShift = 8; + takeProfitShift = 15; } if (mode == TradingMode.Dropping && type == PositionType.Long) { @@ -855,29 +899,29 @@ INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullSt var mode = TradingModes[message.Figi]; if (mode == TradingMode.None) { - res[TradingEvent.UptrendEnd] = Constants.UppingCoefficient; - res[TradingEvent.UptrendStart] = 1; - res[TradingEvent.DowntrendStart] = Constants.LowingCoefficient; - res[TradingEvent.DowntrendEnd] = Constants.UppingCoefficient; + //res[TradingEvent.UptrendEnd] = Constants.UppingCoefficient; + //res[TradingEvent.UptrendStart] = 1; + //res[TradingEvent.DowntrendStart] = Constants.LowingCoefficient; + //res[TradingEvent.DowntrendEnd] = Constants.UppingCoefficient; } if (mode == TradingMode.Growing) { res[TradingEvent.UptrendEnd] = Constants.PowerLowingCoefficient; res[TradingEvent.UptrendStart] = 10; res[TradingEvent.DowntrendStart] = Constants.BlockingCoefficient; - res[TradingEvent.DowntrendEnd] = Constants.BlockingCoefficient; + res[TradingEvent.DowntrendEnd] = Constants.PowerUppingCoefficient; } if (mode == TradingMode.Stable) { res[TradingEvent.UptrendEnd] = 1; //res[TradingEvent.UptrendStart] = Constants.UppingCoefficient; //res[TradingEvent.DowntrendEnd] = Constants.UppingCoefficient; - res[TradingEvent.DowntrendStart] = Constants.BlockingCoefficient; + // res[TradingEvent.DowntrendStart] = Constants.BlockingCoefficient; } if (mode == TradingMode.SlowDropping) { - res[TradingEvent.UptrendEnd] = Constants.PowerUppingCoefficient; - res[TradingEvent.UptrendStart] = Constants.PowerLowingCoefficient; + //res[TradingEvent.UptrendEnd] = Constants.PowerUppingCoefficient; + //res[TradingEvent.UptrendStart] = Constants.PowerLowingCoefficient; //res[TradingEvent.DowntrendStart] = Constants.UppingCoefficient; //res[TradingEvent.DowntrendEnd] = Constants.UppingCoefficient; } @@ -918,7 +962,7 @@ INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullSt return values.ToDictionary(v => v, v => initValue); } - private static Dictionary MergeResults(Dictionary res, ImmutableDictionary data) + private static Dictionary MergeResultsMult(Dictionary res, ImmutableDictionary data) { foreach (var k in res.Keys) { @@ -928,5 +972,16 @@ INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullSt } return res; } + + private static Dictionary MergeResultsMax(Dictionary res, ImmutableDictionary data) + { + foreach (var k in res.Keys) + { + var valRes = res[k]; + var valData = data[k]; + res[k] = System.Math.Max(valRes, valData); + } + return res; + } } } diff --git a/KLHZ.Trader.Core/Exchange/Services/TraderDataProvider.cs b/KLHZ.Trader.Core/Exchange/Services/TraderDataProvider.cs index 544eb5a..dab729e 100644 --- a/KLHZ.Trader.Core/Exchange/Services/TraderDataProvider.cs +++ b/KLHZ.Trader.Core/Exchange/Services/TraderDataProvider.cs @@ -35,6 +35,7 @@ namespace KLHZ.Trader.Core.Exchange.Services private readonly bool _isDataRecievingAllowed = false; private readonly Channel _forSave = Channel.CreateUnbounded(); + private readonly SemaphoreSlim _initSemaphore = new SemaphoreSlim(1, 1); public TraderDataProvider(InvestApiClient investApiClient, IOptions options, IDbContextFactory dbContextFactory, ILogger logger) @@ -61,6 +62,16 @@ namespace KLHZ.Trader.Core.Exchange.Services return ValueTask.CompletedTask; } + public async ValueTask GetLastPrice(string figi) + { + var res = 0m; + if (_historyCash.TryGetValue(figi, out var unit)) + { + res = (await unit.GetLastValues()).price; + } + return res; + } + public async ValueTask<(DateTime[] timestamps, decimal[] prices, bool isFullIntervalExists)> GetData(string figi, TimeSpan timeSpan) { if (_historyCash.TryGetValue(figi, out var unit)) diff --git a/KLHZ.Trader.Core/TG/Services/BotMessagesHandler.cs b/KLHZ.Trader.Core/TG/Services/BotMessagesHandler.cs index b534ecd..817696b 100644 --- a/KLHZ.Trader.Core/TG/Services/BotMessagesHandler.cs +++ b/KLHZ.Trader.Core/TG/Services/BotMessagesHandler.cs @@ -1,4 +1,5 @@ using KLHZ.Trader.Core.Common; +using KLHZ.Trader.Core.Contracts.Messaging.Dtos; using KLHZ.Trader.Core.Contracts.Messaging.Interfaces; using KLHZ.Trader.Core.Exchange.Services; using Microsoft.Extensions.Logging; @@ -78,30 +79,41 @@ namespace KLHZ.Trader.Core.TG.Services case "скинуть IMOEXF": case "сбросить IMOEXF": { - - var assets = _portfolioWrapper.Accounts - .SelectMany(a => a.Value.Assets) - .Select(a => a.Value) - .Where(a => a.Figi == "FUTIMOEXF000"); - foreach (var asset in assets) + var command = new TradeCommand() { - await _portfolioWrapper.Accounts[asset.AccountId].ClosePosition("FUTIMOEXF000"); - } - + AccountId = "", + Figi = "FUTIMOEXF000", + CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.ClosePosition, + EnableMargin = true, + Count = 1 + }; + await _eventBus.Broadcast(command); break; } case "лонг IMOEXF": { - var acc = _portfolioWrapper.Accounts.Values.FirstOrDefault(a => !a.Assets.ContainsKey("FUTIMOEXF000")); - if (acc != null) - await acc.OpenPosition("FUTIMOEXF000", Exchange.Models.AssetsAccounting.PositionType.Long, 4, 6, 1); + var command = new TradeCommand() + { + AccountId = "", + Figi = "FUTIMOEXF000", + CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.OpenLong, + EnableMargin = true, + Count = 1 + }; + await _eventBus.Broadcast(command); break; } case "шорт IMOEXF": { - var acc = _portfolioWrapper.Accounts.Values.FirstOrDefault(a => !a.Assets.ContainsKey("FUTIMOEXF000")); - if (acc != null) - await acc.OpenPosition("FUTIMOEXF000", Exchange.Models.AssetsAccounting.PositionType.Short, 4, 6, 1); + var command = new TradeCommand() + { + AccountId = "", + Figi = "FUTIMOEXF000", + CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.OpenShort, + EnableMargin = true, + Count = 1 + }; + await _eventBus.Broadcast(command); break; } case "stops": diff --git a/KLHZ.Trader.Service/Controllers/PlayController.cs b/KLHZ.Trader.Service/Controllers/PlayController.cs index e8c12df..d55e1ce 100644 --- a/KLHZ.Trader.Service/Controllers/PlayController.cs +++ b/KLHZ.Trader.Service/Controllers/PlayController.cs @@ -34,8 +34,8 @@ namespace KLHZ.Trader.Service.Controllers //var figi1 = "BBG004730N88"; var figi2 = "BBG004730N88"; //var figi2 = "FUTIMOEXF000"; - var time1 = DateTime.UtcNow.AddDays(-shift ?? -7).Date; - //var time1 = new DateTime(2025, 9, 4, 14, 0, 0, DateTimeKind.Utc); + //var time1 = DateTime.UtcNow.AddDays(-shift ?? -7).Date; + var time1 = new DateTime(2025, 9, 23, 17, 0, 0, DateTimeKind.Utc); //var time2 = DateTime.UtcNow.AddMinutes(18); using var context1 = await _dbContextFactory.CreateDbContextAsync(); context1.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;