using KLHZ.Trader.Core.Common; using KLHZ.Trader.Core.Common.Messaging.Contracts; using KLHZ.Trader.Core.Common.Messaging.Contracts.Messages; using KLHZ.Trader.Core.Common.Messaging.Contracts.Messages.Enums; using KLHZ.Trader.Core.DataLayer; using KLHZ.Trader.Core.DataLayer.Entities.Declisions; using KLHZ.Trader.Core.Declisions.Models; using KLHZ.Trader.Core.Declisions.Utils; using KLHZ.Trader.Core.Exchange; using KLHZ.Trader.Core.Exchange.Extentions; using KLHZ.Trader.Core.Exchange.Models; using KLHZ.Trader.Core.Exchange.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using System.Collections.Concurrent; using System.Threading.Channels; using Tinkoff.InvestApi; using AssetType = KLHZ.Trader.Core.Exchange.Models.AssetType; namespace KLHZ.Trader.Core.Declisions.Services { public class Trader : IHostedService { private readonly InvestApiClient _investApiClient; private readonly IServiceProvider _provider; private readonly IDataBus _dataBus; private readonly BotModeSwitcher _botModeSwitcher; private readonly IDbContextFactory _dbContextFactory; private readonly ConcurrentDictionary Accounts = new(); private readonly ConcurrentDictionary _historyCash = new(); 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 Channel _pricesChannel = Channel.CreateUnbounded(); public Trader( BotModeSwitcher botModeSwitcher, IServiceProvider provider, IOptions options, IDataBus dataBus, IDbContextFactory dbContextFactory, InvestApiClient investApiClient) { _botModeSwitcher = botModeSwitcher; _dataBus = dataBus; _provider = provider; _investApiClient = investApiClient; _managedAccountsNamePatterns = options.Value.ManagingAccountNamePatterns.ToArray(); _dbContextFactory = dbContextFactory; _futureComission = options.Value.FutureComission; _shareComission = options.Value.ShareComission; _accountCashPart = options.Value.AccountCashPart; _accountCashPartFutures = options.Value.AccountCashPartFutures; _defaultBuyPartOfAccount = options.Value.DefaultBuyPartOfAccount; } public async Task StartAsync(CancellationToken cancellationToken) { var accounts = await _investApiClient.GetAccounts(_managedAccountsNamePatterns); var accountsList = new List(); int i = 0; foreach (var accountId in accounts) { var acc = _provider.GetKeyedService(i); if (acc != null) { await acc.Init(accountId); Accounts[accountId] = acc; i++; } else { break; } } _dataBus.AddChannel(nameof(Trader), _pricesChannel); _ = ProcessMessages(); } private async Task ProcessMessages() { while (await _pricesChannel.Reader.WaitToReadAsync()) { var message = await _pricesChannel.Reader.ReadAsync(); if (_historyCash.TryGetValue(message.Figi, out var data)) { data.AddData(message); } else { data = new PriceHistoryCacheUnit(message.Figi, message); _historyCash.TryAdd(message.Figi, data); } if (message.IsHistoricalData) { float meanfullDiff; if (message.Figi == "BBG004730N88") { meanfullDiff = 0.05f; } else if (message.Figi == "FUTIMOEXF000") { meanfullDiff = 1f; } else { continue; } try { //var downtrendStarts = data.CheckDowntrendStarting(TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(7), meanfullDiff); var uptrendStarts = data.CheckLongOpen(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(7), meanfullDiff, 8, 3); var uptrendStarts2 = data.CheckLongOpen(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(3), meanfullDiff, 15, 2); var downtrendEnds = data.CheckLongOpen(TimeSpan.FromSeconds(120), TimeSpan.FromSeconds(10), meanfullDiff, 15, 5); uptrendStarts |= downtrendEnds; uptrendStarts |= uptrendStarts2; //var downtrendEnds = data.CheckDowntrendEnding(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(15), meanfullDiff); var uptrendEnds = data.CheckLongClose(TimeSpan.FromSeconds(15), TimeSpan.FromSeconds(20), meanfullDiff * 1.5f, 8, 8); var uptrendEnds2 = data.CheckLongClose(TimeSpan.FromSeconds(120), TimeSpan.FromSeconds(30), meanfullDiff, 15, 8); uptrendEnds |= uptrendEnds2; //var uptrendEnds2 = data.CheckUptrendEnding(TimeSpan.FromSeconds(20), TimeSpan.FromSeconds(20), meanfullDiff); //var uptrendEnds = uptrendEnds1 || uptrendEnds2; var declisionAction = DeclisionTradeAction.Unknown; //if (downtrendStarts) //{ // //declisionAction = DeclisionTradeAction.OpenShort; //} if (uptrendStarts) { declisionAction = DeclisionTradeAction.OpenLong; } //else if (downtrendEnds) //{ // //declisionAction = DeclisionTradeAction.CloseShort; //} else if (uptrendEnds) { declisionAction = DeclisionTradeAction.CloseLong; } if (declisionAction != DeclisionTradeAction.Unknown) { 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 = declisionAction, }); await context.SaveChangesAsync(); } } catch (Exception ex) { } } } } public async Task Preprocess(string figi) { if (_historyCash.TryGetValue(figi, out var unit)) { var periodData1 = unit.GetPriceDiffForTimeSpan(TimeSpan.Zero, TimeSpan.FromSeconds(10)); var periodData2 = unit.GetPriceDiffForTimeSpan(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30)); if (Math.Abs(periodData1.PeriodDiff) <= 1 && periodData2.PeriodDiff > 2) { //можно покупать. } if (Math.Abs(periodData1.PeriodDiff) <= 1 && periodData2.PeriodDiff < -2) { //можно продавать. } } } 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 GetCount(string accountId, decimal boutPrice) { var balance = Accounts[accountId].Balance; return System.Math.Floor(balance * _defaultBuyPartOfAccount / boutPrice); } private bool IsBuyAllowed(string accountId, decimal boutPrice, decimal count, bool needBigCash) { if (!_botModeSwitcher.CanPurchase()) return false; var balance = Accounts[accountId].Balance; var total = Accounts[accountId].Total; var futures = Accounts[accountId].Assets.Values.FirstOrDefault(v => v.Type == AssetType.Futures); if (futures != null || needBigCash) { if ((balance - boutPrice * count) / total < _accountCashPartFutures) return false; } else { if ((balance - boutPrice * count) / total < _accountCashPart) return false; } return true; } private bool IsSellAllowed(AssetType assetType, PositionType positionType, decimal boutPrice, decimal? requiredPrice, TradeCommandType commandType) { if (commandType >= TradeCommandType.MarketSell && commandType < TradeCommandType.ForceClosePosition && requiredPrice.HasValue) { var comission = GetComission(assetType); if (positionType == PositionType.Long) { return requiredPrice.Value * (1 - comission) > boutPrice * (1 + comission); } else if (positionType == PositionType.Short) { return requiredPrice.Value * (1 + comission) < boutPrice * (1 - comission); } } if (commandType == TradeCommandType.ForceClosePosition) return true; return false; } } }