1021 lines
49 KiB
C#
1021 lines
49 KiB
C#
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);
|
||
}
|
||
}
|
||
|
||
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 && await asset.Lock(TimeSpan.FromSeconds(60)))
|
||
{
|
||
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 && await asset.Lock(TimeSpan.FromSeconds(60)))
|
||
{
|
||
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;
|
||
}
|
||
}
|
||
}
|