using KLHZ.Trader.Core.Contracts.Messaging.Dtos; using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Enums; using KLHZ.Trader.Core.Contracts.Messaging.Interfaces; using KLHZ.Trader.Core.DataLayer; using KLHZ.Trader.Core.Exchange.Extentions; using KLHZ.Trader.Core.Exchange.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; using System.Threading.Channels; using Tinkoff.InvestApi; using Tinkoff.InvestApi.V1; using PositionType = KLHZ.Trader.Core.Exchange.Models.PositionType; namespace KLHZ.Trader.Core.Exchange.Services { public class ManagedAccount { public string AccountId { get; private set; } = string.Empty; private readonly Channel _channel = Channel.CreateUnbounded(); #region Поля, собираемые из контейнера DI private readonly InvestApiClient _investApiClient; private readonly IDbContextFactory _dbContextFactory; private readonly ILogger _logger; private readonly IDataBus _dataBus; #endregion #region Кеш рабочих данных private readonly object _locker = new(); private decimal _balance = 0; private decimal _total = 0; internal decimal Balance { get { lock (_locker) return _balance; } set { lock (_locker) _balance = value; } } internal decimal Total { get { lock (_locker) return _total; } set { lock (_locker) _total = value; } } internal readonly ConcurrentDictionary Assets = new(); #endregion public ManagedAccount(InvestApiClient investApiClient, IDataBus dataBus, IDbContextFactory dbContextFactory, ILogger logger) { _dataBus = dataBus; _investApiClient = investApiClient; _dbContextFactory = dbContextFactory; _logger = logger; } public async Task Init(string accountId) { AccountId = accountId; await SyncPortfolio(); _dataBus.AddChannel(accountId, _channel); _ = ProcessCommands(); } private async Task ProcessCommands() { while (await _channel.Reader.WaitToReadAsync()) { var command = await _channel.Reader.ReadAsync(); try { await ProcessMarketCommand(command); } catch (Exception ex) { _logger.LogError(ex, "Ошибка при обработке команды."); } } } internal async Task SyncPortfolio() { try { //await _semaphoreSlim.WaitAsync(); var portfolio = await _investApiClient.Operations.GetPortfolioAsync(new PortfolioRequest() { AccountId = AccountId, }); var oldAssets = Assets.Keys.ToHashSet(); using var context = await _dbContextFactory.CreateDbContextAsync(); context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; var trades = await context.Trades .Where(t => t.AccountId == AccountId && t.ArchiveStatus == 0) .ToListAsync(); foreach (var position in portfolio.Positions) { decimal price = 0; var trade = trades.FirstOrDefault(t => t.Figi == position.Figi); if (trade != null) { trades.Remove(trade); price = trade.Price; } else { price = position.AveragePositionPrice; } #pragma warning disable CS0612 // Тип или член устарел var asset = new Models.Asset() { AccountId = AccountId, Figi = position.Figi, Ticker = position.Ticker, BoughtAt = trade?.BoughtAt ?? DateTime.UtcNow, BoughtPrice = price, Type = position.InstrumentType.ParseInstrumentType(), Position = position.Quantity > 0 ? PositionType.Long : PositionType.Short, BlockedItems = position.BlockedLots, Count = position.Quantity, CountLots = position.QuantityLots, }; #pragma warning restore CS0612 // Тип или член устарел Assets.AddOrUpdate(asset.Figi, asset, (k, v) => asset); oldAssets.Remove(asset.Figi); } Total = portfolio.TotalAmountPortfolio; Balance = portfolio.TotalAmountCurrencies; foreach (var asset in oldAssets) { Assets.TryRemove(asset, out _); } var ids = trades.Select(t => t.Id).ToArray(); await context.Trades .Where(t => ids.Contains(t.Id)) .ExecuteUpdateAsync(t => t.SetProperty(tr => tr.ArchiveStatus, 1)); } catch (Exception ex) { _logger.LogError(ex, "Ошибка при синхранизации портфеля счёта {accountId}", AccountId); } finally { //_semaphoreSlim.Release(); } } internal async Task ClosePosition(string figi) { if (!string.IsNullOrEmpty(figi) && Assets.TryGetValue(figi, out var asset)) { try { var req = new PostOrderRequest() { AccountId = AccountId, InstrumentId = figi, }; if (asset != null) { req.Direction = OrderDirection.Sell; req.OrderType = OrderType.Market; req.Quantity = (long)asset.Count; var res = await _investApiClient.Orders.PostOrderAsync(req); return new DealResult { Count = res.LotsExecuted, Price = res.ExecutedOrderPrice, Success = true, }; } } catch (Exception ex) { _logger.LogError(ex, "Ошибка при закрытии позиции по счёту {acc}. figi: {figi}", AccountId, figi); } } return new DealResult { Count = 0, Price = 0, Success = false, }; } internal async Task BuyAsset(string figi, decimal count, string? ticker = null, decimal? recommendedPrice = null) { try { var req = new PostOrderRequest() { AccountId = AccountId, InstrumentId = figi, Direction = OrderDirection.Buy, OrderType = OrderType.Market, Quantity = (long)count, }; var res = await _investApiClient.Orders.PostOrderAsync(req); using var context = await _dbContextFactory.CreateDbContextAsync(); context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; var trade = await context.Trades.FirstOrDefaultAsync(t => t.ArchiveStatus == 0 && t.Figi == figi); if (trade == null) { var newTrade = new DataLayer.Entities.Trades.Trade() { AccountId = AccountId, Figi = figi, Ticker = ticker ?? string.Empty, BoughtAt = DateTime.UtcNow, Count = res.LotsExecuted, Price = res.ExecutedOrderPrice, Position = DataLayer.Entities.Trades.PositionType.Long, Direction = DataLayer.Entities.Trades.TradeDirection.Buy, Asset = DataLayer.Entities.Trades.AssetType.Common, }; await context.Trades.AddAsync(newTrade); } else { var oldAmount = trade.Price * trade.Count; var newAmount = res.ExecutedOrderPrice * res.LotsExecuted; trade.Count = res.LotsExecuted + trade.Count; trade.Price = (oldAmount + newAmount) / trade.Count; context.Trades.Update(trade); } await context.SaveChangesAsync(); return new DealResult { Count = res.LotsExecuted, Price = res.ExecutedOrderPrice, Success = true, }; } catch (Exception ex) { _logger.LogError(ex, "Ошибка при покупке актива на счёт {acc}. figi: {figi}", AccountId, figi); } return new DealResult { Count = 0, Price = 0, Success = false, }; } private async Task ProcessMarketCommand(TradeCommand command) { if (string.IsNullOrWhiteSpace(command.Figi)) return; if (command.CommandType == TradeCommandType.MarketBuy) { await BuyAsset(command.Figi, command.Count ?? 1, command.Ticker, command.RecomendPrice); } else if (command.CommandType == TradeCommandType.ForceClosePosition) { await ClosePosition(command.Figi); } else return; await SyncPortfolio(); } } }