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; using KLHZ.Trader.Core.DataLayer.Entities.Declisions.Enums; using KLHZ.Trader.Core.DataLayer.Entities.Prices; using KLHZ.Trader.Core.Exchange.Interfaces; using KLHZ.Trader.Core.Exchange.Models.Configs; using KLHZ.Trader.Core.Exchange.Models.Trading; using KLHZ.Trader.Core.Exchange.Utils; using KLHZ.Trader.Core.Math.Declisions.Dtos.FFT.Enums; using KLHZ.Trader.Core.Math.Declisions.Utils; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System.Collections.Concurrent; using System.Collections.Immutable; using System.Linq; using System.Security.Cryptography; using System.Threading.Channels; using Tinkoff.InvestApi; using Asset = KLHZ.Trader.Core.Exchange.Models.AssetsAccounting.Asset; using AssetType = KLHZ.Trader.Core.Exchange.Models.AssetsAccounting.AssetType; using PositionType = KLHZ.Trader.Core.Exchange.Models.AssetsAccounting.PositionType; namespace KLHZ.Trader.Core.Exchange.Services { public class Trader : IHostedService { private readonly IDataBus _dataBus; private readonly TraderDataProvider _tradeDataProvider; private readonly PortfolioWrapper _portfolioWrapper; private readonly ILogger _logger; private readonly ConcurrentDictionary TradingModes = new(); private readonly ConcurrentDictionary LongOpeningStops = new(); private readonly ConcurrentDictionary ShortOpeningStops = new(); private readonly ConcurrentDictionary LongClosingStops = new(); private readonly ConcurrentDictionary ShortClosingStops = new(); private readonly ConcurrentDictionary Leverages = new(); private readonly decimal _futureComission; private readonly decimal _shareComission; private readonly decimal _accountCashPart; private readonly decimal _accountCashPartFutures; private readonly string[] _tradingInstrumentsFigis = []; private readonly Channel _pricesChannel = Channel.CreateUnbounded(); private readonly Channel _commands = Channel.CreateUnbounded(); public Trader( ILogger logger, IOptions options, IDataBus dataBus, PortfolioWrapper portfolioWrapper, TraderDataProvider tradeDataProvider, InvestApiClient investApiClient) { _portfolioWrapper = portfolioWrapper; _tradeDataProvider = tradeDataProvider; _logger = logger; _dataBus = dataBus; _futureComission = options.Value.FutureComission; _shareComission = options.Value.ShareComission; _accountCashPart = options.Value.AccountCashPart; _accountCashPartFutures = options.Value.AccountCashPartFutures; _tradingInstrumentsFigis = options.Value.TradingInstrumentsFigis; foreach (var f in _tradingInstrumentsFigis) { TradingModes[f] = TradingMode.None; } foreach (var lev in options.Value.InstrumentsSettings) { Leverages.TryAdd(lev.Figi, lev); } } public Task StartAsync(CancellationToken cancellationToken) { _dataBus.AddChannel(nameof(Trader), _pricesChannel); //_dataBus.AddChannel(nameof(Trader), _ordersbookChannel); _dataBus.AddChannel(nameof(Trader), _commands); _ = ProcessPrices(); _ = ProcessCommands(); return Task.CompletedTask; } public async ValueTask<(DateTime[] timestamps, decimal[] prices, bool isFullIntervalExists)> GetData(INewPrice message, TimeSpan? windowSize = null) { var data2 = await _tradeDataProvider.GetData(message.Figi, windowSize??TimeSpan.FromHours(1.5)); if (!data2.isFullIntervalExists) { data2 = await _tradeDataProvider.GetData(message.Figi, TimeSpan.FromHours(1)); } if (!data2.isFullIntervalExists) { data2 = await _tradeDataProvider.GetData(message.Figi, TimeSpan.FromHours(0.75)); } return data2; } private async ValueTask<(decimal smallWindow, decimal bigWindow)> GetWindowsSizes(INewPrice message) { var fftFull = await _tradeDataProvider.GetFFtResult(message.Figi + "_full"); if (!fftFull.IsEmpty) { var harms = fftFull.Harmonics.Skip(1).Take(fftFull.Harmonics.Length - 3).ToArray(); var sum = harms.Sum(h => h.Magnitude); var sumtmp = 0f; foreach (var h in harms) { sumtmp += h.Magnitude; if (sumtmp / sum > 0.7f) { return ((decimal)(h.Period.TotalSeconds / 4), (decimal)(h.Period.TotalSeconds)); } } } return (30m, 180m); } private async ValueTask CheckHarmonicPosition((DateTime[] timestamps, decimal[] prices, bool isFullIntervalExists) data, INewPrice message) { var currentTime = message.IsHistoricalData ? message.Time : DateTime.UtcNow; var position = ValueAmplitudePosition.None; var fft = await _tradeDataProvider.GetFFtResult(message.Figi); var fftFull = await _tradeDataProvider.GetFFtResult(message.Figi + "_full"); //var highFreq = await _tradeDataProvider.GetFFtResult(message.Figi + "_high_freq"); //var lowFreq = await _tradeDataProvider.GetFFtResult(message.Figi + "_low_freq"); var step = message.IsHistoricalData ? 40 : 5; if (fft.IsEmpty || (currentTime - fft.LastTime).TotalSeconds > step) { if (data.isFullIntervalExists) { var interpolatedData = SignalProcessing.InterpolateData(data.timestamps, data.prices, TimeSpan.FromSeconds(5)); fftFull = FFT.Analyze(interpolatedData.timestamps, interpolatedData.values, message.Figi+"_full", TimeSpan.FromSeconds(30), TimeSpan.FromHours(24)); fft = FFT.ReAnalyze(fftFull, message.Figi, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(60)); //highFreq = FFT.ReAnalyze(fftFull, message.Figi + "_low_freq", TimeSpan.FromMinutes(20), TimeSpan.FromMinutes(60)); //lowFreq = FFT.ReAnalyze(fftFull, message.Figi + "_high_freq", TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(20)); await _tradeDataProvider.SetFFtResult(fft); await _tradeDataProvider.SetFFtResult(fftFull); //await _tradeDataProvider.SetFFtResult(lowFreq); //await _tradeDataProvider.SetFFtResult(highFreq); } //var highFreqData = await GetData(message, TimeSpan.FromMinutes(120)); //if (highFreqData.isFullIntervalExists) //{ // var interpolatehighFreqData = SignalProcessing.InterpolateData(data.timestamps, data.prices, TimeSpan.FromSeconds(5)); // highFreq = FFT.Analyze(interpolatehighFreqData.timestamps, interpolatehighFreqData.values, message.Figi + "_high_freq", TimeSpan.FromSeconds(20), TimeSpan.FromMinutes(120)); // await _tradeDataProvider.SetFFtResult(highFreq); //} } position = FFT.Check(fft, message.Time); if (position == Math.Declisions.Dtos.FFT.Enums.ValueAmplitudePosition.UpperThen30Decil) { await LogPrice(message, "upper30percent", message.Value); } if (position == Math.Declisions.Dtos.FFT.Enums.ValueAmplitudePosition.LowerThenMedianaGrowing) { await LogPrice(message, "lower30percent", message.Value); } //var hposition = FFT.CheckExtremums(highFreq, message.Time); //if (hposition == Math.Declisions.Dtos.FFT.Enums.ValueAmplitudePosition.UpperThen30Decil) //{ // await LogPrice(message, "high_freq_high", message.Value); //} //if (hposition == Math.Declisions.Dtos.FFT.Enums.ValueAmplitudePosition.LowerThen30Decil) //{ // await LogPrice(message, "high_freq_low", message.Value); //} //var gposition = FFT.CheckSign(highFreq, message.Time); //if (gposition == Math.Declisions.Dtos.FFT.Enums.ValueAmplitudePosition.Growing) //{ // await LogPrice(message, "growing", message.Value); //} //if (gposition == Math.Declisions.Dtos.FFT.Enums.ValueAmplitudePosition.Falling) //{ // await LogPrice(message, "falling", message.Value); //} //var lposition = FFT.CheckExtremums(lowFreq, message.Time); //if (lposition == Math.Declisions.Dtos.FFT.Enums.ValueAmplitudePosition.UpperThen30Decil) //{ // await LogPrice(message, "low_freq_high", message.Value); //} //if (lposition == Math.Declisions.Dtos.FFT.Enums.ValueAmplitudePosition.LowerThen30Decil) //{ // await LogPrice(message, "low_freq_low", message.Value); //} 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>(); var pricesCache2 = new Dictionary>(); var timesCache = new Dictionary(); while (await _pricesChannel.Reader.WaitToReadAsync()) { var message = await _pricesChannel.Reader.ReadAsync(); var changeMods = GetInitDict(0); try { #region Ускорение обработки исторических данных при отладке if (message.Direction == 1) { if (!pricesCache1.TryGetValue(message.Figi, out var list)) { list = new List(); pricesCache1[message.Figi] = list; } list.Add(message); if ((list.Last().Time - list.First().Time).TotalSeconds < 0.5) { list.Add(message); continue; } else { message = new PriceChange() { Figi = message.Figi, Ticker = message.Ticker, Count = message.Count, Direction = message.Direction, IsHistoricalData = message.IsHistoricalData, Time = message.Time, Value = list.Sum(l => l.Value) / list.Count }; list.Clear(); } } if (message.Direction == 2) { if (!pricesCache2.TryGetValue(message.Figi, out var list)) { list = new List(); pricesCache2[message.Figi] = list; } list.Add(message); if ((list.Last().Time - list.First().Time).TotalSeconds < 0.5) { list.Add(message); continue; } else { message = new PriceChange() { Figi = message.Figi, Ticker = message.Ticker, Count = message.Count, Direction = message.Direction, IsHistoricalData = message.IsHistoricalData, Time = message.Time, Value = list.Sum(l => l.Value) / list.Count }; list.Clear(); } } #endregion #region Подсчёт торгового баланса по сберу и IMOEXF if (message.Figi == "BBG004730N88" || message.Figi == "FUTIMOEXF000") { if (message.Direction == 1) { await _tradeDataProvider.AddDataTo20SecondsWindowCache(message.Figi, "1", new Contracts.Declisions.Dtos.CachedValue() { Time = message.Time, Value = message.Count }); await _tradeDataProvider.AddDataTo5MinuteWindowCache(message.Figi, Constants._5minBuyCacheKey, new Contracts.Declisions.Dtos.CachedValue() { Time = message.Time, Value = (decimal)message.Count }); await _tradeDataProvider.AddDataTo1MinuteWindowCache(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._5minSellCacheKey, new Contracts.Declisions.Dtos.CachedValue() { Time = message.Time, Value = (decimal)message.Count }); await _tradeDataProvider.AddDataTo1MinuteWindowCache(message.Figi, Constants._1minSellCacheKey, new Contracts.Declisions.Dtos.CachedValue() { Time = message.Time, Value = (decimal)message.Count }); await _tradeDataProvider.AddDataTo20SecondsWindowCache(message.Figi, "2", new Contracts.Declisions.Dtos.CachedValue() { Time = message.Time, Value = message.Count }); } var buys = await _tradeDataProvider.GetDataFrom20SecondsWindowCache(message.Figi, "1"); var sells = await _tradeDataProvider.GetDataFrom20SecondsWindowCache(message.Figi, "2"); var buysSpeed = buys.Sum(p => p.Value) / 20; var sellsSpeed = sells.Sum(p => p.Value) / 20; var orderBook = _tradeDataProvider.Orderbooks[message.Figi]; if (orderBook.Asks.Length>3 && orderBook.Bids.Length>3) { var asks = (decimal)(orderBook.Asks[0].Count + orderBook.Asks[1].Count + orderBook.Asks[2].Count); //var asks = (decimal)(orderBook.Asks[0].Count + orderBook.Asks[1].Count + orderBook.Asks[2].Count + orderBook.Asks[3].Count); var bids = (decimal)(orderBook.Bids[0].Count + orderBook.Bids[1].Count + orderBook.Bids[2].Count); //var bids = (decimal)(orderBook.Bids[0].Count + orderBook.Bids[1].Count + orderBook.Bids[2].Count + orderBook.Bids[3].Count); if (buysSpeed > 0 && sellsSpeed > 0) { await LogPrice(message, "speed_relation", (sellsSpeed / (sellsSpeed + buysSpeed))); } //var diff = buysSpeed - sellsSpeed; //await LogPrice(message, "speed_diff", diff); //await LogPrice(message, "stabling", (asks+bids)/(sellsSpeed+buysSpeed)); //if (buysSpeed > 0) //{ // var asksLifetime = asks / buysSpeed; // if (asksLifetime > 600) asksLifetime = 600; // var asksLifetime2 = diff > 0?System.Math.Abs( asks / diff):0; // await LogPrice(message, "asks_lifetime", asksLifetime); // await LogPrice(message, "asks_lifetime2", asksLifetime2); // await LogPrice(message, "asks_lifetime2", asksLifetime2); // await LogPrice(message, "buys_speed", buysSpeed); //} //if (sellsSpeed > 0) //{ // var bidsLifetime = bids / sellsSpeed; // if (bidsLifetime > 600) bidsLifetime = 600; // var bidsLifetime2 = diff < 0 ? System.Math.Abs(bids / diff) : 0; // await LogPrice(message, "bids_lifetime", bidsLifetime); // await LogPrice(message, "bids_lifetime2", System.Math.Abs(bidsLifetime2)); // await LogPrice(message, "sells_speed", sellsSpeed); //} //var buys5min = await _tradeDataProvider.GetDataFrom5MinuteWindowCache(message.Figi, Constants._5minBuyCacheKey); //var sells5min = await _tradeDataProvider.GetDataFrom5MinuteWindowCache(message.Figi, Constants._5minSellCacheKey); //var buysSpeed5min = buys5min.Sum(p => p.Value) / 300; //var sellsSpeed5min = sells5min.Sum(p => p.Value) / 300; //var diff5min = buysSpeed5min - sellsSpeed5min; //await LogPrice(message, "speed_diff_5min", diff5min); //var buys1min = await _tradeDataProvider.GetDataFrom1MinuteWindowCache(message.Figi, Constants._1minBuyCacheKey); //var sells1min = await _tradeDataProvider.GetDataFrom1MinuteWindowCache(message.Figi, Constants._1minSellCacheKey); //var buysSpeed1min = buys1min.Sum(p => p.Value) / 60; //var sellsSpeed1min = sells1min.Sum(p => p.Value) / 60; //var diff1min = buysSpeed1min - sellsSpeed1min; //await LogPrice(message, "speed_diff_1min", diff1min); } } #endregion if (_tradingInstrumentsFigis.Contains(message.Figi) && message.Figi == "FUTIMOEXF000" && message.Direction == 1) { var currentTime = message.IsHistoricalData ? message.Time : DateTime.UtcNow; try { if (timesCache.TryGetValue(message.Figi, out var dt)) { if ((message.Time - dt).TotalSeconds > 10) { timesCache[message.Figi] = message.Time; var newMod = await CalcTradingMode(message); if (TradingModes.TryGetValue(message.Figi, out var oldMod)) { if ((oldMod == TradingMode.Growing || oldMod == TradingMode.Stable) && oldMod != newMod) { changeMods[TradingEvent.UptrendEnd] = Constants.PowerUppingCoefficient; } if ((oldMod == TradingMode.Dropping || oldMod == TradingMode.SlowDropping) && oldMod != newMod) { changeMods[TradingEvent.DowntrendEnd] = Constants.PowerUppingCoefficient; } if (newMod == TradingMode.Growing && newMod != oldMod && !LongOpeningStops.ContainsKey(message.Figi)) { //changeMods[TradingEvent.UptrendStart] = Constants.PowerUppingCoefficient; } if (newMod == TradingMode.Dropping && newMod != oldMod && !ShortOpeningStops.ContainsKey(message.Figi)) { //changeMods[TradingEvent.DowntrendStart] = Constants.PowerUppingCoefficient; } TradingModes[message.Figi] = newMod; if (oldMod != newMod) { var accountForStopsChanging = _portfolioWrapper.Accounts .Where(a => a.Value.Assets.ContainsKey(message.Figi)) .ToArray(); foreach (var account in accountForStopsChanging) { if (account.Value.Assets.TryGetValue(message.Figi, out var asset)) { var stops = GetStops(message, asset.Count > 0 ? PositionType.Long : PositionType.Short); if (!message.IsHistoricalData) { await account.Value.ResetStops(message.Figi, stops.stopLoss, stops.takeProfit); if (asset.Count < 0) { await LogDeclision(DeclisionTradeAction.ResetStopsShort, asset.BoughtPrice - stops.takeProfit, message.Time.AddMilliseconds(-100), message); await LogDeclision(DeclisionTradeAction.ResetStopsShort, asset.BoughtPrice + stops.stopLoss, message.Time.AddMilliseconds(100), message); } else { await LogDeclision(DeclisionTradeAction.ResetStopsLong, asset.BoughtPrice + stops.takeProfit, message.Time.AddMilliseconds(-100), message); await LogDeclision(DeclisionTradeAction.ResetStopsLong, asset.BoughtPrice - stops.stopLoss, message.Time.AddMilliseconds(100), message); } } } } } } } } else { timesCache[message.Figi] = message.Time; } } catch (Exception ex) { } if (TradingModes.TryGetValue(message.Figi, out var mode)) { await LogPrice(message, "trading_mode", (decimal)mode); } try { if (message.Direction != 1) continue; await _tradeDataProvider.AddData(message); ProcessStops(message, currentTime); var windowMaxSize = 2000; var data = await _tradeDataProvider.GetData(message.Figi, windowMaxSize); var state = ExchangeScheduler.GetCurrentState(message.Time); await ProcessNewPriceIMOEXF3(data, state, message, windowMaxSize, changeMods.ToImmutableDictionary()); } catch (Exception ex) { _logger.LogError(ex, "Ошибка при боработке новой цены IMOEXF"); } } } catch (Exception e) { } } } private async Task> GetSpeedResultantMods(INewPrice message) { var res = GetInitDict(1); var buys5min = await _tradeDataProvider.GetDataFrom5MinuteWindowCache(message.Figi, Constants._5minBuyCacheKey); var sells5min = await _tradeDataProvider.GetDataFrom5MinuteWindowCache(message.Figi, Constants._5minSellCacheKey); var buysSpeed5min = buys5min.Sum(p => p.Value) / 300; var sellsSpeed5min = sells5min.Sum(p => p.Value) / 300; var diff5min = buysSpeed5min - sellsSpeed5min; await LogPrice(message, "speed_diff_5min", diff5min); if (diff5min < 0) { res[TradingEvent.UptrendStart] = Constants.BlockingCoefficient; } if (diff5min > 0) { res[TradingEvent.DowntrendStart] = Constants.BlockingCoefficient; } if (diff5min > 6) { res[TradingEvent.UptrendEnd] = Constants.BlockingCoefficient; } if (diff5min < -6) { res[TradingEvent.DowntrendEnd] = Constants.BlockingCoefficient; } return res.ToImmutableDictionary(); } private async Task> GetWindowAverageStartData((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, uptrendStartingDetectionMeanfullStep, uptrendEndingDetectionMeanfullStep); if (resultMoveAvFull.bigWindowAv != 0) { await LogPrice(message, Constants.BigWindowCrossingAverageProcessor, resultMoveAvFull.bigWindowAv); await LogPrice(message, Constants.SmallWindowCrossingAverageProcessor, resultMoveAvFull.smallWindowAv); } var res = GetInitDict(0); res[TradingEvent.DowntrendEnd] = Constants.PowerLowingCoefficient; res[TradingEvent.UptrendEnd] = Constants.PowerLowingCoefficient; if ((resultMoveAvFull.events & TradingEvent.UptrendStart) == TradingEvent.UptrendStart) { res[TradingEvent.UptrendStart] = Constants.PowerUppingCoefficient; res[TradingEvent.DowntrendEnd] = Constants.PowerUppingCoefficient; } if ((resultMoveAvFull.events & TradingEvent.UptrendEnd) == TradingEvent.UptrendEnd) { res[TradingEvent.UptrendEnd] = Constants.PowerUppingCoefficient; res[TradingEvent.DowntrendStart] = Constants.PowerUppingCoefficient; } return res; } private async Task GetAreasRelation((DateTime[] timestamps, decimal[] prices) data, INewPrice message) { var areasRel = -1m; if (ShapeAreaCalculator.TryGetAreasRelation(data.timestamps, data.prices, message.Value, Constants.AreasRelationWindow, out var rel)) { await _tradeDataProvider.AddDataTo1MinuteWindowCache(message.Figi, Constants._1minCacheKey, new Contracts.Declisions.Dtos.CachedValue() { Time = message.Time, Value = (decimal)rel }); var areas = await _tradeDataProvider.GetDataFrom1MinuteWindowCache(message.Figi, Constants._1minCacheKey); areasRel = (decimal)areas.Sum(a => a.Value) / areas.Length; await LogPrice(message, Constants.AreasRelationProcessor, areasRel); return areasRel > 0 ? areasRel : null; } return null; } private async Task CheckHarmonicPosition(INewPrice message) { var data2 = await GetData(message); var position = await CheckHarmonicPosition(data2, message); return position; } private async Task ClosePositions(Asset[] assets, INewPrice message, bool withProfitOnly = true) { 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); } price = System.Math.Round(price, 2); var messages = new List(); foreach (var asset in assets) { if (withProfitOnly) { var profit = 0m; if (assetType == AssetType.Futures) { profit = TradingCalculator.CaclProfit(asset.BoughtPrice, price, GetComission(assetType), GetLeverage(message.Figi, asset.Count < 0), asset.Count < 0); } if (profit > 0) { profit = System.Math.Round(profit, 2); assetsForClose.Add(asset); messages.Add($"Закрываю позицию {asset.Figi} ({(asset.Count > 0 ? "лонг" : "шорт")}) на счёте {_portfolioWrapper.Accounts[asset.AccountId].AccountName}. Количество {(long)asset.Count}, цена ~{price}, профит {profit}"); if (loggedDeclisions == 0) { loggedDeclisions++; await LogDeclision(asset.Count < 0 ? DeclisionTradeAction.CloseShortReal : DeclisionTradeAction.CloseLongReal, message, profit); } } } 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) { var loggedDeclisions = 0; var sign = positionType == PositionType.Long ? 1 : 1; foreach (var acc in accounts) { if (IsOperationAllowed(acc, message.Value, 1, _accountCashPartFutures, _accountCashPart)) { await acc.OpenPosition(message.Figi, positionType, stopLossShift, takeProfitShift, count); await _dataBus.Broadcast(new MessageForAdmin() { Text = $"Открываю позицию {message.Figi} ({(positionType == PositionType.Long ? "лонг" : "шорт")}) " + $"на счёте {acc.AccountName}. Количество {(positionType == PositionType.Long ? "" : "-")}{count}, " + $"цена ~{System.Math.Round(message.Value, 2)}. Стоп лосс: {(positionType == PositionType.Long ? "-" : "+")}{stopLossShift}. " + $"Тейк профит: {(positionType == PositionType.Long ? "+" : "-")}{takeProfitShift}" }); } if (loggedDeclisions == 0) { await LogDeclision(DeclisionTradeAction.OpenLongReal, message); LongOpeningStops[message.Figi] = message.Time.AddMinutes(1); loggedDeclisions++; } } } private async Task ProcessNewPriceIMOEXF3((DateTime[] timestamps, decimal[] prices) data, ExchangeState state, INewPrice message, int windowMaxSize, ImmutableDictionary changeModeData) { if (data.timestamps.Length <= 4) { return; } var windows = await GetWindowsSizes(message); //var resTask1 = GetWindowAverageStartData(data, 30, 180, message, windowMaxSize, -2m, 2m,3); var resTask1 = GetWindowAverageStartData(data, (int)windows.smallWindow, (int)windows.bigWindow, message, windowMaxSize, -0.5m, 0.5m); ////var resTask3 = GetWindowAverageStartData(data, 30, 180, message, windowMaxSize, 0, 0,0.7m); var getFFTModsTask = GetFFTMods(message); var getLocalTrendsModsTask = GetLocalTrendsMods(data, message); var getSellsDiffsModsTask = GetSellsDiffsMods(message); var getTradingModeModsTask = GetTradingModeMods(message); var getSpeedResultantModsTask = GetSpeedResultantMods(message); await Task.WhenAll(resTask1, getFFTModsTask, getSellsDiffsModsTask, getTradingModeModsTask, getLocalTrendsModsTask, getSpeedResultantModsTask); //var assetType = _tradeDataProvider.GetAssetTypeByFigi(message.Figi); //if (resTask1.Result[TradingEvent.UptrendStart] >= 1) //{ //} var result = resTask1.Result; //result = MergeResults(result, resTask2.Result.ToImmutableDictionary()); //result = MergeResults(result, resTask3.Result.ToImmutableDictionary()); result = MergeResultsMax(result, changeModeData); result = MergeResultsMax(result, getLocalTrendsModsTask.Result); result = MergeResultsMult(result, getFFTModsTask.Result); result = MergeResultsMult(result, getSellsDiffsModsTask.Result); result = MergeResultsMult(result, getTradingModeModsTask.Result); result = MergeResultsMult(result, getSpeedResultantModsTask.Result); if (result[TradingEvent.UptrendStart] >= Constants.UppingCoefficient && !LongOpeningStops.ContainsKey(message.Figi) && state == ExchangeState.Open ) { var stops = GetStops(message, PositionType.Long); if (!message.IsHistoricalData && BotModeSwitcher.CanPurchase()) { var accounts = _portfolioWrapper.Accounts .Where(a => !a.Value.Assets.ContainsKey(message.Figi)) .Take(1) .Select(a => a.Value) .ToArray(); await OpenPositions(accounts, message, PositionType.Long, stops.stopLoss, stops.takeProfit, 1); LongOpeningStops[message.Figi] = DateTime.UtcNow.AddMinutes(1); } await LogDeclision(DeclisionTradeAction.OpenLong, message.Value, message.Time.AddMilliseconds(RandomNumberGenerator.GetInt32(-100, 100)), message); await LogDeclision(DeclisionTradeAction.ResetStopsLong, message.Value + stops.takeProfit, message.Time.AddMilliseconds(-RandomNumberGenerator.GetInt32(300, 1000)), message); await LogDeclision(DeclisionTradeAction.ResetStopsLong, message.Value - stops.stopLoss, message.Time.AddMilliseconds(RandomNumberGenerator.GetInt32(300, 1000)), message); } if (result[TradingEvent.DowntrendStart] >= Constants.UppingCoefficient && !ShortOpeningStops.ContainsKey(message.Figi) && state == ExchangeState.Open ) { var stops = GetStops(message, PositionType.Short); if (!message.IsHistoricalData && BotModeSwitcher.CanPurchase()) { var accounts = _portfolioWrapper.Accounts .Where(a => !a.Value.Assets.ContainsKey(message.Figi)) .Take(1) .Select(a => a.Value) .ToArray(); await OpenPositions(accounts, message, PositionType.Short, stops.stopLoss, stops.takeProfit, 1); ShortOpeningStops[message.Figi] = DateTime.UtcNow.AddMinutes(1); } await LogDeclision(DeclisionTradeAction.OpenShort, message.Value, message.Time.AddMilliseconds(RandomNumberGenerator.GetInt32(-100, 100)), message); await LogDeclision(DeclisionTradeAction.ResetStopsShort, message.Value - stops.takeProfit, message.Time.AddMilliseconds(-RandomNumberGenerator.GetInt32(300, 1000)), message); await LogDeclision(DeclisionTradeAction.ResetStopsShort, message.Value + stops.stopLoss, message.Time.AddMilliseconds(RandomNumberGenerator.GetInt32(300, 1000)), message); } if (result[TradingEvent.UptrendEnd] >= Constants.UppingCoefficient) { if (!message.IsHistoricalData && BotModeSwitcher.CanSell()) { var assetsForClose = _portfolioWrapper.Accounts .SelectMany(a => a.Value.Assets.Values) .Where(a => a.Figi == message.Figi && a.Count > 0) .ToArray(); await ClosePositions(assetsForClose, message); } await LogDeclision(DeclisionTradeAction.CloseLong, message.Value, message.Time.AddMilliseconds(RandomNumberGenerator.GetInt32(-100, 100)), message); } if (result[TradingEvent.DowntrendEnd] >= Constants.UppingCoefficient) { if (!message.IsHistoricalData && BotModeSwitcher.CanPurchase()) { var assetsForClose = _portfolioWrapper.Accounts .SelectMany(a => a.Value.Assets.Values) .Where(a => a.Figi == message.Figi && a.Count < 0) .ToArray(); await ClosePositions(assetsForClose, message); } await LogDeclision(DeclisionTradeAction.CloseShort, message.Value, message.Time.AddMilliseconds(RandomNumberGenerator.GetInt32(-100, 100)), message); } } private void ProcessStops(INewPrice message, DateTime currentTime) { if (LongOpeningStops.TryGetValue(message.Figi, out var dt)) { if (dt < currentTime) { LongOpeningStops.TryRemove(message.Figi, out _); } } if (ShortClosingStops.TryGetValue(message.Figi, out var dt2)) { if (dt2 < currentTime) { ShortClosingStops.TryRemove(message.Figi, out _); } } if (LongClosingStops.TryGetValue(message.Figi, out var dt3)) { if (dt3 < currentTime) { LongClosingStops.TryRemove(message.Figi, out _); } } if (ShortOpeningStops.TryGetValue(message.Figi, out var dt4)) { if (dt4 < currentTime) { ShortOpeningStops.TryRemove(message.Figi, out _); } } } private async Task LogPrice(INewPrice message, string processor, decimal value) { await _tradeDataProvider.LogPrice(new ProcessedPrice() { Figi = message.Figi, Ticker = message.Ticker, Processor = processor, Time = message.Time, Value = value, }, false); } private async Task LogDeclision(DeclisionTradeAction action, INewPrice message, decimal? profit = null) { await _tradeDataProvider.LogDeclision(new Declision() { AccountId = string.Empty, Figi = message.Figi, Ticker = message.Ticker, Value = profit, Price = message.Value, Time = message.IsHistoricalData ? message.Time : DateTime.UtcNow, Action = action, }, false); } private async Task LogDeclision(DeclisionTradeAction action, decimal price, DateTime time, INewPrice message) { await _tradeDataProvider.LogDeclision(new Declision() { AccountId = string.Empty, Figi = message.Figi, Ticker = message.Ticker, Value = price, Price = price, Time = time, Action = action, }, false); } public Task StopAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } private decimal GetComission(AssetType assetType) { if (assetType == AssetType.Common) { return _shareComission; } else if (assetType == AssetType.Futures) { return _futureComission; } else { return 0; } } private decimal GetLeverage(string figi, bool isShort) { var res = 1m; if (Leverages.TryGetValue(figi, out var leverage)) { res = isShort ? leverage.ShortLeverage : leverage.LongLeverage; } return res; } private async Task CalcTradingMode(string figi) { var res = TradingMode.None; var largeData = await _tradeDataProvider.GetData(figi, TimeSpan.FromMinutes(45)); var smallData = await _tradeDataProvider.GetData(figi, TimeSpan.FromMinutes(10)); 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; } if (smallDataRes > 7) { res = TradingMode.Growing; } if (smallDataRes < - 7) { res = TradingMode.Dropping; } } } return res; } private async Task CalcTradingMode(INewPrice message) { var res = await CalcTradingMode(message.Figi); return res; } private (decimal stopLoss, decimal takeProfit) GetStops(INewPrice message, PositionType type) { var mode = TradingModes[message.Figi]; decimal stopLossShift = 15; decimal takeProfitShift = 6; if (mode == TradingMode.Growing && type == PositionType.Long) { takeProfitShift = 15; } if (mode == TradingMode.Growing && type == PositionType.Short) { stopLossShift = 4; takeProfitShift = 2; } if (mode == TradingMode.Stable && type == PositionType.Long) { takeProfitShift = 2.5m; } if (mode == TradingMode.Stable && type == PositionType.Short) { takeProfitShift = 2.5m; stopLossShift = 6; } if (mode == TradingMode.SlowDropping && type == PositionType.Short) { takeProfitShift = 4m; } if (mode == TradingMode.SlowDropping && type == PositionType.Long) { takeProfitShift = 1.5m; stopLossShift = 5; } if (mode == TradingMode.Dropping && type == PositionType.Short) { takeProfitShift = 15; } if (mode == TradingMode.Dropping && type == PositionType.Long) { stopLossShift = 4; takeProfitShift = 1.5m; } return (stopLossShift, takeProfitShift); } private async Task> GetFFTMods(INewPrice message) { var res = GetInitDict(1); var position = await CheckHarmonicPosition(message); if (position == ValueAmplitudePosition.LowerThenMedianaGrowing) { //res[TradingEvent.UptrendStart] = Constants.UppingCoefficient; res[TradingEvent.DowntrendEnd] = Constants.UppingCoefficient; //res[TradingEvent.UptrendEnd] = Constants.LowingCoefficient; res[TradingEvent.DowntrendStart] = Constants.LowingCoefficient; } if (position == ValueAmplitudePosition.UpperThen30Decil) { res[TradingEvent.UptrendStart] = Constants.LowingCoefficient; //res[TradingEvent.DowntrendEnd] = Constants.LowingCoefficient; //res[TradingEvent.UptrendEnd] = Constants.UppingCoefficient; res[TradingEvent.DowntrendEnd] = Constants.UppingCoefficient; } return res.ToImmutableDictionary(); } private async Task> GetAreasMods((DateTime[] timestamps, decimal[] prices) data, INewPrice message) { var res = GetInitDict(1); var areas = await GetAreasRelation(data, message); if (areas.HasValue && areas.Value > 0.2m && areas.Value <= 0.8m) { //res[TradingEvent.UptrendStart] = Constants.PowerLowingCoefficient; } if (areas.HasValue && areas.Value > 0.8m) { //res[TradingEvent.DowntrendStart] = Constants.UppingCoefficient; //res[TradingEvent.DowntrendEnd] = Constants.UppingCoefficient; } return res.ToImmutableDictionary(); } private Task> GetLocalTrendsMods((DateTime[] timestamps, decimal[] prices) data, INewPrice message) { var res = GetInitDict(0); if (LocalTrends.TryGetLocalTrends(data.timestamps, data.prices, TimeSpan.FromSeconds(15), TimeSpan.FromSeconds(15), 0.5, out var localTrends)) { if ((localTrends & TradingEvent.UptrendEnd) == TradingEvent.UptrendEnd) { res[TradingEvent.UptrendEnd] = Constants.PowerUppingCoefficient; } if ((localTrends & TradingEvent.DowntrendEnd) == TradingEvent.DowntrendEnd) { res[TradingEvent.DowntrendEnd] = Constants.PowerUppingCoefficient; } } return Task.FromResult(res.ToImmutableDictionary()); } private async Task> GetSellsDiffsMods(INewPrice message) { var res = GetInitDict(1); var sberSells5min = await _tradeDataProvider.GetDataFrom5MinuteWindowCache("BBG004730N88", Constants._5minSellCacheKey); var sberBuys5min = await _tradeDataProvider.GetDataFrom5MinuteWindowCache("BBG004730N88", Constants._5minBuyCacheKey); var sberSells1min = await _tradeDataProvider.GetDataFrom1MinuteWindowCache("BBG004730N88", Constants._1minSellCacheKey); var sberBuys1min = await _tradeDataProvider.GetDataFrom1MinuteWindowCache("BBG004730N88", Constants._1minBuyCacheKey); var sells5min = sberSells5min.Sum(s => s.Value); var buys5min = sberBuys5min.Sum(s => s.Value); var sells1min = sberSells1min.Sum(s => s.Value); var buys1min = sberBuys1min.Sum(s => s.Value); var su = sells5min + buys5min; var uptrendEndMode = 1m; var downtrendEndMode = 1m; var downstartMode = 1m; var uptrendStartMode = 1m; if (su != 0) { var trades_rel = (sells5min / su - 0.5m) * 2; var bys_rel = buys1min / su; var sells_rel = sells1min / su; await LogPrice(message, "trades_rel", trades_rel); await LogPrice(message, "bys_rel", bys_rel); await LogPrice(message, "sells_rel", sells_rel); if (trades_rel > 0.7m) { //uptrendStartMode *= Constants.LowingCoefficient; } if (System.Math.Abs(bys_rel) > 0.6m || System.Math.Abs(sells_rel) > 0.6m) { uptrendStartMode *= Constants.PowerUppingCoefficient; downstartMode *= Constants.PowerUppingCoefficient; uptrendEndMode *= Constants.BlockingCoefficient; downtrendEndMode *= Constants.BlockingCoefficient; } else if (System.Math.Abs(bys_rel) > 0.3m || System.Math.Abs(sells_rel) > 0.3m) { uptrendStartMode *= Constants.UppingCoefficient; downstartMode *= Constants.UppingCoefficient; uptrendEndMode *= Constants.LowingCoefficient; downtrendEndMode *= Constants.LowingCoefficient; } //else if (System.Math.Abs(bys_rel) <= 0.2m && System.Math.Abs(sells_rel) <= 0.2m) //{ // uptrendEndMode *= Constants.UppingCoefficient; // downtrendEndMode *= Constants.UppingCoefficient; //} res[TradingEvent.UptrendStart] = uptrendStartMode; res[TradingEvent.UptrendEnd] = uptrendEndMode; res[TradingEvent.DowntrendEnd] = downtrendEndMode; res[TradingEvent.DowntrendStart] = downstartMode; } return res.ToImmutableDictionary(); } private Task> GetTradingModeMods(INewPrice message) { var res = GetInitDict(1); 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; } if (mode == TradingMode.Growing) { res[TradingEvent.UptrendEnd] = Constants.LowingCoefficient; res[TradingEvent.UptrendStart] = Constants.UppingCoefficient; res[TradingEvent.DowntrendStart] = Constants.BlockingCoefficient; res[TradingEvent.DowntrendEnd] = Constants.PowerUppingCoefficient; } if (mode == TradingMode.Stable) { //res[TradingEvent.UptrendEnd] = 1; res[TradingEvent.UptrendStart] = Constants.LowingCoefficient; //res[TradingEvent.DowntrendEnd] = Constants.UppingCoefficient; res[TradingEvent.DowntrendStart] = Constants.LowingCoefficient; } if (mode == TradingMode.SlowDropping) { //res[TradingEvent.UptrendEnd] = Constants.PowerUppingCoefficient; //res[TradingEvent.UptrendStart] = Constants.LowingCoefficient; res[TradingEvent.DowntrendStart] = Constants.UppingCoefficient; //res[TradingEvent.DowntrendEnd] = Constants.UppingCoefficient; } if (mode == TradingMode.Dropping) { res[TradingEvent.UptrendEnd] = Constants.PowerUppingCoefficient; res[TradingEvent.UptrendStart] = Constants.BlockingCoefficient; res[TradingEvent.DowntrendStart] = Constants.UppingCoefficient; res[TradingEvent.DowntrendEnd] = Constants.LowingCoefficient; } return Task.FromResult(res.ToImmutableDictionary()); } internal static bool IsOperationAllowed(IManagedAccount account, decimal boutPrice, decimal count, decimal accountCashPartFutures, decimal accountCashPart) { if (!BotModeSwitcher.CanPurchase()) return false; var balance = account.Balance; var total = account.Total; var futures = account.Assets.Values.FirstOrDefault(v => v.Type == AssetType.Futures); if (futures != null) { if ((balance - boutPrice * count) / total < accountCashPartFutures) return false; } else { if ((balance - boutPrice * count) / total < accountCashPart) return false; } return true; } private static Dictionary GetInitDict(decimal initValue) { var values = System.Enum.GetValues(); return values.ToDictionary(v => v, v => initValue); } private static Dictionary MergeResultsMult(Dictionary res, ImmutableDictionary data) { foreach (var k in res.Keys) { var valRes = res[k]; var valData = data[k]; res[k] = valRes * valData; } 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; } } }