klhztrader/KLHZ.Trader.Core/Declisions/Services/Trader.cs

262 lines
10 KiB
C#

using KLHZ.Trader.Core.Common;
using KLHZ.Trader.Core.Common.Messaging.Contracts;
using KLHZ.Trader.Core.Common.Messaging.Contracts.Messages;
using KLHZ.Trader.Core.Common.Messaging.Contracts.Messages.Enums;
using KLHZ.Trader.Core.DataLayer;
using KLHZ.Trader.Core.DataLayer.Entities.Declisions;
using KLHZ.Trader.Core.Declisions.Models;
using KLHZ.Trader.Core.Declisions.Utils;
using KLHZ.Trader.Core.Exchange;
using KLHZ.Trader.Core.Exchange.Extentions;
using KLHZ.Trader.Core.Exchange.Models;
using KLHZ.Trader.Core.Exchange.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, PriceHistoryCacheUnit> _historyCash = new();
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<INewPriceMessage> _pricesChannel = Channel.CreateUnbounded<INewPriceMessage>();
public Trader(
BotModeSwitcher botModeSwitcher,
IServiceProvider provider,
IOptions<ExchangeConfig> options,
IDataBus dataBus,
IDbContextFactory<TraderDbContext> dbContextFactory,
InvestApiClient investApiClient)
{
_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))
{
data.AddData(message);
}
else
{
data = new PriceHistoryCacheUnit(message.Figi, message);
_historyCash.TryAdd(message.Figi, data);
}
if (message.IsHistoricalData)
{
float meanfullDiff;
if (message.Figi == "BBG004730N88")
{
meanfullDiff = 0.16f;
}
else if (message.Figi == "FUTIMOEXF000")
{
meanfullDiff = 1.5f;
}
else
{
continue;
}
try
{
var downtrendStarts = data.CheckDowntrendStarting(TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(7), meanfullDiff);
var uptrendStarts = data.CheckUptrendStarting(TimeSpan.FromSeconds(45), TimeSpan.FromSeconds(10), meanfullDiff);
var downtrendEnds = data.CheckDowntrendEnding(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(15), meanfullDiff);
var uptrendEnds = data.CheckUptrendEnding(TimeSpan.FromSeconds(25), TimeSpan.FromSeconds(11), meanfullDiff);
//var uptrendEnds2 = data.CheckUptrendEnding(TimeSpan.FromSeconds(20), TimeSpan.FromSeconds(20), meanfullDiff);
//var uptrendEnds = uptrendEnds1 || uptrendEnds2;
var declisionAction = DeclisionTradeAction.Unknown;
if (downtrendStarts)
{
//declisionAction = DeclisionTradeAction.OpenShort;
}
else if (uptrendStarts)
{
declisionAction = DeclisionTradeAction.OpenLong;
}
else if (downtrendEnds)
{
//declisionAction = DeclisionTradeAction.CloseShort;
}
else if(uptrendEnds)
{
declisionAction = DeclisionTradeAction.CloseLong;
}
if (declisionAction != DeclisionTradeAction.Unknown)
{
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 = declisionAction,
});
await context.SaveChangesAsync();
}
}
catch (Exception ex)
{
}
}
}
}
public async Task Preprocess(string figi)
{
if (_historyCash.TryGetValue(figi, out var unit))
{
var periodData1 = unit.GetPriceDiffForTimeSpan(TimeSpan.Zero, TimeSpan.FromSeconds(10));
var periodData2 = unit.GetPriceDiffForTimeSpan(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30));
if (Math.Abs(periodData1.PeriodDiff) <= 1 && periodData2.PeriodDiff > 2)
{
//можно покупать.
}
if (Math.Abs(periodData1.PeriodDiff) <= 1 && periodData2.PeriodDiff < -2)
{
//можно продавать.
}
}
}
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;
}
}
}