using KLHZ.Trader.Core.Contracts.Messaging.Interfaces; using KLHZ.Trader.Core.DataLayer; using KLHZ.Trader.Core.Exchange.Extentions; 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 Tinkoff.InvestApi; using Tinkoff.InvestApi.V1; using Asset = KLHZ.Trader.Core.Exchange.Models.AssetsAccounting.Asset; using AssetType = KLHZ.Trader.Core.Exchange.Models.AssetsAccounting.AssetType; namespace KLHZ.Trader.Core.Exchange.Services { //todo перенести сюда весь кэш и всю работу по сохранению данных. public class TradeDataProvider { private readonly InvestApiClient _investApiClient; private readonly IDbContextFactory _dbContextFactory; private readonly ILogger _logger; private readonly IDataBus _dataBus; private readonly string[] _managedAccountsNamePatterns = []; private readonly string[] _instrumentsFigis = []; private readonly ConcurrentDictionary _instrumentsSettings = new(); private readonly ConcurrentDictionary _tickersCache = new(); private readonly ConcurrentDictionary _assetTypesCache = new(); internal readonly ConcurrentDictionary Accounts = new(); public TradeDataProvider(InvestApiClient investApiClient, IOptions options, IDbContextFactory dbContextFactory, ILogger logger, IDataBus dataBus) { _investApiClient = investApiClient; _dbContextFactory = dbContextFactory; _logger = logger; _dataBus = dataBus; _managedAccountsNamePatterns = options.Value.ManagingAccountNamePatterns.ToArray(); _instrumentsFigis = options.Value.DataRecievingInstrumentsFigis.ToArray(); foreach (var lev in options.Value.InstrumentsSettings) { _instrumentsSettings.TryAdd(lev.Figi, lev); } } public async Task Init() { var shares = await _investApiClient.Instruments.SharesAsync(); foreach (var share in shares.Instruments) { if (_instrumentsFigis.Contains(share.Figi)) { _tickersCache.TryAdd(share.Figi, share.Ticker); _assetTypesCache.TryAdd(share.Figi, AssetType.Common); } } var futures = await _investApiClient.Instruments.FuturesAsync(); foreach (var future in futures.Instruments) { if (_instrumentsFigis.Contains(future.Figi)) { _tickersCache.TryAdd(future.Figi, future.Ticker); _assetTypesCache.TryAdd(future.Figi, AssetType.Common); } } var accounts = await _investApiClient.GetAccounts(_managedAccountsNamePatterns); var accountsList = new List(); int i = 0; foreach (var accountId in accounts) { var acc = new ManagedAccount(accountId); await SyncPortfolio(acc); Accounts[accountId] = acc; } } public string GetTickerByFigi(string figi) { return _tickersCache.TryGetValue(figi, out var ticker) ? ticker : string.Empty; } public AssetType GetAssetTypeByFigi(string figi) { return _assetTypesCache.TryGetValue(figi, out var t) ? t : AssetType.Unknown; } internal async Task SyncPortfolio(ManagedAccount account) { try { //await _semaphoreSlim.WaitAsync(); var portfolio = await _investApiClient.Operations.GetPortfolioAsync(new PortfolioRequest() { AccountId = account.AccountId, }); var oldAssets = account.Assets.Keys.ToHashSet(); using var context = await _dbContextFactory.CreateDbContextAsync(); context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; var trades = await context.Trades .Where(t => t.AccountId == account.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.AssetsAccounting.Asset() { TradeId = trade?.Id, AccountId = account.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 // Тип или член устарел account.Assets.AddOrUpdate(asset.Figi, asset, (k, v) => asset); oldAssets.Remove(asset.Figi); } account.Total = portfolio.TotalAmountPortfolio; account.Balance = portfolio.TotalAmountCurrencies; foreach (var asset in oldAssets) { account.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}", account.AccountId); } finally { //_semaphoreSlim.Release(); } } public async Task LogDeal(DealResult dealResult) { using var context = await _dbContextFactory.CreateDbContextAsync(); context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; var priceCoeff = 1m; if (_instrumentsSettings.TryGetValue(dealResult.Figi, out var se)) { priceCoeff = se.PriceToRubConvertationCoefficient; } var trade = await context.Trades.FirstOrDefaultAsync(t => t.ArchiveStatus == 0 && t.Figi == dealResult.Figi && t.AccountId == dealResult.AccountId); if (trade == null) { var newTrade = new DataLayer.Entities.Trades.Trade() { AccountId = dealResult.AccountId, Figi = dealResult.Figi, Ticker = GetTickerByFigi(dealResult.Figi), BoughtAt = DateTime.UtcNow, Count = dealResult.Count, Price = dealResult.Price * priceCoeff, Position = dealResult.Count > 0 ? DataLayer.Entities.Trades.Enums.PositionType.Long : DataLayer.Entities.Trades.Enums.PositionType.Short, Direction = (DataLayer.Entities.Trades.Enums.TradeDirection)(int)dealResult.Direction, Asset = (DataLayer.Entities.Trades.Enums.AssetType)(int)GetAssetTypeByFigi(dealResult.Figi) }; await context.Trades.AddAsync(newTrade); await context.SaveChangesAsync(); } else { var oldAmount = trade.Price * trade.Count; var newAmount = dealResult.Price * priceCoeff * dealResult.Count; var oldCount = trade.Count; trade.Count = trade.Count + dealResult.Count; if (trade.Count != 0 && System.Math.Abs(oldCount) < System.Math.Abs(trade.Count))// Если суммарное количество элементов позиции сокращается - пересчитывать цену не нужно. { trade.Price = (oldAmount + newAmount) / trade.Count; } if (Accounts.TryGetValue(dealResult.AccountId, out var account)) { if (account.Assets.TryGetValue(dealResult.Figi, out var asset)) { if (trade.Count == 0) { await context.Trades.Where(t => t.Id == trade.Id && t.ArchiveStatus == 0) .ExecuteUpdateAsync(t => t.SetProperty(tr => tr.ArchiveStatus, 1)); account.Assets.TryRemove(dealResult.Figi, out _); return; } else { context.Trades.Update(trade); await context.SaveChangesAsync(); var newAsset = new Asset() { AccountId = asset.AccountId, Figi = asset.Figi, Ticker = asset.Ticker, BlockedItems = asset.BlockedItems, BoughtAt = DateTime.UtcNow, BoughtPrice = trade.Price, Count = trade.Count, Position = trade.Count > 0 ? PositionType.Long : PositionType.Short, Type = asset.Type, TradeId = asset.TradeId, }; account.Assets[dealResult.Figi] = newAsset; return; } } } } await SyncPortfolio(Accounts[dealResult.AccountId]); } } }