using KLHZ.Trader.Core.Common.Extentions; using KLHZ.Trader.Core.DataLayer; using KLHZ.Trader.Core.Exchange.Extentions; using KLHZ.Trader.Core.Exchange.Interfaces; using KLHZ.Trader.Core.Exchange.Models.AssetsAccounting; using KLHZ.Trader.Core.Exchange.Models.Configs; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System.Collections.Concurrent; using System.Collections.Immutable; using Tinkoff.InvestApi; using Tinkoff.InvestApi.V1; using Asset = KLHZ.Trader.Core.Exchange.Models.AssetsAccounting.Asset; namespace KLHZ.Trader.Core.Exchange.Services { public class ManagedAccount : IManagedAccount { public string AccountId { get; private set; } = string.Empty; public string AccountName { get; private set; } = string.Empty; public bool Initialized { get; private set; } = false; public decimal Balance { get { lock (_locker) return _balance; } private set { lock (_locker) _balance = value; } } public decimal Total { get { lock (_locker) return _total; } private set { lock (_locker) _total = value; } } public ImmutableDictionary Assets => GetAssets(); private readonly InvestApiClient _investApiClient; private readonly IDbContextFactory _dbContextFactory; private readonly ILogger _logger; private readonly IOptions _options; private readonly Dictionary _assets = new(); private readonly ConcurrentDictionary _usedOrderIds = new(); private readonly object _locker = new(); private decimal _balance = 0; private decimal _total = 0; private readonly TimeSpan _defaultLockTimeSpan = TimeSpan.FromSeconds(30); private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(0, 1); private readonly SemaphoreSlim _initSemaphore = new SemaphoreSlim(1, 1); public ManagedAccount(InvestApiClient investApiClient, IOptions options, IDbContextFactory dbContextFactory, ILogger logger) { _investApiClient = investApiClient; _dbContextFactory = dbContextFactory; _options = options; _logger = logger; } public async Task Init(string accountId, string? accountName = null) { try { await _initSemaphore.WaitAsync2(TimeSpan.FromMilliseconds(100)); AccountName = accountName ?? AccountId; AccountId = accountId; _semaphore.Release(); await LoadPortfolio(); _ = CyclingOperations(); Initialized = true; } catch (TaskCanceledException) { } catch (Exception ex) { _initSemaphore.Release(); } } public async Task LoadPortfolio() { try { await _semaphore.WaitAsync2(_defaultLockTimeSpan); await LoadPortfolioNolock(); } catch (TaskCanceledException) { } catch (Exception ex) { _logger.LogError(ex, "Ошибка при синхранизации портфеля счёта {accountId}", AccountId); } _semaphore.Release(); } private ImmutableDictionary GetAssets() { var res = ImmutableDictionary.Empty; try { _semaphore.WaitAsync2(TimeSpan.FromMilliseconds(100)).Wait(); res = _assets.ToImmutableDictionary(); } catch (TaskCanceledException) { } catch (Exception ex) { } _semaphore.Release(); return res; } private async Task LoadPortfolioNolock() { var portfolio = await _investApiClient.Operations.GetPortfolioAsync(new PortfolioRequest() { AccountId = AccountId, }); using var context = await _dbContextFactory.CreateDbContextAsync(); context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; var trades = await context.Trades .Where(t => t.ArchiveStatus == 0) .OrderByDescending(t => t.BoughtAt) .ToListAsync(); var oldAssets = _assets.ToDictionary(); _assets.Clear(); foreach (var position in portfolio.Positions) { oldAssets.TryGetValue(position.Figi, out var oldAsset); var newAssetId = oldAsset?.AssetId ?? Guid.NewGuid(); var trade = trades.FirstOrDefault(t => t.Figi == position.Figi && t.AssetId == newAssetId); var asset = new Asset() { AssetId = newAssetId, AccountId = AccountId, Figi = position.Figi, Ticker = position.Ticker, BoughtAt = trade?.BoughtAt ?? DateTime.UtcNow, BoughtPrice = trade?.Price ?? position.AveragePositionPrice, Type = position.InstrumentType.ParseInstrumentType(), Position = position.Quantity > 0 ? PositionType.Long : PositionType.Short, BlockedItems = position.BlockedLots, Count = position.Quantity, }; _assets[asset.Figi] = asset; } Total = portfolio.TotalAmountPortfolio; Balance = portfolio.TotalAmountCurrencies; } public async Task OpenPosition(string figi, PositionType positionType, decimal stopLossShift, decimal takeProfitShift, long count = 1) { try { await _semaphore.WaitAsync2(_defaultLockTimeSpan); if (!_assets.ContainsKey(figi) && _options.Value.TradingInstrumentsFigis.Contains(figi)) { var openingDirection = positionType == PositionType.Short ? OrderDirection.Sell : OrderDirection.Buy; var stopOrdersDirection = positionType == PositionType.Short ? StopOrderDirection.Buy : StopOrderDirection.Sell; var req = new PostOrderRequest() { AccountId = AccountId, InstrumentId = figi, Direction = openingDirection, OrderType = OrderType.Market, Quantity = count, ConfirmMarginTrade = true, }; var res = await _investApiClient.Orders.PostOrderAsync(req); _usedOrderIds.TryAdd(res.OrderId, DateTime.UtcNow); var executedPrice = res.ExecutedOrderPrice / 10; var slReq = new PostStopOrderRequest() { AccountId = AccountId, ConfirmMarginTrade = false, InstrumentId = figi, Direction = stopOrdersDirection, PriceType = PriceType.Point, Quantity = count, StopOrderType = StopOrderType.StopLoss, StopPrice = positionType == PositionType.Long ? executedPrice - stopLossShift : executedPrice + stopLossShift, ExchangeOrderType = ExchangeOrderType.Market, ExpirationType = StopOrderExpirationType.GoodTillCancel, }; var slOrderRes = await _investApiClient.StopOrders.PostStopOrderAsync(slReq); var tpReq = new PostStopOrderRequest() { AccountId = AccountId, ConfirmMarginTrade = false, InstrumentId = figi, Direction = stopOrdersDirection, PriceType = PriceType.Point, Quantity = count, StopOrderType = StopOrderType.TakeProfit, StopPrice = positionType == PositionType.Long ? executedPrice + takeProfitShift : executedPrice - takeProfitShift, ExchangeOrderType = ExchangeOrderType.Market, ExpirationType = StopOrderExpirationType.GoodTillCancel, }; var tpOrderRes = await _investApiClient.StopOrders.PostStopOrderAsync(tpReq); await LoadPortfolioNolock(); } } catch (TaskCanceledException) { } catch (Exception ex) { _logger.LogError(ex, "Ошибка при открытии позиции."); } _semaphore.Release(); } public async Task ResetStops(string figi, decimal stopLossShift, decimal takeProfitShift) { try { await _semaphore.WaitAsync2(_defaultLockTimeSpan); if (_assets.TryGetValue(figi, out var asset) && _options.Value.TradingInstrumentsFigis.Contains(figi)) { var stopsReq = new GetStopOrdersRequest() { AccountId = asset.AccountId }; var stopOrders = await _investApiClient.StopOrders.GetStopOrdersAsync(stopsReq); if (stopOrders.StopOrders != null) { foreach (var stopOrder in stopOrders.StopOrders) { try { await _investApiClient.StopOrders.CancelStopOrderAsync(new CancelStopOrderRequest() { AccountId = asset.AccountId, StopOrderId = stopOrder.StopOrderId }); } catch (Exception ex) { _logger.LogError(ex, "Ошибка при закрытии стопов для позиции."); } } } var stopOrdersDirection = asset.Count < 0 ? StopOrderDirection.Buy : StopOrderDirection.Sell; var executedPrice = asset.BoughtPrice; var slReq = new PostStopOrderRequest() { AccountId = AccountId, ConfirmMarginTrade = false, InstrumentId = figi, Direction = stopOrdersDirection, PriceType = PriceType.Point, Quantity = (long)asset.Count, StopOrderType = StopOrderType.StopLoss, StopPrice = asset.Count > 0 ? executedPrice - stopLossShift : executedPrice + stopLossShift, ExchangeOrderType = ExchangeOrderType.Market, ExpirationType = StopOrderExpirationType.GoodTillCancel, }; var slOrderRes = await _investApiClient.StopOrders.PostStopOrderAsync(slReq); var tpReq = new PostStopOrderRequest() { AccountId = AccountId, ConfirmMarginTrade = false, InstrumentId = figi, Direction = stopOrdersDirection, PriceType = PriceType.Point, Quantity = (long)asset.Count, StopOrderType = StopOrderType.TakeProfit, StopPrice = asset.Count > 0 ? executedPrice + takeProfitShift : executedPrice - takeProfitShift, ExchangeOrderType = ExchangeOrderType.Market, ExpirationType = StopOrderExpirationType.GoodTillCancel, }; var tpOrderRes = await _investApiClient.StopOrders.PostStopOrderAsync(tpReq); await LoadPortfolioNolock(); } } catch (TaskCanceledException) { } catch (Exception ex) { _logger.LogError(ex, "Ошибка при переопределении стопов."); } _semaphore.Release(); } public async Task ClosePosition(string figi) { try { await _semaphore.WaitAsync2(_defaultLockTimeSpan); if (_assets.TryGetValue(figi, out var asset)) { var closingDirection = asset.Count > 0 ? OrderDirection.Sell : OrderDirection.Buy; var req = new PostOrderRequest() { AccountId = AccountId, InstrumentId = figi, Direction = closingDirection, OrderType = OrderType.Market, Quantity = (long)System.Math.Abs(asset.Count), ConfirmMarginTrade = true, }; var stopsReq = new GetStopOrdersRequest() { AccountId = asset.AccountId }; var res = await _investApiClient.Orders.PostOrderAsync(req); var stopOrders = await _investApiClient.StopOrders.GetStopOrdersAsync(stopsReq); if (stopOrders.StopOrders != null) { foreach (var stopOrder in stopOrders.StopOrders) { try { await _investApiClient.StopOrders.CancelStopOrderAsync(new CancelStopOrderRequest() { AccountId = asset.AccountId, StopOrderId = stopOrder.StopOrderId }); } catch (Exception ex) { _logger.LogError(ex, "Ошибка при закрытии стопов для позиции."); } } } await LoadPortfolioNolock(); } } catch (TaskCanceledException) { } catch (Exception ex) { _logger.LogError(ex, "Ошибка при закрытии позиции."); } _semaphore.Release(); } private async Task CyclingOperations() { while (true) { await Task.Delay(10000); await LoadPortfolio(); } } } }