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

358 lines
15 KiB
C#

using KLHZ.Trader.Core.Common.Extentions;
using KLHZ.Trader.Core.Contracts.Common.Enums;
using KLHZ.Trader.Core.DataLayer;
using KLHZ.Trader.Core.Exchange.Extentions;
using KLHZ.Trader.Core.Exchange.Interfaces;
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<string, Asset> Assets => GetAssets();
private readonly InvestApiClient _investApiClient;
private readonly IDbContextFactory<TraderDbContext> _dbContextFactory;
private readonly ILogger<TraderDataProvider> _logger;
private readonly IOptions<ExchangeConfig> _options;
private readonly Dictionary<string, Asset> _assets = new();
private readonly ConcurrentDictionary<string, DateTime> _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<ExchangeConfig> options, IDbContextFactory<TraderDbContext> dbContextFactory, ILogger<TraderDataProvider> 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<string, Asset> GetAssets()
{
var res = ImmutableDictionary<string, Asset>.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 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 asset = new Asset()
{
AssetId = newAssetId,
AccountId = AccountId,
Figi = position.Figi,
Ticker = position.Ticker,
BoughtAt = oldAsset?.BoughtAt ?? DateTime.UtcNow,
BoughtPrice = oldAsset?.BoughtPrice ?? 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 = System.Math.Abs((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 = System.Math.Abs((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();
}
}
}
}