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

292 lines
11 KiB
C#

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<TradeCommand> _channel = Channel.CreateUnbounded<TradeCommand>();
#region Поля, собираемые из контейнера DI
private readonly InvestApiClient _investApiClient;
private readonly IDbContextFactory<TraderDbContext> _dbContextFactory;
private readonly ILogger<ManagedAccount> _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<string, Models.Asset> Assets = new();
#endregion
public ManagedAccount(InvestApiClient investApiClient, IDataBus dataBus, IDbContextFactory<TraderDbContext> dbContextFactory, ILogger<ManagedAccount> 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<DealResult> 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<DealResult> 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();
}
}
}