262 lines
10 KiB
C#
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;
|
|
}
|
|
|
|
}
|
|
}
|