217 lines
8.7 KiB
C#
217 lines
8.7 KiB
C#
using KLHZ.Trader.Core.Common;
|
|
using KLHZ.Trader.Core.Contracts.Declisions.Interfaces;
|
|
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Enums;
|
|
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Intarfaces;
|
|
using KLHZ.Trader.Core.Contracts.Messaging.Interfaces;
|
|
using KLHZ.Trader.Core.DataLayer;
|
|
using KLHZ.Trader.Core.DataLayer.Entities.Declisions;
|
|
using KLHZ.Trader.Core.Exchange;
|
|
using KLHZ.Trader.Core.Exchange.Extentions;
|
|
using KLHZ.Trader.Core.Exchange.Models;
|
|
using KLHZ.Trader.Core.Exchange.Services;
|
|
using KLHZ.Trader.Core.Math.Declisions.Services;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Options;
|
|
using System.Collections.Concurrent;
|
|
using System.Threading.Channels;
|
|
using Tinkoff.InvestApi;
|
|
using AssetType = KLHZ.Trader.Core.Exchange.Models.AssetType;
|
|
|
|
namespace KLHZ.Trader.Core.Declisions.Services
|
|
{
|
|
public class Trader : IHostedService
|
|
{
|
|
private readonly InvestApiClient _investApiClient;
|
|
private readonly IServiceProvider _provider;
|
|
private readonly IDataBus _dataBus;
|
|
private readonly BotModeSwitcher _botModeSwitcher;
|
|
private readonly IDbContextFactory<TraderDbContext> _dbContextFactory;
|
|
private readonly ConcurrentDictionary<string, ManagedAccount> Accounts = new();
|
|
private readonly ConcurrentDictionary<string, IPriceHistoryCacheUnit> _historyCash = new();
|
|
private readonly ITradingEventsDetector _tradingEventsDetector;
|
|
|
|
|
|
private readonly decimal _futureComission;
|
|
private readonly decimal _shareComission;
|
|
private readonly decimal _accountCashPart;
|
|
private readonly decimal _accountCashPartFutures;
|
|
private readonly decimal _defaultBuyPartOfAccount;
|
|
private readonly string[] _managedAccountsNamePatterns = [];
|
|
|
|
private readonly Channel<INewPrice> _pricesChannel = Channel.CreateUnbounded<INewPrice>();
|
|
|
|
public Trader(
|
|
ITradingEventsDetector tradingEventsDetector,
|
|
BotModeSwitcher botModeSwitcher,
|
|
IServiceProvider provider,
|
|
IOptions<ExchangeConfig> options,
|
|
IDataBus dataBus,
|
|
IDbContextFactory<TraderDbContext> dbContextFactory,
|
|
InvestApiClient investApiClient)
|
|
{
|
|
_tradingEventsDetector = tradingEventsDetector;
|
|
_botModeSwitcher = botModeSwitcher;
|
|
_dataBus = dataBus;
|
|
_provider = provider;
|
|
_investApiClient = investApiClient;
|
|
_managedAccountsNamePatterns = options.Value.ManagingAccountNamePatterns.ToArray();
|
|
_dbContextFactory = dbContextFactory;
|
|
_futureComission = options.Value.FutureComission;
|
|
_shareComission = options.Value.ShareComission;
|
|
_accountCashPart = options.Value.AccountCashPart;
|
|
_accountCashPartFutures = options.Value.AccountCashPartFutures;
|
|
_defaultBuyPartOfAccount = options.Value.DefaultBuyPartOfAccount;
|
|
}
|
|
|
|
public async Task StartAsync(CancellationToken cancellationToken)
|
|
{
|
|
var accounts = await _investApiClient.GetAccounts(_managedAccountsNamePatterns);
|
|
var accountsList = new List<ManagedAccount>();
|
|
int i = 0;
|
|
foreach (var accountId in accounts)
|
|
{
|
|
var acc = _provider.GetKeyedService<ManagedAccount>(i);
|
|
if (acc != null)
|
|
{
|
|
await acc.Init(accountId);
|
|
Accounts[accountId] = acc;
|
|
i++;
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
_dataBus.AddChannel(nameof(Trader), _pricesChannel);
|
|
_ = ProcessMessages();
|
|
}
|
|
|
|
private async Task ProcessMessages()
|
|
{
|
|
while (await _pricesChannel.Reader.WaitToReadAsync())
|
|
{
|
|
var message = await _pricesChannel.Reader.ReadAsync();
|
|
if (_historyCash.TryGetValue(message.Figi, out var data))
|
|
{
|
|
await data.AddData(message);
|
|
}
|
|
else
|
|
{
|
|
data = new PriceHistoryCacheUnit2(message.Figi, message);
|
|
_historyCash.TryAdd(message.Figi, data);
|
|
}
|
|
var result = await _tradingEventsDetector.Detect(data);
|
|
|
|
try
|
|
{
|
|
if (result.LongOpen)
|
|
{
|
|
using var context = await _dbContextFactory.CreateDbContextAsync();
|
|
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
|
await context.Declisions.AddAsync(new Declision()
|
|
{
|
|
AccountId = string.Empty,
|
|
Figi = message.Figi,
|
|
Ticker = message.Ticker,
|
|
Price = message.Value,
|
|
Time = message.IsHistoricalData ? message.Time : DateTime.UtcNow,
|
|
Action = DeclisionTradeAction.OpenLong,
|
|
});
|
|
await context.SaveChangesAsync();
|
|
}
|
|
if (result.LongClose)
|
|
{
|
|
using var context = await _dbContextFactory.CreateDbContextAsync();
|
|
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
|
await context.Declisions.AddAsync(new Declision()
|
|
{
|
|
AccountId = string.Empty,
|
|
Figi = message.Figi,
|
|
Ticker = message.Ticker,
|
|
Price = message.Value,
|
|
Time = message.IsHistoricalData ? message.Time : DateTime.UtcNow,
|
|
Action = DeclisionTradeAction.CloseLong,
|
|
});
|
|
await context.SaveChangesAsync();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
public Task StopAsync(CancellationToken cancellationToken)
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private decimal GetComission(AssetType assetType)
|
|
{
|
|
if (assetType == AssetType.Common)
|
|
{
|
|
return _shareComission;
|
|
}
|
|
else if (assetType == AssetType.Futures)
|
|
{
|
|
return _futureComission;
|
|
}
|
|
else
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
private decimal GetCount(string accountId, decimal boutPrice)
|
|
{
|
|
var balance = Accounts[accountId].Balance;
|
|
return System.Math.Floor(balance * _defaultBuyPartOfAccount / boutPrice);
|
|
}
|
|
|
|
private bool IsBuyAllowed(string accountId, decimal boutPrice, decimal count, bool needBigCash)
|
|
{
|
|
if (!_botModeSwitcher.CanPurchase()) return false;
|
|
|
|
var balance = Accounts[accountId].Balance;
|
|
var total = Accounts[accountId].Total;
|
|
|
|
var futures = Accounts[accountId].Assets.Values.FirstOrDefault(v => v.Type == AssetType.Futures);
|
|
if (futures != null || needBigCash)
|
|
{
|
|
if ((balance - boutPrice * count) / total < _accountCashPartFutures) return false;
|
|
}
|
|
else
|
|
{
|
|
if ((balance - boutPrice * count) / total < _accountCashPart) return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool IsSellAllowed(AssetType assetType, PositionType positionType, decimal boutPrice, decimal? requiredPrice, TradeCommandType commandType)
|
|
{
|
|
if (commandType >= TradeCommandType.MarketSell && commandType < TradeCommandType.ForceClosePosition && requiredPrice.HasValue)
|
|
{
|
|
var comission = GetComission(assetType);
|
|
if (positionType == PositionType.Long)
|
|
{
|
|
return requiredPrice.Value * (1 - comission) > boutPrice * (1 + comission);
|
|
}
|
|
else if (positionType == PositionType.Short)
|
|
{
|
|
return requiredPrice.Value * (1 + comission) < boutPrice * (1 - comission);
|
|
}
|
|
}
|
|
|
|
if (commandType == TradeCommandType.ForceClosePosition) return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
}
|
|
}
|