klhztrader/KLHZ.Trader.Core/Exchange/Services/ExchangeDataReader.cs

234 lines
9.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

using Grpc.Core;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos;
using KLHZ.Trader.Core.Contracts.Messaging.Interfaces;
using KLHZ.Trader.Core.DataLayer;
using KLHZ.Trader.Core.DataLayer.Entities.Orders;
using KLHZ.Trader.Core.DataLayer.Entities.Prices;
using KLHZ.Trader.Core.Exchange.Extentions;
using KLHZ.Trader.Core.Exchange.Models.Configs;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Collections.Concurrent;
using Tinkoff.InvestApi;
using Tinkoff.InvestApi.V1;
namespace KLHZ.Trader.Core.Exchange.Services
{
public class ExchangeDataReader : IHostedService
{
private readonly PortfolioWrapper _portfolioWrapper;
private readonly TraderDataProvider _tradeDataProvider;
private readonly InvestApiClient _investApiClient;
private readonly string[] _instrumentsFigis = [];
private readonly string[] _managedAccountNamePatterns;
private readonly ILogger<ExchangeDataReader> _logger;
private readonly IDbContextFactory<TraderDbContext> _dbContextFactory;
private readonly CancellationTokenSource _cts = new();
private readonly IDataBus _eventBus;
private readonly bool _exchangeDataRecievingEnabled;
private readonly ConcurrentDictionary<string, DateTime> _usedOrderIds = new();
public ExchangeDataReader(InvestApiClient investApiClient, IDataBus eventBus, TraderDataProvider tradeDataProvider,
IOptions<ExchangeConfig> options, IDbContextFactory<TraderDbContext> dbContextFactory, PortfolioWrapper portfolioWrapper,
ILogger<ExchangeDataReader> logger)
{
_exchangeDataRecievingEnabled = options.Value.ExchangeDataRecievingEnabled;
_eventBus = eventBus;
_dbContextFactory = dbContextFactory;
_investApiClient = investApiClient;
_instrumentsFigis = options.Value.DataRecievingInstrumentsFigis.ToArray();
_logger = logger;
_managedAccountNamePatterns = options.Value.ManagingAccountNamePatterns.ToArray();
_tradeDataProvider = tradeDataProvider;
_portfolioWrapper = portfolioWrapper;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
await _tradeDataProvider.Init();
_logger.LogInformation("Инициализация приемника данных с биржи");
var accounts = await _investApiClient.GetAccounts(_managedAccountNamePatterns);
foreach (var acc in accounts)
{
await _portfolioWrapper.AddAccount(acc.Key, acc.Value);
}
_ = CycleSubscribtion(accounts.Keys.ToArray());
}
private async Task CycleSubscribtion(string[] accounts)
{
while (true)
{
try
{
if (_exchangeDataRecievingEnabled)
{
var t1 = SubscribeTrades(accounts);
var t2 = SubscribeExchangeData();
await Task.WhenAll(t1, t2);
}
await Task.Delay(1000);
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка в одном из стримов получения данных от биржи.");
}
}
}
private async Task SubscribeTrades(string[] accounts)
{
var req = new TradesStreamRequest();
foreach (var a in accounts)
{
req.Accounts.Add(a);
}
using var stream = _investApiClient.OrdersStream.TradesStream(req);
await foreach (var response in stream.ResponseStream.ReadAllAsync())
{
if (response.OrderTrades?.Trades != null)
{
if (_usedOrderIds.TryAdd(response.OrderTrades.OrderId, DateTime.UtcNow))
{
_ = _portfolioWrapper.Accounts[response.OrderTrades.AccountId].LoadPortfolio();
}
}
}
}
private async Task SubscribeExchangeData()
{
using var stream = _investApiClient.MarketDataStream.MarketDataStream();
var tradesRequest = new SubscribeTradesRequest
{
SubscriptionAction = SubscriptionAction.Subscribe
};
var bookRequest = new SubscribeOrderBookRequest
{
SubscriptionAction = SubscriptionAction.Subscribe
};
foreach (var f in _instrumentsFigis)
{
tradesRequest.Instruments.Add(
new TradeInstrument()
{
InstrumentId = f
});
bookRequest.Instruments.Add(
new OrderBookInstrument()
{
InstrumentId = f,
Depth = 10
});
}
await stream.RequestStream.WriteAsync(new MarketDataRequest
{
SubscribeTradesRequest = tradesRequest,
});
await stream.RequestStream.WriteAsync(new MarketDataRequest
{
SubscribeOrderBookRequest = bookRequest
});
var lastUpdateDict = new Dictionary<string, PriceChange>();
var pricesBuffer = new List<PriceChange>();
var orderbookItemsBuffer = new List<OrderbookItem>();
var lastWrite = DateTime.UtcNow;
await foreach (var response in stream.ResponseStream.ReadAllAsync())
{
if (response.Trade != null)
{
var message = new PriceChange()
{
Figi = response.Trade.Figi,
Ticker = _tradeDataProvider.GetTickerByFigi(response.Trade.Figi),
Time = response.Trade.Time.ToDateTime().ToUniversalTime(),
Price = response.Trade.Price,
IsHistoricalData = false,
Direction = (int)response.Trade.Direction,
Count = response.Trade.Quantity,
};
await _eventBus.Broadcast(message);
pricesBuffer.Add(message);
}
if (response.Orderbook != null)
{
var asks = response.Orderbook.Asks.Take(4).Select(a => new OrderbookItem()
{
Count = a.Quantity,
Price = a.Price,
Figi = response.Orderbook.Figi,
Ticker = _tradeDataProvider.GetTickerByFigi(response.Orderbook.Figi),
ItemType = DataLayer.Entities.Orders.Enums.OrderbookItemType.Ask,
Time = response.Orderbook.Time.ToDateTime().ToUniversalTime(),
}).ToArray();
var bids = response.Orderbook.Bids.Take(4).Select(a => new OrderbookItem()
{
Count = a.Quantity,
Price = a.Price,
Figi = response.Orderbook.Figi,
Ticker = _tradeDataProvider.GetTickerByFigi(response.Orderbook.Figi),
ItemType = DataLayer.Entities.Orders.Enums.OrderbookItemType.Bid,
Time = response.Orderbook.Time.ToDateTime().ToUniversalTime(),
}).ToArray();
orderbookItemsBuffer.AddRange(asks);
orderbookItemsBuffer.AddRange(bids);
var message = new NewOrderbookMessage()
{
Ticker = _tradeDataProvider.GetTickerByFigi(response.Orderbook.Figi),
Figi = response.Orderbook.Figi,
Time = response.Orderbook.Time.ToDateTime().ToUniversalTime(),
Asks = asks,
Bids = bids,
AsksCount = asks.Length,
BidsCount = asks.Length,
};
await _eventBus.Broadcast(message);
}
if (orderbookItemsBuffer.Count + pricesBuffer.Count > 100 || (DateTime.UtcNow - lastWrite).TotalSeconds > 5)
{
try
{
using var context = await _dbContextFactory.CreateDbContextAsync();
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
lastWrite = DateTime.UtcNow;
if (orderbookItemsBuffer.Count > 0)
{
await context.OrderbookItems.AddRangeAsync(orderbookItemsBuffer);
orderbookItemsBuffer.Clear();
}
if (pricesBuffer.Count > 0)
{
await context.PriceChanges.AddRangeAsync(pricesBuffer);
pricesBuffer.Clear();
}
await context.SaveChangesAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка при сохранении данных биржи.");
}
}
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_cts.Cancel();
return Task.CompletedTask;
}
}
}