358 lines
15 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|
|
}
|