обновление стратегии
test / deploy_trader_prod (push) Successful in 2m14s Details

dev
vlad zverzhkhovskiy 2025-10-15 16:19:58 +03:00
parent f7cecbe44a
commit ecd9a70e2e
10 changed files with 310 additions and 80 deletions

View File

@ -1,4 +1,5 @@
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces;
using System.ComponentModel.DataAnnotations.Schema;
namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos
{
@ -13,5 +14,27 @@ namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos
public int Direction { get; set; }
public decimal Value { get; init; }
public decimal Value2 { get; init; }
[NotMapped]
public AttachedInfo? AttachedInfo
{
get
{
lock (_locker)
{
return _attachedInfo;
}
}
}
public void SetAttachedInfo(AttachedInfo? attachedInfo)
{
lock (_locker)
{
_attachedInfo = attachedInfo;
}
}
private AttachedInfo? _attachedInfo;
private readonly object _locker = new();
}
}

View File

@ -1,4 +1,5 @@
using KLHZ.Trader.Core.Contracts.Declisions.Dtos.Enums;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces;
using MathNet.Numerics;
namespace KLHZ.Trader.Core.Math.Declisions.Utils
@ -116,5 +117,34 @@ namespace KLHZ.Trader.Core.Math.Declisions.Utils
return true;
}
}
public static bool TryCalcTrendDiff(ITradeDataItem[] data, out decimal diff)
{
diff = 0;
if (data.Length <= 1)
{
return false;
}
else
{
var startTime = data[0].Time;
var x = new double[data.Length];
for (int i = 0; i < data.Length; i++)
{
x[i] = (data[i].Time - startTime).TotalSeconds;
}
if (x.Min() == x.Max())
{
return false;
}
var line = Fit.Line(x.ToArray(), data.Select(d => (double)d.Price).ToArray());
var p1 = line.A + line.B * 0;
var p2 = line.A + line.B * (data[data.Length - 1].Time - data[0].Time).TotalSeconds;
diff = (decimal)(p2 - p1);
return true;
}
}
}
}

View File

@ -1,4 +1,4 @@
using KLHZ.Trader.Core.Exchange.Models.Trading;
using KLHZ.Trader.Core.Exchange.Models.Trading.Enums;
using KLHZ.Trader.Core.Exchange.Utils;
namespace KLHZ.Trader.Core.Tests

View File

@ -7,8 +7,6 @@ namespace KLHZ.Trader.Core.DataLayer.Entities.Prices
[Table("price_changes")]
public class PriceChange : ITradeDataItem
{
private readonly object _locker = new();
[Column("id")]
public long Id { get; set; }
@ -59,5 +57,6 @@ namespace KLHZ.Trader.Core.DataLayer.Entities.Prices
}
private AttachedInfo? _attachedInfo;
private readonly object _locker = new();
}
}

View File

@ -2,24 +2,14 @@
{
internal static class Constants
{
internal const string _1minCacheKey = "1min";
internal const string _15minSellCacheKey = "5min_sell";
internal const string _5minSellCacheKey = "5min_sell";
internal const string _5minBuyCacheKey = "5min_buy";
internal const string _15minBuyCacheKey = "5min_buy";
internal const string _1minSellCacheKey = "1min_sell";
internal const string _1minBuyCacheKey = "1min_buy";
internal const string BigWindowCrossingAverageProcessor = "Trader_big";
internal const string SmallWindowCrossingAverageProcessor = "Trader_small";
internal const string AreasRelationProcessor = "balancescalc30min";
internal readonly static TimeSpan AreasRelationWindow = TimeSpan.FromMinutes(15);
internal const decimal ForceExecuteCoefficient = 500000m;
internal const decimal PowerUppingCoefficient = 1.69m;
internal const decimal UppingCoefficient = 1.3m;
internal const decimal LowingCoefficient = .76m;
internal const decimal PowerLowingCoefficient = .59m;
internal const decimal BlockingCoefficient = 0.01m;
internal const string PriceIsInSupportLevel = "PriceIsInSupportLevel";
internal const string PriceIsNotInSupportLevel = "PriceIsNotInSupportLevel";
}
}

View File

