992 lines
48 KiB
C#
992 lines
48 KiB
C#
using KLHZ.Trader.Core.Common;
|
||
using KLHZ.Trader.Core.Contracts.Common.Enums;
|
||
using KLHZ.Trader.Core.Contracts.Declisions.Dtos.Enums;
|
||
using KLHZ.Trader.Core.Contracts.Messaging.Dtos;
|
||
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Enums;
|
||
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces;
|
||
using KLHZ.Trader.Core.Contracts.Messaging.Interfaces;
|
||
using KLHZ.Trader.Core.DataLayer.Entities.Declisions.Enums;
|
||
using KLHZ.Trader.Core.Exchange.Interfaces;
|
||
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.Models.Trading.Enums;
|
||
using KLHZ.Trader.Core.Exchange.Utils;
|
||
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.Collections.Immutable;
|
||
using System.Security.Cryptography;
|
||
using System.Threading.Channels;
|
||
using Tinkoff.InvestApi;
|
||
|
||
namespace KLHZ.Trader.Core.Exchange.Services
|
||
{
|
||
public class Trader : IHostedService
|
||
{
|
||
private readonly IDataBus _dataBus;
|
||
private readonly TraderDataProvider _tradeDataProvider;
|
||
private readonly PortfolioWrapper _portfolioWrapper;
|
||
private readonly ExchangeConfig _exchangeConfig;
|
||
private readonly ILogger<Trader> _logger;
|
||
|
||
private readonly ConcurrentDictionary<string, SupportLevel[]> SupportLevels = new();
|
||
private readonly ConcurrentDictionary<string, decimal> _pirsonValues = new();
|
||
private readonly ConcurrentDictionary<string, LinkedList<SupportLevel[]>> SupportLevelsHistory = new();
|
||
|
||
private readonly ConcurrentDictionary<string, DateTime> _supportLevelsCalculationTimes = new();
|
||
private readonly ConcurrentDictionary<string, DateTime> _marginCloses = new();
|
||
private readonly ConcurrentDictionary<string, DateTime> _usedSupportLevels = new();
|
||
private readonly ConcurrentDictionary<string, DateTime> _usedSupportLevelsForClosing = new();
|
||
private readonly ConcurrentDictionary<string, ITradeDataItem> _oldItems = new();
|
||
|
||
private readonly Channel<ITradeDataItem> _pricesChannel = Channel.CreateUnbounded<ITradeDataItem>();
|
||
private readonly Channel<ITradeCommand> _commands = Channel.CreateUnbounded<ITradeCommand>();
|
||
private readonly Channel<IOrderbook> _orderbooks = Channel.CreateUnbounded<IOrderbook>();
|
||
public Trader(
|
||
ILogger<Trader> logger,
|
||
IOptions<ExchangeConfig> options,
|
||
IDataBus dataBus,
|
||
PortfolioWrapper portfolioWrapper,
|
||
TraderDataProvider tradeDataProvider,
|
||
InvestApiClient investApiClient)
|
||
{
|
||
_portfolioWrapper = portfolioWrapper;
|
||
_tradeDataProvider = tradeDataProvider;
|
||
_logger = logger;
|
||
_dataBus = dataBus;
|
||
_exchangeConfig = options.Value;
|
||
}
|
||
|
||
public Task StartAsync(CancellationToken cancellationToken)
|
||
{
|
||
_dataBus.AddChannel(nameof(Trader), _pricesChannel);
|
||
_dataBus.AddChannel(nameof(Trader), _orderbooks);
|
||
_dataBus.AddChannel(nameof(Trader), _commands);
|
||
_ = ProcessPrices();
|
||
_ = ProcessOrderbooks();
|
||
_ = ProcessCommands();
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
private async Task ProcessCommands()
|
||
{
|
||
while (await _commands.Reader.WaitToReadAsync())
|
||
{
|
||
var command = await _commands.Reader.ReadAsync();
|
||
try
|
||
{
|
||
if (command.CommandType == TradeCommandType.OpenLong
|
||
|| command.CommandType == TradeCommandType.OpenShort)
|
||
{
|
||
ITradeDataItem message;
|
||
if (_oldItems.TryGetValue(command.Figi, out var message1))
|
||
{
|
||
message = message1;
|
||
}
|
||
else
|
||
{
|
||
message = new TradeDataItem() { Figi = command.Figi, Ticker = "", Count = command.Count, Direction = 1, IsHistoricalData = false, Time = DateTime.UtcNow, Price = command.RecomendPrice ?? 0m };
|
||
}
|
||
|
||
var positionType = command.CommandType == TradeCommandType.OpenLong ? PositionType.Long : PositionType.Short;
|
||
var st = GetStops(message);
|
||
var stops = st.GetStops(positionType);
|
||
var accounts = _portfolioWrapper.Accounts
|
||
.Where(a => !a.Value.Assets.ContainsKey(command.Figi))
|
||
.Take(1)
|
||
.Select(a => a.Value)
|
||
.ToArray();
|
||
await OpenPositions(accounts, message, positionType, stops.stopLoss, stops.takeProfit, System.Math.Abs(command.Count));
|
||
}
|
||
else
|
||
{
|
||
var fakeMessage = new TradeDataItem() { Figi = command.Figi, Ticker = "", Count = command.Count, Direction = 1, IsHistoricalData = false, Time = DateTime.UtcNow, Price = command.RecomendPrice ?? 0m };
|
||
var assetsForClose = _portfolioWrapper.Accounts
|
||
.SelectMany(a => a.Value.Assets.Values)
|
||
.Where(a => a.Figi == fakeMessage.Figi)
|
||
.ToArray();
|
||
await ClosePositions(assetsForClose, fakeMessage, false);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "Ошибка при выполнении команды.");
|
||
}
|
||
}
|
||
}
|
||
|
||
private async Task ProcessOrderbooks()
|
||
{
|
||
while (await _orderbooks.Reader.WaitToReadAsync())
|
||
{
|
||
var message = await _orderbooks.Reader.ReadAsync();
|
||
await _tradeDataProvider.AddOrderbook(message);
|
||
}
|
||
}
|
||
|
||
private async Task ProcessPrices()
|
||
{
|
||
var pricesCache1 = new Dictionary<string, List<ITradeDataItem>>();
|
||
var pricesCache2 = new Dictionary<string, List<ITradeDataItem>>();
|
||
|
||
while (await _pricesChannel.Reader.WaitToReadAsync())
|
||
{
|
||
var message = await _pricesChannel.Reader.ReadAsync();
|
||
if (!message.IsHistoricalData && DateTime.UtcNow - message.Time > TimeSpan.FromMinutes(1))
|
||
{
|
||
continue;
|
||
}
|
||
await CloseMarginPositionsIfNeed(message);
|
||
|
||
try
|
||
{
|
||
if (message.IsHistoricalData)
|
||
{
|
||
message = TraderUtils.FilterHighFreqValues(message, message.Direction == 1 ? pricesCache1 : pricesCache2);
|
||
}
|
||
|
||
if (_exchangeConfig.TradingInstrumentsFigis.Contains(message.Figi) && message.Direction == 1)
|
||
{
|
||
await _tradeDataProvider.AddData(message);
|
||
|
||
if (message.Figi == "FUTIMOEXF000")
|
||
{
|
||
await ProcessIMOEXF(message);
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "Ошибка при боработке новой цены.");
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
private async Task ProcessIMOEXF(ITradeDataItem message)
|
||
{
|
||
if (message.Figi == "FUTIMOEXF000")
|
||
{
|
||
await CalcSupportLevels(message, 3, 3);
|
||
var processSupportLevelsRes = await ProcessSupportLevels(message);
|
||
|
||
var mode = await CalcTradingMode(message);
|
||
var stops = GetStops(message);
|
||
if ((mode & TradingMode.InSupportLevel) == TradingMode.InSupportLevel)
|
||
{
|
||
if ((mode & TradingMode.Bezpont) == TradingMode.Bezpont)
|
||
{
|
||
|
||
}
|
||
else
|
||
{
|
||
var mavRes = await CalcTimeWindowAverageValue(message, true);
|
||
await ExecuteDeclisions(mavRes, message, stops, 1);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
var mavRes = await CalcTimeWindowAverageValue(message);
|
||
var pirson = await CalcPirson(message);
|
||
var declisionPirson = ProcessPirson(pirson, message);
|
||
var declisionsStops = ProcessStops(stops, 2m);
|
||
var tradingModeResult = ProcessTradingMode(mode);
|
||
|
||
var res = TraderUtils.MergeResultsMult(declisionPirson, processSupportLevelsRes);
|
||
res = TraderUtils.MergeResultsMult(res, declisionsStops);
|
||
res = TraderUtils.MergeResultsMult(res, tradingModeResult);
|
||
res = TraderUtils.MergeResultsMax(res, mavRes);
|
||
|
||
await ExecuteDeclisions(res.ToImmutableDictionary(), message, stops, 1);
|
||
}
|
||
_oldItems[message.Figi] = message;
|
||
}
|
||
}
|
||
|
||
private async Task<TradingMode> CalcTradingMode(ITradeDataItem message)
|
||
{
|
||
var res = TradingMode.None;
|
||
|
||
//var res1hour = await CalcTradingMode(message, TimeSpan.FromMinutes(60),8);
|
||
//var res30min = await CalcTradingMode(message, TimeSpan.FromMinutes(30),6);
|
||
|
||
var res20min = await CalcTradingMode(message, TimeSpan.FromMinutes(20), 3);
|
||
var res10min = await CalcTradingMode(message, TimeSpan.FromMinutes(10), 3);
|
||
//res |= res1hour;
|
||
//res|=res30min;
|
||
res |= res20min;
|
||
res &= res10min;
|
||
|
||
if ((res & TradingMode.TryingGrowFromSupportLevel) == TradingMode.TryingGrowFromSupportLevel)
|
||
{
|
||
await _tradeDataProvider.LogPrice(message, "TryingGrowFromSupportLevel", message.Price);
|
||
}
|
||
if ((res & TradingMode.TryingFallFromSupportLevel) == TradingMode.TryingFallFromSupportLevel)
|
||
{
|
||
await _tradeDataProvider.LogPrice(message, "TryingFallFromSupportLevel", message.Price);
|
||
}
|
||
|
||
if ((res & TradingMode.InSupportLevel) == TradingMode.InSupportLevel)
|
||
{
|
||
await _tradeDataProvider.LogPrice(message, "InSupportLevel", message.Price);
|
||
}
|
||
if ((res & TradingMode.Growing) == TradingMode.Growing)
|
||
{
|
||
await _tradeDataProvider.LogPrice(message, "InSupportLevelGrowing", message.Price);
|
||
}
|
||
if ((res & TradingMode.Falling) == TradingMode.Falling)
|
||
{
|
||
await _tradeDataProvider.LogPrice(message, "InSupportLevelGrowingFalling", message.Price);
|
||
}
|
||
if ((res & TradingMode.Bezpont) == TradingMode.Bezpont)
|
||
{
|
||
var data = await _tradeDataProvider.GetDataForTimeWindow(message.Figi, TimeSpan.FromMinutes(30), selector: (i) => i.Direction == 1);
|
||
|
||
if (data.Any() && (data.Max(d => d.Price) - data.Min(d => d.Price)) < 4)
|
||
{
|
||
await _tradeDataProvider.LogPrice(message, "Bezpont", message.Price);
|
||
}
|
||
else
|
||
{
|
||
res |= ~TradingMode.Bezpont;
|
||
}
|
||
}
|
||
|
||
return res;
|
||
}
|
||
|
||
private async Task<TradingMode> CalcTradingMode(ITradeDataItem message, TimeSpan period, decimal meanfullValue)
|
||
{
|
||
var res = TradingMode.None;
|
||
|
||
var data = await _tradeDataProvider.GetDataForTimeWindow(message.Figi, period);
|
||
if (LocalTrends.TryCalcTrendDiff(data, out var diff))
|
||
{
|
||
var items = data.Where(d => d.AttachedInfo != null).ToArray();
|
||
var itemsInSupportLevels = (decimal)items.Count(i => i.AttachedInfo?.Key == Constants.PriceIsInSupportLevel);
|
||
var itemsHigherThenSupportLevel = (decimal)items.Count(i => i.AttachedInfo?.Key == Constants.PriceIsNotInSupportLevel
|
||
&& (i.AttachedInfo?.Value2 < i.Price));
|
||
var itemsLowerThenSupportLevel = (decimal)items.Count(i => i.AttachedInfo?.Key == Constants.PriceIsNotInSupportLevel
|
||
&& (i.AttachedInfo?.Value1 > i.Price));
|
||
|
||
if (items.Length > 0)
|
||
{
|
||
var itemsInSupportLevelsRel = itemsInSupportLevels / items.Length;
|
||
var itemsHigherThenSupportLevelRel = itemsHigherThenSupportLevel / items.Length;
|
||
var itemsLowerThenSupportLevelRel = itemsLowerThenSupportLevel / items.Length;
|
||
|
||
if (itemsInSupportLevelsRel > 0.7m && message?.AttachedInfo?.Key == Constants.PriceIsInSupportLevel)
|
||
{
|
||
res |= TradingMode.InSupportLevel;
|
||
if (itemsHigherThenSupportLevelRel > 0.05m && (itemsLowerThenSupportLevelRel == 0 || itemsHigherThenSupportLevelRel / itemsLowerThenSupportLevelRel > 2))
|
||
{
|
||
res |= TradingMode.TryingGrowFromSupportLevel;
|
||
}
|
||
if (itemsLowerThenSupportLevelRel > 0.05m && (itemsHigherThenSupportLevelRel == 0 || itemsLowerThenSupportLevelRel / itemsHigherThenSupportLevelRel > 2))
|
||
{
|
||
res |= TradingMode.TryingGrowFromSupportLevel;
|
||
}
|
||
}
|
||
|
||
|
||
if (diff > meanfullValue)
|
||
{
|
||
res |= TradingMode.Growing;
|
||
}
|
||
if (diff < -meanfullValue)
|
||
{
|
||
res |= TradingMode.Falling;
|
||
}
|
||
if (itemsInSupportLevelsRel > 0.8m && message?.AttachedInfo?.Key == Constants.PriceIsInSupportLevel &&
|
||
System.Math.Abs(diff) < 1.5m * meanfullValue)
|
||
{
|
||
res |= TradingMode.Bezpont;
|
||
}
|
||
}
|
||
}
|
||
|
||
return res;
|
||
}
|
||
|
||
private async Task<ImmutableDictionary<TradingEvent, decimal>> CalcTimeWindowAverageValue(ITradeDataItem message, bool calcOpens = false)
|
||
{
|
||
var res = TraderUtils.GetInitDict(Constants.BlockingCoefficient);
|
||
var cacheSize = TimeSpan.FromSeconds(60 * 60);
|
||
var data = await _tradeDataProvider.GetDataForTimeWindow(message.Figi, cacheSize, selector: (i) => i.Direction == 1);
|
||
|
||
if (calcOpens)
|
||
{
|
||
var re = MovingAverage.CheckByWindowAverageMean2(data, 100, 15, 300, -2m, 2m);
|
||
|
||
if ((re.events & TradingEvent.OpenShort) == TradingEvent.OpenShort)
|
||
{
|
||
res[TradingEvent.OpenShort] = Constants.PowerUppingCoefficient;
|
||
}
|
||
if ((re.events & TradingEvent.OpenLong) == TradingEvent.OpenLong)
|
||
{
|
||
res[TradingEvent.OpenLong] = Constants.PowerUppingCoefficient;
|
||
}
|
||
}
|
||
|
||
var closings = MovingAverage.CheckByWindowAverageMean2(data, data.Length, 15, 300, -5m, 5m);
|
||
if (closings.smallWindowAv != 0)
|
||
{
|
||
await _tradeDataProvider.LogPrice(message, "maw_small", closings.smallWindowAv);
|
||
await _tradeDataProvider.LogPrice(message, "maw_big", closings.bigWindowAv);
|
||
}
|
||
if ((closings.events & TradingEvent.CloseShort) == TradingEvent.CloseShort)
|
||
{
|
||
res[TradingEvent.CloseShort] = Constants.PowerUppingCoefficient;
|
||
}
|
||
if ((closings.events & TradingEvent.CloseLong) == TradingEvent.CloseLong)
|
||
{
|
||
res[TradingEvent.CloseLong] = Constants.PowerUppingCoefficient;
|
||
}
|
||
|
||
return res.ToImmutableDictionary();
|
||
}
|
||
|
||
private ImmutableDictionary<TradingEvent, decimal> ProcessTradingMode(TradingMode mode)
|
||
{
|
||
var res = TraderUtils.GetInitDict(1);
|
||
if ((mode & TradingMode.Growing) == TradingMode.Growing)
|
||
{
|
||
res[TradingEvent.OpenShort] = Constants.BlockingCoefficient;
|
||
res[TradingEvent.OpenLong] = Constants.UppingCoefficient;
|
||
}
|
||
if ((mode & TradingMode.Falling) == TradingMode.Growing)
|
||
{
|
||
res[TradingEvent.OpenLong] = Constants.BlockingCoefficient;
|
||
res[TradingEvent.OpenShort] = Constants.UppingCoefficient;
|
||
}
|
||
|
||
return res.ToImmutableDictionary();
|
||
}
|
||
|
||
private ImmutableDictionary<TradingEvent, decimal> ProcessPirson(PirsonCalculatingResult pirson, ITradeDataItem message)
|
||
{
|
||
var res = TraderUtils.GetInitDict(Constants.BlockingCoefficient);
|
||
if (pirson.Success && _pirsonValues.TryGetValue(message.Figi, out var olddpirs))
|
||
{
|
||
|
||
if (olddpirs < -0.3m && pirson.Pirson > -0.3m && pirson.PriceDiff > 0 && (pirson.TradesDiffRelative > 0.2m))
|
||
{
|
||
res[TradingEvent.OpenLong] = Constants.PowerUppingCoefficient;
|
||
}
|
||
|
||
if (olddpirs > 0.3m && pirson.Pirson < 0.3m && pirson.PriceDiff < 0 && (pirson.TradesDiffRelative > 0.2m))
|
||
{
|
||
res[TradingEvent.OpenShort] = Constants.PowerUppingCoefficient;
|
||
}
|
||
|
||
|
||
if (pirson.Pirson > 0.5m && pirson.Pirson < 0.8m && (pirson.Pirson - olddpirs > 0.05m) && pirson.PriceDiff > 0 && (pirson.TradesDiffRelative > 0.25m))
|
||
{
|
||
res[TradingEvent.OpenLong] = Constants.PowerUppingCoefficient;
|
||
}
|
||
|
||
if (pirson.Pirson < -0.5m && pirson.Pirson > -0.8m && (pirson.Pirson - olddpirs < -0.05m) && pirson.PriceDiff < 0 && (pirson.TradesDiffRelative > 0.25m))
|
||
{
|
||
res[TradingEvent.OpenShort] = Constants.PowerUppingCoefficient;
|
||
}
|
||
|
||
//if (pirson.Pirson > 0.7m && (pirson.Pirson > olddpirs) && pirson.PriceDiff > 0 && (pirson.TradesDiffRelative > 0.25m))
|
||
//{
|
||
// res[TradingEvent.OpenLong] = Constants.UppingCoefficient;
|
||
//}
|
||
|
||
//if (pirson.Pirson < -0.7m && (pirson.Pirson < olddpirs) && pirson.PriceDiff < 0 && (pirson.TradesDiffRelative > 0.25m))
|
||
//{
|
||
// res[TradingEvent.OpenShort] = Constants.UppingCoefficient;
|
||
//}
|
||
|
||
|
||
|
||
if (olddpirs > 0.9m && pirson.Pirson <= 0.9m && pirson.TradesDiffRelative < -0.1m && pirson.TradesDiff <= 0)
|
||
{
|
||
res[TradingEvent.CloseLong] = Constants.PowerUppingCoefficient;
|
||
//await _tradeDataProvider.LogPrice(message, "diffs_pirson_diff_point_long_out", message.Price);
|
||
}
|
||
if (olddpirs < -0.8m && pirson.Pirson >= -0.8m && pirson.TradesDiffRelative < -0.1m && pirson.TradesDiff >= 0)
|
||
{
|
||
res[TradingEvent.CloseShort] = Constants.PowerUppingCoefficient;
|
||
// await _tradeDataProvider.LogPrice(message, "diffs_pirson_diff_point_short_out", message.Price);
|
||
}
|
||
|
||
//_dpirsonValues[message.Figi] = dpirson;
|
||
}
|
||
_pirsonValues[message.Figi] = pirson.Pirson;
|
||
return res.ToImmutableDictionary();
|
||
}
|
||
|
||
private async Task<PirsonCalculatingResult> CalcPirson(ITradeDataItem message)
|
||
{
|
||
var cacheSize = TimeSpan.FromSeconds(400);
|
||
var smallWindow = TimeSpan.FromSeconds(180);
|
||
var bigWindow = TimeSpan.FromSeconds(360);
|
||
var meanWindowForCottelation = TimeSpan.FromSeconds(360);
|
||
var currentTime = message.IsHistoricalData ? message.Time : DateTime.UtcNow;
|
||
|
||
var buys = await _tradeDataProvider.GetDataForTimeWindow(message.Figi, cacheSize, selector: (i) => i.Direction == 1);
|
||
var trades = await _tradeDataProvider.GetDataForTimeWindow(message.Figi, cacheSize);
|
||
if (trades.TryCalcTimeWindowsDiff(bigWindow, smallWindow, v => v.Count, false, out var tradesDiff, out var tradesDiffRelative)
|
||
&& buys.TryCalcTimeDiff(bigWindow, smallWindow, v => v.Price, true, out var pricesDiff))
|
||
{
|
||
await _tradeDataProvider.LogPrice(message, "privcesDiff", pricesDiff);
|
||
await _tradeDataProvider.LogPrice(message, "tradevolume_diff", tradesDiff);
|
||
await _tradeDataProvider.AddData(message.Figi, "5min_diff", new TradeDataItem()
|
||
{
|
||
Time = message.Time,
|
||
Value2 = tradesDiff,
|
||
Value = pricesDiff,
|
||
Figi = message.Figi,
|
||
Ticker = message.Ticker,
|
||
});
|
||
|
||
var diffs = await _tradeDataProvider.GetDataForTimeWindow(message.Figi, cacheSize, "5min_diff");
|
||
if (diffs.TryCalcPirsonCorrelation(meanWindowForCottelation, out var pirson))
|
||
{
|
||
var res = pirson;
|
||
await _tradeDataProvider.LogPrice(message, "diffs_pirson", (decimal)pirson);
|
||
//await _tradeDataProvider.AddData(message.Figi, "diffs_pirson", new Contracts.Declisions.Dtos.CachedValue()
|
||
//{
|
||
// Time = message.Time,
|
||
// Value = (decimal)pirson,
|
||
// Figi = message.Figi,
|
||
// Ticker = message.Ticker,
|
||
//});
|
||
return new PirsonCalculatingResult()
|
||
{
|
||
Pirson = res,
|
||
PriceDiff = pricesDiff,
|
||
TradesDiff = tradesDiff,
|
||
TradesDiffRelative = tradesDiffRelative,
|
||
Success = true,
|
||
};
|
||
}
|
||
}
|
||
return new PirsonCalculatingResult()
|
||
{
|
||
Success = false,
|
||
};
|
||
}
|
||
|
||
private async Task CloseMarginPositionsIfNeed(ITradeDataItem message)
|
||
{
|
||
var state = ExchangeScheduler.GetCurrentState(message.Time);
|
||
if (!message.IsHistoricalData && state == ExchangeState.ClearingTime)
|
||
{
|
||
if (_marginCloses.TryGetValue(string.Empty, out var time))
|
||
{
|
||
if (message.Time - time < TimeSpan.FromHours(2))
|
||
{
|
||
return;
|
||
}
|
||
}
|
||
var futuresFigis = _portfolioWrapper.Accounts.Values.SelectMany(v => v.Assets.Values.Where(a => a.Type == AssetType.Futures)).ToArray();
|
||
await ClosePositions(futuresFigis, message, false);
|
||
_marginCloses[string.Empty] = message.Time;
|
||
}
|
||
}
|
||
|
||
private async Task CalcSupportLevels(ITradeDataItem message, int leverage, decimal supportLevelWidth, int depthHours = 3)
|
||
{
|
||
if (_supportLevelsCalculationTimes.TryGetValue(message.Figi, out var lastTime))
|
||
{
|
||
if ((message.Time - lastTime).TotalMinutes < 10)
|
||
{
|
||
return;
|
||
}
|
||
}
|
||
|
||
var data = await _tradeDataProvider.GetDataForTimeWindow(message.Figi, TimeSpan.FromHours(depthHours));
|
||
if (data.Length > 0)
|
||
{
|
||
if (data[^1].Time - data[0].Time < TimeSpan.FromHours(0.5))
|
||
{
|
||
data = await _tradeDataProvider.GetDataForTimeWindow(message.Figi, TimeSpan.FromHours(depthHours + 12));
|
||
if (data[^1].Time - data[0].Time < TimeSpan.FromHours(0.5))
|
||
{
|
||
return;
|
||
}
|
||
}
|
||
|
||
var hist = Statistics.CalcHistogram(data);
|
||
var convs = Statistics.CalcConvolution(hist, leverage).ToList();
|
||
var orderedConvs = convs.OrderByDescending(c => c.Sum).Take(5).ToList();
|
||
orderedConvs = [.. orderedConvs.OrderBy(c => c.Value)];
|
||
var levelsForAdd = new List<SupportLevel>();
|
||
foreach (var c in orderedConvs)
|
||
{
|
||
var low = c.Value - supportLevelWidth;
|
||
var high = c.Value + supportLevelWidth;
|
||
|
||
if (levelsForAdd.Count > 0)
|
||
{
|
||
var last = levelsForAdd.Last();
|
||
if (last.HighValue < low)
|
||
{
|
||
levelsForAdd.Add(new SupportLevel()
|
||
{
|
||
HighValue = high,
|
||
LowValue = low,
|
||
Value = c.Value,
|
||
CalculatedAt = message.Time,
|
||
});
|
||
}
|
||
else if (last.HighValue >= low && last.HighValue < high)
|
||
{
|
||
levelsForAdd[^1] = new SupportLevel()
|
||
{
|
||
LowValue = last.LowValue,
|
||
HighValue = high,
|
||
Value = last.LowValue + (high - last.LowValue) / 2,
|
||
CalculatedAt = message.Time,
|
||
};
|
||
}
|
||
}
|
||
else
|
||
{
|
||
levelsForAdd.Add(new SupportLevel()
|
||
{
|
||
HighValue = high,
|
||
LowValue = low,
|
||
Value = c.Value,
|
||
CalculatedAt = message.Time,
|
||
});
|
||
}
|
||
}
|
||
var finalLevels = new SupportLevel[levelsForAdd.Count];
|
||
var i = 0;
|
||
foreach (var level in levelsForAdd)
|
||
{
|
||
|
||
DateTime? time = null;
|
||
foreach (var item in data)
|
||
{
|
||
if (item.Price >= level.LowValue && item.Price < level.HighValue)
|
||
{
|
||
time = item.Time;
|
||
}
|
||
}
|
||
finalLevels[i] = new SupportLevel()
|
||
{
|
||
HighValue = level.HighValue,
|
||
LowValue = level.LowValue,
|
||
Value = level.Value,
|
||
LastLevelTime = time,
|
||
CalculatedAt = message.Time,
|
||
};
|
||
i++;
|
||
}
|
||
|
||
SupportLevels[message.Figi] = finalLevels;
|
||
if (SupportLevelsHistory.TryGetValue(message.Figi, out var list))
|
||
{
|
||
list.AddLast(finalLevels);
|
||
while (list.Last != null && list.First != null
|
||
&& list.Last.Value.Length > 0 && list.First.Value.Length > 0
|
||
&& (list.Last.Value[0].CalculatedAt - list.First.Value[0].CalculatedAt).TotalHours > 3)
|
||
{
|
||
list.RemoveFirst();
|
||
}
|
||
}
|
||
else
|
||
{
|
||
SupportLevelsHistory[message.Figi] = new LinkedList<SupportLevel[]>();
|
||
SupportLevelsHistory[message.Figi].AddLast(finalLevels);
|
||
}
|
||
|
||
await _tradeDataProvider.LogPrice(message, "support_level_calc", message.Price);
|
||
}
|
||
|
||
_supportLevelsCalculationTimes[message.Figi] = message.Time;
|
||
}
|
||
|
||
private async Task ClosePositions(Asset[] assets, ITradeDataItem message, bool withProfitOnly = true)
|
||
{
|
||
var loggedDeclisions = 0;
|
||
var assetType = _tradeDataProvider.GetAssetTypeByFigi(message.Figi);
|
||
var assetsForClose = new List<Asset>();
|
||
var price = message.Price;
|
||
if (price == 0)
|
||
{
|
||
price = await _tradeDataProvider.GetLastPrice(message.Figi);
|
||
}
|
||
price = System.Math.Round(price, 2);
|
||
var messages = new List<string>();
|
||
foreach (var asset in assets)
|
||
{
|
||
Asset? assetForClose = null;
|
||
string? mess = null;
|
||
var profit = 0m;
|
||
if (withProfitOnly)
|
||
{
|
||
if (_tradeDataProvider.Orderbooks.TryGetValue(message.Figi, out var orderbook))
|
||
{
|
||
if (asset.Count < 0 && orderbook.Asks.Length > 0)
|
||
{
|
||
price = orderbook.Asks[0].Price;
|
||
}
|
||
else if (orderbook.Bids.Length > 0)
|
||
{
|
||
price = orderbook.Bids[0].Price;
|
||
}
|
||
}
|
||
|
||
profit = TradingCalculator.CaclProfit(asset.BoughtPrice, price,
|
||
GetComission(assetType), GetLeverage(message.Figi, asset.Count < 0), asset.Count < 0);
|
||
if (profit > 0)
|
||
{
|
||
profit = System.Math.Round(profit, 2);
|
||
assetForClose = asset;
|
||
if (loggedDeclisions == 0)
|
||
{
|
||
loggedDeclisions++;
|
||
await _tradeDataProvider.LogDeclision(asset.Count < 0 ? DeclisionTradeAction.CloseShortReal : DeclisionTradeAction.CloseLongReal, message, profit);
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
assetForClose = asset;
|
||
}
|
||
|
||
if (assetForClose != null)
|
||
{
|
||
var settings = _exchangeConfig.InstrumentsSettings.FirstOrDefault(l => l.Figi == message.Figi);
|
||
var closingResult = await _portfolioWrapper.Accounts[asset.AccountId].ClosePosition(message.Figi);
|
||
if (closingResult.Success)
|
||
{
|
||
var profitText = profit == 0 ? string.Empty : $", профит {profit}";
|
||
mess = $"Закрываю позицию {asset.Figi} ({(asset.Count > 0 ? "лонг" : "шорт")}) на счёте {_portfolioWrapper.Accounts[asset.AccountId].AccountName}. Количество {(long)asset.Count}, цена ~{closingResult.ExecutedPrice / (settings?.PointPriceRub ?? 1)}, комиссия {closingResult.Comission}" + profitText;
|
||
}
|
||
else
|
||
{
|
||
mess = $"Закрытие позиции прошло с ошибками.";
|
||
}
|
||
|
||
await _dataBus.Broadcast(new MessageForAdmin() { Text = mess });
|
||
}
|
||
}
|
||
}
|
||
|
||
private async Task OpenPositions(IManagedAccount[] accounts, ITradeDataItem message, PositionType positionType, decimal stopLossShift, decimal takeProfitShift, long count = 1)
|
||
{
|
||
var loggedDeclisions = 0;
|
||
var sign = positionType == PositionType.Long ? 1 : 1;
|
||
|
||
foreach (var acc in accounts)
|
||
{
|
||
if (TraderUtils.IsOperationAllowed(acc, message.Price, count, _exchangeConfig.AccountCashPartFutures, _exchangeConfig.AccountCashPart))
|
||
{
|
||
var settings = _exchangeConfig.InstrumentsSettings.FirstOrDefault(l => l.Figi == message.Figi);
|
||
takeProfitShift = takeProfitShift * 2;
|
||
takeProfitShift = System.Math.Round(takeProfitShift);
|
||
takeProfitShift = takeProfitShift / 2;
|
||
|
||
stopLossShift = stopLossShift * 2;
|
||
stopLossShift = System.Math.Round(stopLossShift);
|
||
stopLossShift = stopLossShift / 2;
|
||
var openingResult = await acc.OpenPosition(message.Figi, positionType, stopLossShift, takeProfitShift, count, settings?.PointPriceRub ?? 1);
|
||
await _dataBus.Broadcast(new MessageForAdmin()
|
||
{
|
||
Text = $"Открываю позицию {message.Figi} ({(positionType == PositionType.Long ? "лонг" : "шорт")}) " +
|
||
$"на счёте {acc.AccountName}. Количество {(positionType == PositionType.Long ? "" : "-")}{openingResult.Count}, " +
|
||
$"цена ~{System.Math.Round(openingResult.ExecutedPrice / (settings?.PointPriceRub ?? 1), 2)}. Комиссия:{System.Math.Round(openingResult.Comission, 2)}. Стоп лосс: {(positionType == PositionType.Long ? "-" : "+")}{stopLossShift}. " +
|
||
$"Тейк профит: {(positionType == PositionType.Long ? "+" : "-")}{takeProfitShift}"
|
||
});
|
||
}
|
||
|
||
if (loggedDeclisions == 0)
|
||
{
|
||
await _tradeDataProvider.LogDeclision(DeclisionTradeAction.OpenLongReal, message);
|
||
loggedDeclisions++;
|
||
}
|
||
}
|
||
}
|
||
|
||
private async Task ExecuteDeclisions(ImmutableDictionary<TradingEvent, decimal> result, ITradeDataItem message, Stops st, int accountsForOpening = 1)
|
||
{
|
||
var state = ExchangeScheduler.GetCurrentState(message.Time);
|
||
if (result[TradingEvent.OpenLong] > Constants.UppingCoefficient
|
||
&& state == ExchangeState.Open
|
||
)
|
||
{
|
||
var stops = st.GetStops(PositionType.Long);
|
||
|
||
if (!message.IsHistoricalData && BotModeSwitcher.CanPurchase())
|
||
{
|
||
var accounts = _portfolioWrapper.Accounts
|
||
.Where(a => !a.Value.Assets.ContainsKey(message.Figi))
|
||
.Take(accountsForOpening)
|
||
.Select(a => a.Value)
|
||
.ToArray();
|
||
await OpenPositions(accounts, message, PositionType.Long, stops.stopLoss, stops.takeProfit, 1);
|
||
}
|
||
var val = message.Price;
|
||
var valLow = message.Price - stops.stopLoss;
|
||
var valHigh = message.Price + stops.takeProfit;
|
||
await _tradeDataProvider.LogDeclision(DeclisionTradeAction.OpenLong, val, message.Time.AddMilliseconds(RandomNumberGenerator.GetInt32(-100, 100)), message);
|
||
//await _tradeDataProvider.LogDeclision(DeclisionTradeAction.ResetStopsLong, valHigh, message.Time.AddMilliseconds(-RandomNumberGenerator.GetInt32(300, 1000)), message);
|
||
await _tradeDataProvider.LogDeclision(DeclisionTradeAction.ResetStopsLong, valLow, message.Time.AddMilliseconds(RandomNumberGenerator.GetInt32(300, 1000)), message);
|
||
}
|
||
if (result[TradingEvent.OpenShort] > Constants.UppingCoefficient
|
||
&& state == ExchangeState.Open
|
||
)
|
||
{
|
||
var stops = st.GetStops(PositionType.Short);
|
||
if (!message.IsHistoricalData && BotModeSwitcher.CanPurchase())
|
||
{
|
||
var accounts = _portfolioWrapper.Accounts
|
||
.Where(a => !a.Value.Assets.ContainsKey(message.Figi))
|
||
.Take(1)
|
||
.Select(a => a.Value)
|
||
.ToArray();
|
||
await OpenPositions(accounts, message, PositionType.Short, stops.stopLoss, stops.takeProfit, 1);
|
||
}
|
||
var val = message.Price;
|
||
var valLow = message.Price - stops.takeProfit;
|
||
var valHigh = message.Price + stops.stopLoss;
|
||
await _tradeDataProvider.LogDeclision(DeclisionTradeAction.OpenShort, val, message.Time.AddMilliseconds(RandomNumberGenerator.GetInt32(-100, 100)), message);
|
||
//await _tradeDataProvider.LogDeclision(DeclisionTradeAction.ResetStopsShort, valLow, message.Time.AddMilliseconds(-RandomNumberGenerator.GetInt32(300, 1000)), message);
|
||
await _tradeDataProvider.LogDeclision(DeclisionTradeAction.ResetStopsShort, valHigh, message.Time.AddMilliseconds(RandomNumberGenerator.GetInt32(300, 1000)), message);
|
||
}
|
||
if (result[TradingEvent.CloseLong] >= Constants.UppingCoefficient)
|
||
{
|
||
if (!message.IsHistoricalData && BotModeSwitcher.CanSell())
|
||
{
|
||
var assetsForClose = _portfolioWrapper.Accounts
|
||
.SelectMany(a => a.Value.Assets.Values)
|
||
.Where(a => a.Figi == message.Figi && a.Count > 0)
|
||
.ToArray();
|
||
await ClosePositions(assetsForClose, message);
|
||
}
|
||
await _tradeDataProvider.LogDeclision(DeclisionTradeAction.CloseLong, message.Price, message.Time.AddMilliseconds(RandomNumberGenerator.GetInt32(-100, 100)), message);
|
||
|
||
}
|
||
|
||
if (result[TradingEvent.CloseShort] >= Constants.UppingCoefficient)
|
||
{
|
||
if (!message.IsHistoricalData && BotModeSwitcher.CanPurchase())
|
||
{
|
||
var assetsForClose = _portfolioWrapper.Accounts
|
||
.SelectMany(a => a.Value.Assets.Values)
|
||
.Where(a => a.Figi == message.Figi && a.Count < 0)
|
||
.ToArray();
|
||
await ClosePositions(assetsForClose, message);
|
||
}
|
||
await _tradeDataProvider.LogDeclision(DeclisionTradeAction.CloseShort, message.Price, message.Time.AddMilliseconds(RandomNumberGenerator.GetInt32(-100, 100)), message);
|
||
|
||
}
|
||
}
|
||
|
||
public Task StopAsync(CancellationToken cancellationToken)
|
||
{
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
private decimal GetComission(AssetType assetType)
|
||
{
|
||
if (assetType == AssetType.Common)
|
||
{
|
||
return _exchangeConfig.ShareComission;
|
||
}
|
||
else if (assetType == AssetType.Futures)
|
||
{
|
||
return _exchangeConfig.FutureComission;
|
||
}
|
||
else
|
||
{
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
private decimal GetLeverage(string figi, bool isShort)
|
||
{
|
||
var res = 1m;
|
||
var leverage = _exchangeConfig.InstrumentsSettings.FirstOrDefault(l => l.Figi == figi);
|
||
if (leverage != null)
|
||
{
|
||
res = isShort ? leverage.ShortLeverage : leverage.LongLeverage;
|
||
}
|
||
return res;
|
||
}
|
||
|
||
private Stops GetStops(ITradeDataItem message)
|
||
{
|
||
var additionalShift = message.Price * 0.0005m;
|
||
var longStopLossShift = message.Price * 0.0025m;
|
||
var longTakeProfitShift = message.Price * 0.02m;
|
||
var shortStopLossShift = message.Price * 0.0025m;
|
||
var shortTakeProfitShift = message.Price * 0.02m;
|
||
if (SupportLevels.TryGetValue(message.Figi, out var levels))
|
||
{
|
||
if (levels.Length > 0)
|
||
{
|
||
var levelsByTime = levels.Where(l => l.LastLevelTime.HasValue)
|
||
.OrderByDescending(l => l.LastLevelTime)
|
||
.ToArray();
|
||
if (message.Price >= levelsByTime[0].LowValue && message.Price < levelsByTime[0].HighValue)
|
||
{
|
||
longStopLossShift = message.Price - levelsByTime[0].LowValue + additionalShift;
|
||
shortStopLossShift = levelsByTime[0].HighValue - message.Price + additionalShift;
|
||
}
|
||
else
|
||
{
|
||
var levelsByDiffForLong = levels.Where(l => l.LastLevelTime.HasValue)
|
||
.OrderBy(l => l.Value - message.Price)
|
||
.ToArray();
|
||
|
||
var levelsByDiffForShort = levels.Where(l => l.LastLevelTime.HasValue)
|
||
.OrderByDescending(l => l.Value - message.Price)
|
||
.ToArray();
|
||
|
||
var nearestLevel = levelsByDiffForLong[0];
|
||
if (message.Price > nearestLevel.HighValue)
|
||
{
|
||
longStopLossShift = message.Price - nearestLevel.HighValue + additionalShift;
|
||
shortStopLossShift = message.Price - nearestLevel.HighValue + additionalShift;
|
||
}
|
||
|
||
nearestLevel = levelsByDiffForShort[0];
|
||
if (message.Price < nearestLevel.LowValue)
|
||
{
|
||
shortStopLossShift = nearestLevel.LowValue - message.Price + additionalShift;
|
||
longStopLossShift = nearestLevel.LowValue - message.Price + additionalShift;
|
||
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return new Stops(longStopLossShift, longTakeProfitShift, shortStopLossShift, shortTakeProfitShift);
|
||
}
|
||
|
||
private static ImmutableDictionary<TradingEvent, decimal> ProcessStops(Stops stops, decimal meanfullLevel)
|
||
{
|
||
var res = TraderUtils.GetInitDict(1);
|
||
if (stops.LongTakeProfitShift < meanfullLevel || stops.LongStopLossShift < meanfullLevel)
|
||
{
|
||
res[TradingEvent.OpenLong] = Constants.BlockingCoefficient;
|
||
}
|
||
if (stops.ShortTakeProfitShift < meanfullLevel || stops.ShortStopLossShift < meanfullLevel)
|
||
{
|
||
res[TradingEvent.OpenShort] = Constants.BlockingCoefficient;
|
||
}
|
||
|
||
return res.ToImmutableDictionary();
|
||
}
|
||
|
||
private async Task<ImmutableDictionary<TradingEvent, decimal>> ProcessSupportLevels(ITradeDataItem message)
|
||
{
|
||
var res = TraderUtils.GetInitDict(1);
|
||
if (SupportLevels.TryGetValue(message.Figi, out var levels))
|
||
{
|
||
foreach (var lev in levels)
|
||
{
|
||
if (message.Price >= lev.LowValue && message.Price < lev.HighValue)
|
||
{
|
||
await _tradeDataProvider.LogPrice(message, "support_level", message.Price);
|
||
}
|
||
}
|
||
|
||
if (levels.Length > 0)
|
||
{
|
||
var levelsByTime = levels.Where(l => l.LastLevelTime.HasValue)
|
||
.OrderByDescending(l => l.LastLevelTime)
|
||
.ToArray();
|
||
var levelByTime = levelsByTime[0];
|
||
if (message.Price >= levelByTime.LowValue && message.Price < levelByTime.HighValue)
|
||
{
|
||
var info = new AttachedInfo()
|
||
{
|
||
Key = Constants.PriceIsInSupportLevel,
|
||
Value1 = levelByTime.LowValue,
|
||
Value2 = levelByTime.HighValue,
|
||
};
|
||
message.SetAttachedInfo(info);
|
||
if (message.Price > levelByTime.Value)
|
||
{
|
||
res[TradingEvent.OpenLong] = Constants.BlockingCoefficient;
|
||
}
|
||
if (message.Price < levelByTime.Value)
|
||
{
|
||
res[TradingEvent.OpenShort] = Constants.BlockingCoefficient;
|
||
}
|
||
if (_oldItems.TryGetValue(message.Figi, out var old1))
|
||
{
|
||
var islevelUsed = false;
|
||
if (_usedSupportLevelsForClosing.TryGetValue(message.Figi, out var time))
|
||
{
|
||
if (time == levelByTime.CalculatedAt)
|
||
{
|
||
islevelUsed = true;
|
||
}
|
||
}
|
||
if (!islevelUsed)
|
||
{
|
||
if (old1.Price < levelByTime.LowValue || levelByTime.CalculatedAt == message.Time)
|
||
{
|
||
res[TradingEvent.CloseLong] = Constants.ForceExecuteCoefficient;
|
||
_usedSupportLevelsForClosing[message.Figi] = levelByTime.CalculatedAt;
|
||
}
|
||
if (old1.Price > levelByTime.HighValue || levelByTime.CalculatedAt == message.Time)
|
||
{
|
||
res[TradingEvent.CloseShort] = Constants.ForceExecuteCoefficient;
|
||
_usedSupportLevelsForClosing[message.Figi] = levelByTime.CalculatedAt;
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|
||
else
|
||
{
|
||
var info = new AttachedInfo()
|
||
{
|
||
Key = Constants.PriceIsNotInSupportLevel,
|
||
Value1 = levelByTime.LowValue,
|
||
Value2 = levelByTime.HighValue,
|
||
};
|
||
message.SetAttachedInfo(info);
|
||
if (_oldItems.TryGetValue(message.Figi, out var old))
|
||
{
|
||
if (old.Price >= levelByTime.LowValue && old.Price < levelByTime.HighValue)
|
||
{
|
||
var islevelUsed = false;
|
||
if (_usedSupportLevels.TryGetValue(message.Figi, out var time))
|
||
{
|
||
if (time == levelByTime.CalculatedAt)
|
||
{
|
||
islevelUsed = true;
|
||
}
|
||
}
|
||
if (!islevelUsed)
|
||
{
|
||
if (message.Price < levelByTime.LowValue)
|
||
{
|
||
res[TradingEvent.OpenShort] = Constants.UppingCoefficient;
|
||
res[TradingEvent.OpenLong] = Constants.UppingCoefficient;
|
||
_usedSupportLevels[message.Figi] = levelByTime.CalculatedAt;
|
||
}
|
||
else if (message.Price > levelByTime.HighValue)
|
||
{
|
||
res[TradingEvent.OpenShort] = Constants.UppingCoefficient;
|
||
res[TradingEvent.OpenLong] = Constants.UppingCoefficient;
|
||
_usedSupportLevels[message.Figi] = levelByTime.CalculatedAt;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|
||
return res.ToImmutableDictionary();
|
||
}
|
||
}
|
||
}
|