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

1022 lines
49 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

using KLHZ.Trader.Core.Common;
using KLHZ.Trader.Core.Contracts.Declisions.Dtos.Enums;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces;
using KLHZ.Trader.Core.Contracts.Messaging.Interfaces;
using KLHZ.Trader.Core.DataLayer.Entities.Declisions;
using KLHZ.Trader.Core.DataLayer.Entities.Declisions.Enums;
using KLHZ.Trader.Core.DataLayer.Entities.Prices;
using KLHZ.Trader.Core.Exchange.Models.AssetsAccounting;
using KLHZ.Trader.Core.Exchange.Models.Configs;
using KLHZ.Trader.Core.Exchange.Models.Trading;
using KLHZ.Trader.Core.Exchange.Utils;
using KLHZ.Trader.Core.Math.Declisions.Dtos.FFT.Enums;
using KLHZ.Trader.Core.Math.Declisions.Utils;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Threading.Channels;
using Tinkoff.InvestApi;
using Tinkoff.InvestApi.V1;
using AssetType = KLHZ.Trader.Core.Exchange.Models.AssetsAccounting.AssetType;
namespace KLHZ.Trader.Core.Exchange.Services
{
public class Trader : IHostedService
{
private readonly IDataBus _dataBus;
private readonly TraderDataProvider _tradeDataProvider;
private readonly ILogger<Trader> _logger;
private readonly ConcurrentDictionary<string, TradingMode> TradingModes = new();
private readonly ConcurrentDictionary<string, DateTime> LongOpeningStops = new();
private readonly ConcurrentDictionary<string, DateTime> LongClosingStops = new();
private readonly ConcurrentDictionary<string, DateTime> ShortClosingStops = new();
private readonly ConcurrentDictionary<string, InstrumentSettings> Leverages = new();
private readonly decimal _futureComission;
private readonly decimal _shareComission;
private readonly decimal _accountCashPart;
private readonly decimal _accountCashPartFutures;
private readonly string[] _tradingInstrumentsFigis = [];
private readonly bool _isDebug = false;
private readonly Channel<INewPrice> _pricesChannel = Channel.CreateUnbounded<INewPrice>();
private readonly Channel<IOrderbook> _ordersbookChannel = Channel.CreateUnbounded<IOrderbook>();
public Trader(
ILogger<Trader> logger,
IOptions<ExchangeConfig> options,
IDataBus dataBus,
TraderDataProvider tradeDataProvider,
InvestApiClient investApiClient)
{
_tradeDataProvider = tradeDataProvider;
_logger = logger;
_dataBus = dataBus;
_futureComission = options.Value.FutureComission;
_shareComission = options.Value.ShareComission;
_accountCashPart = options.Value.AccountCashPart;
_accountCashPartFutures = options.Value.AccountCashPartFutures;
_tradingInstrumentsFigis = options.Value.TradingInstrumentsFigis;
_isDebug = !options.Value.ExchangeDataRecievingEnabled;
foreach (var f in _tradingInstrumentsFigis)
{
TradingModes[f] = TradingMode.None;
}
foreach (var lev in options.Value.InstrumentsSettings)
{
Leverages.TryAdd(lev.Figi, lev);
}
}
public async Task StartAsync(CancellationToken cancellationToken)
{
await _tradeDataProvider.Init();
_dataBus.AddChannel(nameof(Trader), _pricesChannel);
_dataBus.AddChannel(nameof(Trader), _ordersbookChannel);
_ = ProcessPrices();
_ = ProcessOrders();
if (!_isDebug)
{
_ = TradingModeUpdatingWorker();
}
}
public async ValueTask<(DateTime[] timestamps, decimal[] prices, bool isFullIntervalExists)> GetData(INewPrice message)
{
var data2 = await _tradeDataProvider.GetData(message.Figi, TimeSpan.FromHours(1.5));
if (!data2.isFullIntervalExists)
{
data2 = await _tradeDataProvider.GetData(message.Figi, TimeSpan.FromHours(1));
}
if (!data2.isFullIntervalExists)
{
data2 = await _tradeDataProvider.GetData(message.Figi, TimeSpan.FromHours(0.75));
}
return data2;
}
private async ValueTask<ValueAmplitudePosition> CheckPosition((DateTime[] timestamps, decimal[] prices, bool isFullIntervalExists) data, INewPrice message)
{
var currentTime = message.IsHistoricalData ? message.Time : DateTime.UtcNow;
var position = ValueAmplitudePosition.None;
var fft = await _tradeDataProvider.GetFFtResult(message.Figi);
var step = message.IsHistoricalData ? 5 : 5;
if (fft.IsEmpty || (currentTime - fft.LastTime).TotalSeconds > step)
{
if (data.isFullIntervalExists)
{
var interpolatedData = SignalProcessing.InterpolateData(data.timestamps, data.prices, TimeSpan.FromSeconds(5));
fft = FFT.Analyze(interpolatedData.timestamps, interpolatedData.values, message.Figi, TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(40));
await _tradeDataProvider.SetFFtResult(fft);
}
}
else
{
position = FFT.Check(fft, message.Time);
if (position == Math.Declisions.Dtos.FFT.Enums.ValueAmplitudePosition.UpperThen30Decil)
{
await LogPrice(message, "upper30percent", message.Value);
}
if (position == Math.Declisions.Dtos.FFT.Enums.ValueAmplitudePosition.LowerThenMediana)
{
await LogPrice(message, "lower30percent", message.Value);
}
}
return position;
}
private async Task ProcessPrices()
{
var pricesCache = new Dictionary<string, List<INewPrice>>();
var timesCache = new Dictionary<string, DateTime>();
while (await _pricesChannel.Reader.WaitToReadAsync())
{
var message = await _pricesChannel.Reader.ReadAsync();
try
{
#region Ускорение обработки исторических данных при отладке
if (message.IsHistoricalData)
{
await _tradeDataProvider.AddData(message, TimeSpan.FromHours(6));
if (!pricesCache.TryGetValue(message.Figi, out var list))
{
list = new List<INewPrice>();
pricesCache[message.Figi] = list;
}
list.Add(message);
if ((list.Last().Time - list.First().Time).TotalSeconds < 0.5)
{
list.Add(message);
continue;
}
else
{
message = new PriceChange()
{
Figi = message.Figi,
Ticker = message.Ticker,
Count = message.Count,
Direction = message.Direction,
IsHistoricalData = message.IsHistoricalData,
Time = message.Time,
Value = list.Sum(l => l.Value) / list.Count
};
list.Clear();
}
try
{
if (timesCache.TryGetValue(message.Figi, out var dt))
{
if ((message.Time - dt).TotalSeconds > 120)
{
timesCache[message.Figi] = message.Time;
TradingModes[message.Figi] = await CalcTradingMode(message);
}
}
else
{
timesCache[message.Figi] = message.Time;
}
}
catch (Exception ex)
{
}
}
if (TradingModes.TryGetValue(message.Figi, out var mode))
{
await LogPrice(message, "trading_mode", (decimal)mode);
}
//continue;
#endregion
if (message.Figi == "BBG004730N88")
{
if (message.Direction == 1)
{
await _tradeDataProvider.AddDataTo5MinuteWindowCache(message.Figi, Constants._1minBuyCacheKey, new Contracts.Declisions.Dtos.CachedValue()
{
Time = message.Time,
Value = (decimal)message.Count
});
}
if (message.Direction == 2)
{
await _tradeDataProvider.AddDataTo5MinuteWindowCache(message.Figi, Constants._1minSellCacheKey, new Contracts.Declisions.Dtos.CachedValue()
{
Time = message.Time,
Value = (decimal)message.Count
});
}
var sberSells = await _tradeDataProvider.GetDataFrom5MinuteWindowCache("BBG004730N88", Constants._1minSellCacheKey);
var sberBuys = await _tradeDataProvider.GetDataFrom5MinuteWindowCache("BBG004730N88", Constants._1minBuyCacheKey);
var sells = sberSells.Sum(s => s.Value);
var buys = sberBuys.Sum(s => s.Value);
var su = sells + buys;
if (su != 0)
{
await LogPrice(message, "sellsbuysbalance", (sells / su - 0.5m) * 2);
}
}
continue;
if (_tradingInstrumentsFigis.Contains(message.Figi))
{
var currentTime = message.IsHistoricalData ? message.Time : DateTime.UtcNow;
try
{
ProcessStops(message, currentTime);
var windowMaxSize = 2000;
await SellAssetsIfNeed(message);
var data = await _tradeDataProvider.GetData(message.Figi, windowMaxSize);
var state = ExchangeScheduler.GetCurrentState(message.Time);
await ProcessClearing(data, state, message);
if (TradingModes[message.Figi] == TradingMode.Stable)
{
await ProcessNewPriceIMOEXF_Stable(data, state, message, windowMaxSize);
}
else if (TradingModes[message.Figi] == TradingMode.SlowDropping)
{
await ProcessNewPriceIMOEXF_Dropping(data, state, message, windowMaxSize, 3);
}
else if (TradingModes[message.Figi] == TradingMode.Dropping)
{
await ProcessNewPriceIMOEXF_Dropping(data, state, message, windowMaxSize, 6);
}
else
{
await ProcessNewPriceIMOEXF2(data, state, message, windowMaxSize);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка при боработке новой цены IMOEXF");
}
}
}
catch(Exception e)
{
}
}
}
private async Task ProcessOrders()
{
while (true)
{
await ProcessOrdersAction();
await Task.Delay(5000);
}
}
private async Task ProcessOrdersAction(bool cancellAll = false, string? figi = null)
{
var accounts = _tradeDataProvider.Accounts.Values.ToArray();
foreach (var account in accounts)
{
foreach (var order in account.Orders)
{
if (!string.IsNullOrEmpty(figi))
{
if (order.Value.Figi != figi)
{
continue;
}
}
if (cancellAll || order.Value.ExpirationTime < DateTime.UtcNow)
{
await _dataBus.Broadcast(new TradeCommand()
{
AccountId = account.AccountId,
Figi = "",
OrderId = order.Key,
CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.CancelOrder,
});
}
}
}
}
private async Task SellAssetsIfNeed(INewPrice message)
{
if (!BotModeSwitcher.CanSell())
{
_logger.LogWarning("Сброс активов недоступен, т.к. отключены продажи.");
return;
}
var accounts = _tradeDataProvider.Accounts.Values.ToArray();
var assetType = _tradeDataProvider.GetAssetTypeByFigi(message.Figi);
foreach (var acc in accounts)
{
var assets = acc.Assets.Values.Where(a => a.Figi == message.Figi).ToArray();
foreach (var asset in assets)
{
if (await asset.Lock(TimeSpan.FromSeconds(60)))
{
var profit = TradingCalculator.CaclProfit(asset.BoughtPrice, message.Value,
GetComission(assetType), GetLeverage(message.Figi, asset.Count < 0), asset.Count < 0);
var stoppingKey = message.Figi + asset.AccountId;
if (profit < -100m)
{
var command = new TradeCommand()
{
AccountId = acc.AccountId,
Figi = message.Figi,
CommandType = asset.Count < 0 ? Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketBuy
: Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketSell,
Count = System.Math.Abs((long)asset.Count),
RecomendPrice = null,
EnableMargin = false,
};
await _dataBus.Broadcast(command);
_logger.LogWarning("Сброс актива {figi}! id команды {commandId} Направление сделки: {dir}; Количество активов: {count}; Разрешена ли маржиналка: {margin}",
message.Figi, command.CommandId, command.CommandType, command.Count, command.EnableMargin);
await LogDeclision(DeclisionTradeAction.CloseLong, message, profit);
await LogDeclision(DeclisionTradeAction.CloseLongReal, message, profit);
}
}
}
}
}
private async Task<TradingEvent> CheckByWindowAverageMean((DateTime[] timestamps, decimal[] prices) data,
INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullStep = 0m, decimal uptrendEndingDetectionMeanfullStep = 3m)
{
var resultMoveAvFull = MovingAverage.CheckByWindowAverageMean(data.timestamps, data.prices,
windowMaxSize, 30, 180, TimeSpan.FromSeconds(20), uptrendStartingDetectionMeanfullStep, uptrendEndingDetectionMeanfullStep);
if (resultMoveAvFull.bigWindowAv != 0)
{
await LogPrice(message, Constants.BigWindowCrossingAverageProcessor, resultMoveAvFull.bigWindowAv);
await LogPrice(message, Constants.SmallWindowCrossingAverageProcessor, resultMoveAvFull.smallWindowAv);
}
return resultMoveAvFull.events;
}
private Task<TradingEvent> CheckByWindowAverageMeanNolog((DateTime[] timestamps, decimal[] prices) data,
INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullStep = 0m, decimal uptrendEndingDetectionMeanfullStep = 3m)
{
var resultMoveAvFull = MovingAverage.CheckByWindowAverageMean(data.timestamps, data.prices,
windowMaxSize, 30, 180, TimeSpan.FromSeconds(20), uptrendStartingDetectionMeanfullStep, uptrendEndingDetectionMeanfullStep);
return Task.FromResult(resultMoveAvFull.events);
}
private Task<TradingEvent> CheckByWindowAverageMeanForShotrs((DateTime[] timestamps, decimal[] prices) data,
INewPrice message, int windowMaxSize)
{
var resultMoveAvFull = MovingAverage.CheckByWindowAverageMean(data.timestamps, data.prices,
windowMaxSize, 30, 240, TimeSpan.FromSeconds(20), -1m, 1m);
return Task.FromResult(resultMoveAvFull.events);
}
private Task<TradingEvent> CheckByLocalTrends((DateTime[] timestamps, decimal[] prices) data,
INewPrice message, int windowMaxSize)
{
var res = TradingEvent.None;
if (LocalTrends.TryGetLocalTrends(data.timestamps, data.prices, TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(20), 1, out var resLocalTrends))
{
res |= (resLocalTrends & TradingEvent.UptrendStart);
if ((resLocalTrends & TradingEvent.UptrendStart) == TradingEvent.UptrendStart)
{
res |= TradingEvent.DowntrendEnd;
}
}
return Task.FromResult(res);
}
private async Task<decimal?> GetAreasRelation((DateTime[] timestamps, decimal[] prices) data, INewPrice message)
{
var areasRel = -1m;
if (ShapeAreaCalculator.TryGetAreasRelation(data.timestamps, data.prices, message.Value, Constants.AreasRelationWindow, out var rel))
{
await _tradeDataProvider.AddDataTo1MinuteWindowCache(message.Figi, Constants._1minCacheKey, new Contracts.Declisions.Dtos.CachedValue()
{
Time = message.Time,
Value = (decimal)rel
});
var areas = await _tradeDataProvider.GetDataFrom1MinuteWindowCache(message.Figi, Constants._1minCacheKey);
areasRel = (decimal)areas.Sum(a => a.Value) / areas.Length;
await LogPrice(message, Constants.AreasRelationProcessor, areasRel);
return areasRel > 0 ? areasRel : null;
}
return null;
}
private async Task<ValueAmplitudePosition> CheckPosition(INewPrice message)
{
var data2 = await GetData(message);
var position = await CheckPosition(data2, message);
return position;
}
private async Task<decimal?> CalcTrendDiff(INewPrice message)
{
var data = await _tradeDataProvider.GetData(message.Figi, TimeSpan.FromHours(1));
if (data.isFullIntervalExists && LocalTrends.TryCalcTrendDiff(data.timestamps, data.prices, out var res))
{
return res;
}
return null;
}
private async Task ProcessNewPriceIMOEXF2((DateTime[] timestamps, decimal[] prices) data,
ExchangeState state,
INewPrice message, int windowMaxSize)
{
if (data.timestamps.Length <= 4)
{
return;
}
var sberSells = await _tradeDataProvider.GetDataFrom5MinuteWindowCache("BBG004730N88", Constants._1minSellCacheKey);
var sberBuys = await _tradeDataProvider.GetDataFrom5MinuteWindowCache("BBG004730N88", Constants._1minBuyCacheKey);
var sells = sberSells.Sum(s => s.Value);
var buys = sberBuys.Sum(s => s.Value);
var su = sells + buys;
if (su!=0)
{
var dsell = (sells / su - 0.5m) * 2;
}
var mavTask = CheckByWindowAverageMean(data, message, windowMaxSize, -1, 2m);
var mavTaskEnds = CheckByWindowAverageMeanNolog(data, message, windowMaxSize, -1, 1m);
var mavTaskShorts = CheckByWindowAverageMeanForShotrs(data, message, windowMaxSize);
var ltTask = CheckByLocalTrends(data, message, windowMaxSize);
var areasTask = GetAreasRelation(data, message);
var positionTask = CheckPosition(message);
var trendTask = CalcTrendDiff(message);
var ends = mavTaskEnds.Result & TradingEvent.UptrendEnd;
await Task.WhenAll(mavTask, ltTask, areasTask, positionTask, trendTask, mavTaskShorts, mavTaskEnds);
var assetType = _tradeDataProvider.GetAssetTypeByFigi(message.Figi);
var res = mavTask.Result | ltTask.Result;
res |= ends;
if ((res & TradingEvent.UptrendStart) == TradingEvent.UptrendStart
&& !LongOpeningStops.ContainsKey(message.Figi)
&& trendTask.Result.HasValue
&& trendTask.Result.Value > -5
&& state == ExchangeState.Open
&& areasTask.Result.HasValue
&& (areasTask.Result.Value >= 20 && areasTask.Result.Value < 75)
&& (positionTask.Result == ValueAmplitudePosition.LowerThenMediana)
)
{
if (!message.IsHistoricalData && BotModeSwitcher.CanPurchase())
{
var accounts = _tradeDataProvider.Accounts
.Where(a => !a.Value.Assets.ContainsKey(message.Figi))
.ToArray();
var loggedDeclisions = 0;
foreach (var acc in accounts)
{
if (IsBuyAllowed(acc.Value, message.Value, 1, _accountCashPartFutures, _accountCashPart))
{
if (RandomNumberGenerator.GetInt32(100) > 50 && await acc.Value.Lock(TimeSpan.FromSeconds(60)))
{
var command = new TradeCommand()
{
AccountId = acc.Value.AccountId,
Figi = message.Figi,
CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketBuy,
Count = 1,
RecomendPrice = null,
ExchangeObject = acc.Value,
};
await _dataBus.Broadcast(command);
_logger.LogWarning("Покупка актива {figi}! id команды {commandId}. Направление сделки: {dir}; Количество активов: {count}; Разрешена ли маржиналка: {margin}",
message.Figi, command.CommandId, command.CommandType, command.Count, command.EnableMargin);
if (loggedDeclisions == 0)
{
await LogDeclision(DeclisionTradeAction.OpenLongReal, message);
LongOpeningStops[message.Figi] = message.Time.AddMinutes(1);
loggedDeclisions++;
}
}
}
}
}
await LogDeclision(DeclisionTradeAction.OpenLong, message);
}
if ((res & TradingEvent.UptrendEnd) == TradingEvent.UptrendEnd)
{
if (!message.IsHistoricalData && BotModeSwitcher.CanSell())
{
var loggedDeclisions = 0;
var assetsForClose = _tradeDataProvider.Accounts
.SelectMany(a => a.Value.Assets.Values)
.Where(a => a.Figi == message.Figi && a.Count > 0)
.ToArray();
foreach (var asset in assetsForClose)
{
if (await asset.Lock(TimeSpan.FromSeconds(60)))
{
var profit = 0m;
if (assetType == AssetType.Common && asset.Count > 0)
{
profit = TradingCalculator.CaclProfit(asset.BoughtPrice, message.Value,
GetComission(assetType), 1, false);
}
if (assetType == AssetType.Futures)
{
profit = TradingCalculator.CaclProfit(asset.BoughtPrice, message.Value,
GetComission(assetType), GetLeverage(message.Figi, asset.Count < 0), asset.Count < 0);
}
if (profit > 0)
{
LongClosingStops[message.Figi] = message.Time.AddSeconds(30);
var command = new TradeCommand()
{
AccountId = asset.AccountId,
Figi = message.Figi,
CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketSell,
Count = (long)asset.Count,
RecomendPrice = null,
EnableMargin = false,
};
await _dataBus.Broadcast(command);
_logger.LogWarning("Продажа актива {figi}! id команды {commandId}. Направление сделки: {dir}; Количество активов: {count}; Разрешена ли маржиналка: {margin}",
message.Figi, command.CommandId, command.CommandType, command.Count, command.EnableMargin);
if (loggedDeclisions == 0)
{
loggedDeclisions++;
await LogDeclision(DeclisionTradeAction.CloseLongReal, message, profit);
}
}
}
}
}
await LogDeclision(DeclisionTradeAction.CloseLong, message);
}
if ((res & TradingEvent.DowntrendEnd) == TradingEvent.DowntrendEnd)
{
if (!ShortClosingStops.ContainsKey(message.Figi))
{
if (!message.IsHistoricalData && BotModeSwitcher.CanPurchase())
{
var loggedDeclisions = 0;
var assetsForClose = _tradeDataProvider.Accounts
.SelectMany(a => a.Value.Assets.Values)
.Where(a => a.Figi == message.Figi && a.Count < 0)
.ToArray();
foreach (var asset in assetsForClose)
{
if (await asset.Lock(TimeSpan.FromSeconds(60)))
{
var profit = 0m;
if (assetType == AssetType.Futures)
{
profit = TradingCalculator.CaclProfit(asset.BoughtPrice, message.Value,
GetComission(assetType), GetLeverage(message.Figi, asset.Count < 0), asset.Count < 0);
}
if (profit > 0)
{
var command = new TradeCommand()
{
AccountId = asset.AccountId,
Figi = message.Figi,
CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketBuy,
Count = System.Math.Abs((long)asset.Count),
RecomendPrice = null,
EnableMargin = false,
};
await _dataBus.Broadcast(command);
_logger.LogWarning("Продажа актива {figi}! id команды {commandId}. Направление сделки: {dir}; Количество активов: {count}; Разрешена ли маржиналка: {margin}",
message.Figi, command.CommandId, command.CommandType, command.Count, command.EnableMargin);
if (loggedDeclisions == 0)
{
loggedDeclisions++;
await LogDeclision(DeclisionTradeAction.CloseShortReal, message, profit);
}
}
}
}
}
if (message.IsHistoricalData)
{
ShortClosingStops[message.Figi] = message.Time.AddSeconds(30);
}
await LogDeclision(DeclisionTradeAction.CloseShort, message);
}
}
}
private async Task ProcessNewPriceIMOEXF_Stable(
(DateTime[] timestamps, decimal[] prices) data,
ExchangeState state,
INewPrice message, int windowMaxSize)
{
if (data.timestamps.Length <= 4 || state!=ExchangeState.Open)
{
return;
}
var mavTask = CheckByWindowAverageMean(data, message, windowMaxSize, -1, 1m);
var ltTask = CheckByLocalTrends(data, message, windowMaxSize);
var positionTask = CheckPosition(message);
await Task.WhenAll(mavTask, ltTask, positionTask);
var assetType = _tradeDataProvider.GetAssetTypeByFigi(message.Figi);
var res = mavTask.Result | ltTask.Result;
if ((res & TradingEvent.UptrendStart) == TradingEvent.UptrendStart && (positionTask.Result == ValueAmplitudePosition.None || positionTask.Result == ValueAmplitudePosition.LowerThenMediana))
{
if (!message.IsHistoricalData && BotModeSwitcher.CanPurchase())
{
var accounts = _tradeDataProvider.Accounts
.Where(a => !a.Value.Assets.ContainsKey(message.Figi))
.ToArray();
var loggedDeclisions = 0;
foreach (var acc in accounts)
{
if (IsBuyAllowed(acc.Value, message.Value, 1, _accountCashPartFutures, _accountCashPart))
{
if (RandomNumberGenerator.GetInt32(100) > 50 && await acc.Value.Lock(TimeSpan.FromSeconds(12)))
{
var command = new TradeCommand()
{
AccountId = acc.Value.AccountId,
Figi = message.Figi,
CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.LimitBuy,
Count = 1,
RecomendPrice = message.Value - 0.5m,
ExchangeObject = acc.Value,
};
await _dataBus.Broadcast(command);
_logger.LogWarning("Выставлена заявка на покупку актива {figi}! id команды {commandId}. Направление сделки: {dir}; Количество активов: {count}; Разрешена ли маржиналка: {margin}",
message.Figi, command.CommandId, command.CommandType, command.Count, command.EnableMargin);
if (loggedDeclisions == 0)
{
await LogDeclision(DeclisionTradeAction.OpenLongReal, message);
LongOpeningStops[message.Figi] = message.Time.AddMinutes(1);
loggedDeclisions++;
}
}
}
}
}
await LogDeclision(DeclisionTradeAction.OpenLong, message);
}
if (!message.IsHistoricalData)
{
foreach (var acc in _tradeDataProvider.Accounts)
{
if (acc.Value.Assets.TryGetValue(message.Figi, out var asset))
{
var order = acc.Value.Orders.Values.FirstOrDefault(o => o.Figi == message.Figi && o.Direction == DealDirection.Sell);
if (order == null && asset.Count > 0)
{
var command = new TradeCommand()
{
AccountId = asset.AccountId,
Figi = message.Figi,
CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.LimitSell,
Count = (long)asset.Count,
RecomendPrice = asset.BoughtPrice + 3,
EnableMargin = false,
};
await _dataBus.Broadcast(command);
}
}
}
}
}
private async Task ProcessNewPriceIMOEXF_Dropping(
(DateTime[] timestamps, decimal[] prices) data,
ExchangeState state,
INewPrice message, int windowMaxSize, decimal step)
{
if (data.timestamps.Length <= 4 && state !=ExchangeState.Open)
{
return;
}
var mavTask = CheckByWindowAverageMean(data, message, windowMaxSize, -1, 1m);
var ltTask = CheckByLocalTrends(data, message, windowMaxSize);
var positionTask = CheckPosition(message);
await Task.WhenAll(mavTask, ltTask, positionTask);
var assetType = _tradeDataProvider.GetAssetTypeByFigi(message.Figi);
var res = mavTask.Result | ltTask.Result;
if ((res & TradingEvent.UptrendEnd) == TradingEvent.UptrendEnd && (positionTask.Result != ValueAmplitudePosition.LowerThenMediana))
{
if (!message.IsHistoricalData && BotModeSwitcher.CanSell())
{
var accounts = _tradeDataProvider.Accounts
.Where(a => !a.Value.Assets.ContainsKey(message.Figi))
.ToArray();
var loggedDeclisions = 0;
foreach (var acc in accounts)
{
if (IsBuyAllowed(acc.Value, message.Value, 1, _accountCashPartFutures, _accountCashPart))
{
if (RandomNumberGenerator.GetInt32(100) > 50 && await acc.Value.Lock(TimeSpan.FromSeconds(12)))
{
var command = new TradeCommand()
{
AccountId = acc.Value.AccountId,
Figi = message.Figi,
CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketSell,
Count = 1,
RecomendPrice = message.Value,
ExchangeObject = acc.Value,
};
await _dataBus.Broadcast(command);
_logger.LogWarning("Выставлена заявка на продажу в шорт актива {figi}! id команды {commandId}. Направление сделки: {dir}; Количество активов: {count}; Разрешена ли маржиналка: {margin}",
message.Figi, command.CommandId, command.CommandType, command.Count, command.EnableMargin);
if (loggedDeclisions == 0)
{
await LogDeclision(DeclisionTradeAction.OpenLongReal, message);
LongOpeningStops[message.Figi] = message.Time.AddMinutes(1);
loggedDeclisions++;
}
}
}
}
}
await LogDeclision(DeclisionTradeAction.OpenShort, message);
}
if ((res & TradingEvent.UptrendStart) == TradingEvent.UptrendStart)
{
if (!ShortClosingStops.ContainsKey(message.Figi))
{
if (!message.IsHistoricalData && BotModeSwitcher.CanPurchase())
{
var loggedDeclisions = 0;
var assetsForClose = _tradeDataProvider.Accounts
.SelectMany(a => a.Value.Assets.Values)
.Where(a => a.Figi == message.Figi && a.Count < 0)
.ToArray();
foreach (var asset in assetsForClose)
{
if (await asset.Lock(TimeSpan.FromSeconds(60)))
{
var profit = 0m;
if (assetType == AssetType.Futures)
{
profit = TradingCalculator.CaclProfit(asset.BoughtPrice, message.Value,
GetComission(assetType), GetLeverage(message.Figi, asset.Count < 0), asset.Count < 0);
}
if (profit > 0)
{
var command = new TradeCommand()
{
AccountId = asset.AccountId,
Figi = message.Figi,
CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketBuy,
Count = System.Math.Abs((long)asset.Count),
RecomendPrice = null,
EnableMargin = false,
};
await _dataBus.Broadcast(command);
_logger.LogWarning("Продажа актива {figi}! id команды {commandId}. Направление сделки: {dir}; Количество активов: {count}; Разрешена ли маржиналка: {margin}",
message.Figi, command.CommandId, command.CommandType, command.Count, command.EnableMargin);
if (loggedDeclisions == 0)
{
loggedDeclisions++;
await LogDeclision(DeclisionTradeAction.CloseShortReal, message, profit);
}
}
}
}
}
if (message.IsHistoricalData)
{
ShortClosingStops[message.Figi] = message.Time.AddSeconds(30);
}
await LogDeclision(DeclisionTradeAction.CloseShort, message);
}
}
if (!message.IsHistoricalData)
{
foreach (var acc in _tradeDataProvider.Accounts)
{
if (acc.Value.Assets.TryGetValue(message.Figi, out var asset))
{
var order = acc.Value.Orders.Values.FirstOrDefault(o => o.Figi == message.Figi && o.Direction == DealDirection.Buy);
if (order == null && asset.Count < 0)
{
var command = new TradeCommand()
{
AccountId = asset.AccountId,
Figi = message.Figi,
CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.LimitBuy,
Count = System.Math.Abs((long)asset.Count),
RecomendPrice = asset.BoughtPrice - step,
EnableMargin = false,
};
await _dataBus.Broadcast(command);
}
}
}
}
}
private async Task ProcessClearing((DateTime[] timestamps, decimal[] prices) data, ExchangeState state, INewPrice message)
{
if (state == ExchangeState.ClearingTime
&& !message.IsHistoricalData
&& data.timestamps.Length > 1
&& (data.timestamps[data.timestamps.Length - 1] - data.timestamps[data.timestamps.Length - 2]) > TimeSpan.FromMinutes(3))
{
await _tradeDataProvider.UpdateFuturesPrice(message, data.prices[data.prices.Length - 2]);
}
}
private void ProcessStops(INewPrice message, DateTime currentTime)
{
if (LongOpeningStops.TryGetValue(message.Figi, out var dt))
{
if (dt < currentTime)
{
LongOpeningStops.TryRemove(message.Figi, out _);
}
}
if (ShortClosingStops.TryGetValue(message.Figi, out var dt2))
{
if (dt2 < currentTime)
{
ShortClosingStops.TryRemove(message.Figi, out _);
}
}
if (LongClosingStops.TryGetValue(message.Figi, out var dt3))
{
if (dt3 < currentTime)
{
LongClosingStops.TryRemove(message.Figi, out _);
}
}
}
private async Task LogPrice(INewPrice message, string processor, decimal value)
{
await _tradeDataProvider.LogPrice(new ProcessedPrice()
{
Figi = message.Figi,
Ticker = message.Ticker,
Processor = processor,
Time = message.Time,
Value = value,
}, false);
}
private async Task LogDeclision(DeclisionTradeAction action, INewPrice message, decimal? profit = null)
{
await _tradeDataProvider.LogDeclision(new Declision()
{
AccountId = string.Empty,
Figi = message.Figi,
Ticker = message.Ticker,
Value = profit,
Price = message.Value,
Time = message.IsHistoricalData ? message.Time : DateTime.UtcNow,
Action = action,
}, false);
}
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 GetLeverage(string figi, bool isShort)
{
var res = 1m;
if (Leverages.TryGetValue(figi, out var leverage))
{
res = isShort ? leverage.ShortLeverage : leverage.LongLeverage;
}
return res;
}
private async Task<TradingMode> CalcTradingMode(string figi)
{
var res = TradingMode.None;
var largeData = await _tradeDataProvider.GetData(figi, TimeSpan.FromMinutes(90));
var smallData = await _tradeDataProvider.GetData(figi, TimeSpan.FromMinutes(15));
if (largeData.isFullIntervalExists && smallData.isFullIntervalExists)
{
if (LocalTrends.TryCalcTrendDiff(largeData.timestamps, largeData.prices, out var largeDataRes)
&& LocalTrends.TryCalcTrendDiff(smallData.timestamps, smallData.prices, out var smallDataRes))
{
if (largeDataRes>0 && largeDataRes <= 4 && System.Math.Abs(smallDataRes)<3)
{
res = TradingMode.Stable;
}
if (largeDataRes < 0 && largeDataRes >= -5 && smallDataRes < 1)
{
res = TradingMode.SlowDropping;
}
if (largeDataRes>5 && smallDataRes > 0)
{
res = TradingMode.Growing;
}
if (largeDataRes < -5 && smallDataRes < 0)
{
res = TradingMode.Dropping;
}
}
}
return res;
}
private async Task<TradingMode> CalcTradingMode(INewPrice message)
{
var res = await CalcTradingMode(message.Figi);
//await LogPrice(message, "trading_mode", (int)res);
return res;
}
private async Task TradingModeUpdatingWorker()
{
while (true)
{
try
{
foreach (var figi in _tradingInstrumentsFigis)
{
TradingModes[figi] = await CalcTradingMode(figi);
}
await Task.Delay(120000);
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка при вычислении режима торговли.");
}
}
}
internal static bool IsBuyAllowed(ManagedAccount account, decimal boutPrice, decimal count,
decimal accountCashPartFutures, decimal accountCashPart)
{
if (!BotModeSwitcher.CanPurchase()) return false;
var balance = account.Balance;
var total = account.Total;
var futures = account.Assets.Values.FirstOrDefault(v => v.Type == AssetType.Futures);
if (futures != null)
{
if ((balance - boutPrice * count) / total < accountCashPartFutures) return false;
}
else
{
if ((balance - boutPrice * count) / total < accountCashPart) return false;
}
return true;
}
}
}