@ -1,4 +1,4 @@
namespace KLHZ.Trader.Core.Exchange.Models.Trading
namespace KLHZ.Trader.Core.Exchange.Models.Trading.Enums
{
internal enum ExchangeState
{

View File

@ -0,0 +1,14 @@
namespace KLHZ.Trader.Core.Exchange.Models.Trading.Enums
{
[Flags]
internal enum TradingMode
{
None = 0,
Growing = 1,
Falling = 2,
InSupportLevel = 4,
TryingGrowFromSupportLevel = 8,
TryingFallFromSupportLevel = 16,
Bezpont = 32,
}
}

View File

@ -10,6 +10,7 @@ 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;
@ -37,6 +38,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
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();
@ -169,52 +171,173 @@ namespace KLHZ.Trader.Core.Exchange.Services
{
if (message.Figi == "FUTIMOEXF000")
{
await CalcSupportLevels(message, 2, 3);
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)
{
var declisionsSupportLevels = await ProcessSupportLevels(message);
//var pirson = await CalcPirson(message);
//var mavRes = await CalcTimeWindowAverageValue(message);
}
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 declisionPirson = ProcessPirson(pirson, message);
//var declisionsStops = ProcessStops(stops, 2m);
var res = TraderUtils.MergeResultsMult(declisionPirson, processSupportLevelsRes);
res = TraderUtils.MergeResultsMult(res, declisionsStops);
res = TraderUtils.MergeResultsMult(res, tradingModeResult);
res = TraderUtils.MergeResultsMax(res, mavRes);
//var res = TraderUtils.MergeResultsMult(declisionPirson, declisionsSupportLevels);
//res = TraderUtils.MergeResultsMult(res, declisionsStops);
//res = TraderUtils.MergeResultsMax(res, mavRes);
//await ExecuteDeclisions(res.ToImmutableDictionary(), message, stops, 1);
await ExecuteDeclisions(res.ToImmutableDictionary(), message, stops, 1);
}
_oldItems[message.Figi] = message;
}
}
private async Task CheckTradingMode()
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<ImmutableDictionary<TradingEvent, decimal>> CalcTimeWindowAverageValue(ITradeDataItem message)
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);
//var re = MovingAverage.CheckByWindowAverageMean2(data, 100, 15, 300, -4m, 4m);
if (closings.smallWindowAv != 0)
{
await _tradeDataProvider.LogPrice(message, "maw_small", closings.smallWindowAv);
await _tradeDataProvider.LogPrice(message, "maw_big", closings.bigWindowAv);
}
//if ((re.events & TradingEvent.OpenShort) == TradingEvent.OpenShort)
//{
// res[TradingEvent.OpenShort] = Constants.PowerUppingCoefficient;
//}
//if ((re.events & TradingEvent.OpenLong) == TradingEvent.OpenLong)
//{
// res[TradingEvent.OpenLong] = Constants.PowerUppingCoefficient;
//}
if ((closings.events & TradingEvent.CloseShort) == TradingEvent.CloseShort)
{
res[TradingEvent.CloseShort] = Constants.PowerUppingCoefficient;
@ -227,6 +350,23 @@ namespace KLHZ.Trader.Core.Exchange.Services
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);
@ -244,6 +384,28 @@ namespace KLHZ.Trader.Core.Exchange.Services
}
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;
@ -318,8 +480,16 @@ namespace KLHZ.Trader.Core.Exchange.Services
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;
}
}
@ -492,7 +662,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
var closingResult = await _portfolioWrapper.Accounts[asset.AccountId].ClosePosition(message.Figi);
if (closingResult.Success)
{
var profitText = profit == 0 ? string.Empty : ", профит {profit}";
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
@ -543,7 +713,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
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
if (result[TradingEvent.OpenLong] > Constants.UppingCoefficient
&& state == ExchangeState.Open
)
{
@ -565,7 +735,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
//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
if (result[TradingEvent.OpenShort] > Constants.UppingCoefficient
&& state == ExchangeState.Open
)
{
@ -680,12 +850,15 @@ namespace KLHZ.Trader.Core.Exchange.Services
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;
}
}
}
@ -729,6 +902,13 @@ namespace KLHZ.Trader.Core.Exchange.Services
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;
@ -763,35 +943,46 @@ namespace KLHZ.Trader.Core.Exchange.Services
}
}
else if (_oldItems.TryGetValue(message.Figi, out var old))
else
{
if (old.Price >= levelByTime.LowValue && old.Price < levelByTime.HighValue)
var info = new AttachedInfo()
{
var islevelUsed = false;
if (_usedSupportLevels.TryGetValue(message.Figi, out var time))
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)
{
if (time == levelByTime.CalculatedAt)
var islevelUsed = false;
if (_usedSupportLevels.TryGetValue(message.Figi, out var time))
{
islevelUsed = true;
if (time == levelByTime.CalculatedAt)
{
islevelUsed = true;
}
}
}
if (!islevelUsed)
{
if (message.Price < levelByTime.LowValue)
if (!islevelUsed)
{
res[TradingEvent.OpenShort] = Constants.ForceExecuteCoefficient;
res[TradingEvent.OpenLong] = Constants.ForceExecuteCoefficient;
_usedSupportLevels[message.Figi] = levelByTime.CalculatedAt;
}
else if (message.Price > levelByTime.HighValue)
{
res[TradingEvent.OpenShort] = Constants.ForceExecuteCoefficient;
res[TradingEvent.OpenLong] = Constants.ForceExecuteCoefficient;
_usedSupportLevels[message.Figi] = levelByTime.CalculatedAt;
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();

View File

@ -89,23 +89,6 @@ namespace KLHZ.Trader.Core.Exchange.Services
return ValueTask.FromResult(Array.Empty<ITradeDataItem>());
}
public ValueTask<ITradeDataItem[]> GetDataFrom20SecondsWindowCache2(string figi, string key)
{
return GetDataForTimeWindow(figi, TimeSpan.FromSeconds(20), key);
}
public ValueTask<ITradeDataItem[]> GetDataFrom1MinuteWindowCache2(string figi, string key)
{
return GetDataForTimeWindow(figi, TimeSpan.FromSeconds(60), key);
}
public ValueTask<ITradeDataItem[]> GetDataFrom5MinuteWindowCache2(string figi, string key)
{
return GetDataForTimeWindow(figi, TimeSpan.FromMinutes(5), key);
}
public ValueTask<ITradeDataItem[]> GetDataFrom15MinuteWindowCache2(string figi, string key)
{
return GetDataForTimeWindow(figi, TimeSpan.FromMinutes(15), key);
}
public async ValueTask AddOrderbook(IOrderbook orderbook)
{
if (!_historyCash3.TryGetValue(orderbook.Figi, out var unit))

View File

@ -1,4 +1,4 @@
using KLHZ.Trader.Core.Exchange.Models.Trading;
using KLHZ.Trader.Core.Exchange.Models.Trading.Enums;
namespace KLHZ.Trader.Core.Exchange.Utils
{