обновление торговой стратегии и механики сброса покупки активов
test / deploy_trader_prod (push) Successful in 12m44s Details

dev
vlad zverzhkhovskiy 2025-09-24 09:58:38 +03:00
parent 816ad0f677
commit ce58c7a22c
7 changed files with 152 additions and 62 deletions

View File

@ -3,11 +3,8 @@
public enum TradeCommandType
{
Unknown = 0,
MarketBuy = 1,
MarketSell = 101,
LimitBuy = 200,
LimitSell = 300,
CancelOrder = 400,
OpenLong = 1,
OpenShort = 101,
ClosePosition = 200,
}
}

View File

@ -8,8 +8,10 @@ namespace KLHZ.Trader.Core.Contracts.Messaging.Interfaces
public bool AddChannel(string key, Channel<IOrderbook> channel);
public bool AddChannel(string key, Channel<INewPrice> channel);
public bool AddChannel(string key, Channel<IMessage> channel);
public bool AddChannel(string key, Channel<ITradeCommand> channel);
public Task Broadcast(INewPrice newPriceMessage);
public Task Broadcast(IOrderbook orderbook);
public Task Broadcast(IMessage message);
public Task Broadcast(ITradeCommand message);
}
}

View File

@ -9,6 +9,7 @@ namespace KLHZ.Trader.Core.Common.Messaging.Services
{
private readonly ConcurrentDictionary<string, Channel<IOrderbook>> _orderbooksChannels = new();
private readonly ConcurrentDictionary<string, Channel<IMessage>> _messagesChannels = new();
private readonly ConcurrentDictionary<string, Channel<ITradeCommand>> _commandsChannel = new();
private readonly ConcurrentDictionary<string, Channel<INewPrice>> _priceChannels = new();
public bool AddChannel(string key, Channel<IMessage> channel)
@ -16,6 +17,11 @@ namespace KLHZ.Trader.Core.Common.Messaging.Services
return _messagesChannels.TryAdd(key, channel);
}
public bool AddChannel(string key, Channel<ITradeCommand> channel)
{
return _commandsChannel.TryAdd(key, channel);
}
public bool AddChannel(string key, Channel<INewPrice> channel)
{
return _priceChannels.TryAdd(key, channel);
@ -34,6 +40,13 @@ namespace KLHZ.Trader.Core.Common.Messaging.Services
}
}
public async Task Broadcast(ITradeCommand command)
{
foreach (var channel in _commandsChannel.Values)
{
await channel.Writer.WriteAsync(command);
}
}
public async Task Broadcast(IOrderbook orderbook)
{
foreach (var channel in _orderbooksChannels.Values)

View File

@ -1,6 +1,7 @@
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.Enums;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces;
using KLHZ.Trader.Core.Contracts.Messaging.Interfaces;
using KLHZ.Trader.Core.DataLayer.Entities.Declisions;
@ -20,6 +21,7 @@ using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Threading.Channels;
using Telegram.Bot.Types;
using Tinkoff.InvestApi;
using Asset = KLHZ.Trader.Core.Exchange.Models.AssetsAccounting.Asset;
using AssetType = KLHZ.Trader.Core.Exchange.Models.AssetsAccounting.AssetType;
@ -43,6 +45,8 @@ namespace KLHZ.Trader.Core.Exchange.Services
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;
@ -50,8 +54,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
private readonly string[] _tradingInstrumentsFigis = [];
private readonly Channel<INewPrice> _pricesChannel = Channel.CreateUnbounded<INewPrice>();
private readonly Channel<IOrderbook> _ordersbookChannel = Channel.CreateUnbounded<IOrderbook>();
private readonly Channel<ITradeCommand> _commands = Channel.CreateUnbounded<ITradeCommand>();
public Trader(
ILogger<Trader> logger,
IOptions<ExchangeConfig> options,
@ -83,8 +86,10 @@ namespace KLHZ.Trader.Core.Exchange.Services
public Task StartAsync(CancellationToken cancellationToken)
{
_dataBus.AddChannel(nameof(Trader), _pricesChannel);
_dataBus.AddChannel(nameof(Trader), _ordersbookChannel);
//_dataBus.AddChannel(nameof(Trader), _ordersbookChannel);
_dataBus.AddChannel(nameof(Trader), _commands);
_ = ProcessPrices();
_ = ProcessCommands();
return Task.CompletedTask;
}
@ -133,6 +138,43 @@ namespace KLHZ.Trader.Core.Exchange.Services
return position;
}
private async Task ProcessCommands()
{
while (await _commands.Reader.WaitToReadAsync())
{
var command = await _commands.Reader.ReadAsync();
try
{
if (command.CommandType == Contracts.Messaging.Dtos.Enums.TradeCommandType.OpenLong
|| command.CommandType == Contracts.Messaging.Dtos.Enums.TradeCommandType.OpenShort)
{
var fakeMessage = new NewPriceMessage() { Figi = command.Figi, Ticker = "", Count = command.Count, Direction = 1, IsHistoricalData = false, Time = DateTime.UtcNow, Value = command.RecomendPrice ?? 0m };
var positionType = command.CommandType == TradeCommandType.OpenLong ? PositionType.Long : PositionType.Short;
var stops = GetStops(fakeMessage, positionType);
var accounts = _portfolioWrapper.Accounts
.Where(a => !a.Value.Assets.ContainsKey(command.Figi))
.Take(1)
.Select(a => a.Value)
.ToArray();
await OpenPositions(accounts, fakeMessage, positionType, stops.stopLoss, stops.takeProfit, System.Math.Abs(command.Count));
}
else
{
var fakeMessage = new NewPriceMessage() { Figi = command.Figi, Ticker = "", Count = command.Count, Direction = 1, IsHistoricalData = false, Time = DateTime.UtcNow, Value = 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)
{
}
}
}
private async Task ProcessPrices()
{
var pricesCache1 = new Dictionary<string, List<INewPrice>>();
@ -141,7 +183,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
while (await _pricesChannel.Reader.WaitToReadAsync())
{
var message = await _pricesChannel.Reader.ReadAsync();
var changeMods = GetInitDict(1);
var changeMods = GetInitDict(0);
try
{
if (message.IsHistoricalData)
@ -241,7 +283,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
}
}
#endregion
if (_tradingInstrumentsFigis.Contains(message.Figi) && message.Figi == "FUTIMOEXF000")
if (_tradingInstrumentsFigis.Contains(message.Figi) && message.Figi == "FUTIMOEXF000" && message.Direction==1)
{
var currentTime = message.IsHistoricalData ? message.Time : DateTime.UtcNow;
try
@ -355,7 +397,7 @@ INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullSt
res[TradingEvent.UptrendEnd] = Constants.PowerLowingCoefficient;
if ((resultMoveAvFull.events & TradingEvent.UptrendStart) == TradingEvent.UptrendStart)
{
res[TradingEvent.UptrendStart] = 2*initValue;
res[TradingEvent.UptrendStart] = initValue;
res[TradingEvent.DowntrendEnd] = initValue;
}
if ((resultMoveAvFull.events & TradingEvent.UptrendEnd) == TradingEvent.UptrendEnd)
@ -397,7 +439,12 @@ INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullSt
var loggedDeclisions = 0;
var assetType = _tradeDataProvider.GetAssetTypeByFigi(message.Figi);
var assetsForClose = new List<Asset>();
var price = message.Value;
if (price == 0)
{
price = await _tradeDataProvider.GetLastPrice(message.Figi);
}
var messages = new List<string>();
foreach (var asset in assets)
{
if (withProfitOnly)
@ -406,16 +453,13 @@ INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullSt
if (assetType == AssetType.Futures)
{
profit = TradingCalculator.CaclProfit(asset.BoughtPrice, message.Value,
profit = TradingCalculator.CaclProfit(asset.BoughtPrice, price,
GetComission(assetType), GetLeverage(message.Figi, asset.Count < 0), asset.Count < 0);
}
if (profit > 0)
{
assetsForClose.Add(asset);
await _dataBus.Broadcast(new MessageForAdmin()
{
Text = $"Закрываю позицию {asset.Figi} ({(asset.Count > 0 ? "лонг" : "шорт")}) на счёте {_portfolioWrapper.Accounts[asset.AccountId].AccountName}. Количество {(long)asset.Count}, цена ~{message.Value}, профит {profit}"
});
messages.Add($"Закрываю позицию {asset.Figi} ({(asset.Count > 0 ? "лонг" : "шорт")}) на счёте {_portfolioWrapper.Accounts[asset.AccountId].AccountName}. Количество {(long)asset.Count}, цена ~{price}, профит {profit}");
if (loggedDeclisions == 0)
{
loggedDeclisions++;
@ -425,12 +469,17 @@ INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullSt
}
else
{
messages.Add($"Закрываю позицию {asset.Figi} ({(asset.Count > 0 ? "лонг" : "шорт")}) на счёте {_portfolioWrapper.Accounts[asset.AccountId].AccountName}. Количество {(long)asset.Count}, цена ~{price}");
assetsForClose.Add(asset);
}
}
var tasks = assetsForClose.Select(asset => _portfolioWrapper.Accounts[asset.AccountId].ClosePosition(message.Figi));
await Task.WhenAll(tasks);
foreach (var mess in messages)
{
await _dataBus.Broadcast(new MessageForAdmin() { Text = mess });
}
}
private async Task OpenPositions(IManagedAccount[] accounts, INewPrice message, PositionType positionType, decimal stopLossShift, decimal takeProfitShift, long count = 1)
@ -469,7 +518,7 @@ INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullSt
return;
}
//var resTask1 = GetWindowAverageStartData(data, 30, 180, message, windowMaxSize, -2m, 2m,3);
var resTask1 = GetWindowAverageStartData(data, 30, 180, message, windowMaxSize, 0m, 0.5m, Constants.PowerUppingCoefficient);
var resTask1 = GetWindowAverageStartData(data, 30, 180, message, windowMaxSize, 0m, 0.5m, 2*Constants.PowerUppingCoefficient);
//var resTask3 = GetWindowAverageStartData(data, 30, 180, message, windowMaxSize, 0, 0,0.7m);
var getFFTModsTask = GetFFTMods(message);
//var getAreasModsTask = GetAreasMods(data, message);
@ -487,11 +536,11 @@ INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullSt
//result = MergeResults(result, resTask2.Result.ToImmutableDictionary());
//result = MergeResults(result, resTask3.Result.ToImmutableDictionary());
//result = MergeResults(result, changeModeData);
result = MergeResults(result, getFFTModsTask.Result);
result = MergeResultsMax(result, changeModeData);
result = MergeResultsMult(result, getFFTModsTask.Result);
//result = MergeResults(result, getAreasModsTask.Result);
result = MergeResults(result, getSellsDiffsModsTask.Result);
result = MergeResults(result, getTradingModeModsTask.Result);
result = MergeResultsMult(result, getSellsDiffsModsTask.Result);
result = MergeResultsMult(result, getTradingModeModsTask.Result);
if (result[TradingEvent.UptrendStart] > Constants.UppingCoefficient
&& !LongOpeningStops.ContainsKey(message.Figi)
@ -684,11 +733,11 @@ INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullSt
{
res = TradingMode.SlowDropping;
}
if (largeDataRes > 5 && smallDataRes > 0)
if ((largeDataRes > 5 && smallDataRes > 0)||smallDataRes>7)
{
res = TradingMode.Growing;
}
if (largeDataRes < -5 && smallDataRes < 0)
if ((largeDataRes < -5 && smallDataRes < 0)|| smallDataRes<-7)
{
res = TradingMode.Dropping;
}
@ -706,12 +755,11 @@ INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullSt
private (decimal stopLoss, decimal takeProfit) GetStops(INewPrice message, PositionType type)
{
var mode = TradingModes[message.Figi];
decimal stopLossShift = 4;
decimal takeProfitShift = 5;
decimal stopLossShift = 15;
decimal takeProfitShift = 6;
if (mode == TradingMode.Growing && type == PositionType.Long)
{
stopLossShift = 6;
takeProfitShift = 9;
takeProfitShift = 15;
}
if (mode == TradingMode.Growing && type == PositionType.Short)
{
@ -720,28 +768,24 @@ INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullSt
}
if (mode == TradingMode.Stable && type == PositionType.Long)
{
stopLossShift = 4;
takeProfitShift = 2.5m;
}
if (mode == TradingMode.Stable && type == PositionType.Short)
{
stopLossShift = 4;
takeProfitShift = 2.5m;
stopLossShift = 5;
}
if (mode == TradingMode.SlowDropping && type == PositionType.Short)
{
stopLossShift = 6;
takeProfitShift = 2.5m;
takeProfitShift = 4m;
}
if (mode == TradingMode.SlowDropping && type == PositionType.Long)
{
stopLossShift = 4;
takeProfitShift = 1.5m;
}
if (mode == TradingMode.Dropping && type == PositionType.Short)
{
stopLossShift = 6;
takeProfitShift = 8;
takeProfitShift = 15;
}
if (mode == TradingMode.Dropping && type == PositionType.Long)
{
@ -855,29 +899,29 @@ INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullSt
var mode = TradingModes[message.Figi];
if (mode == TradingMode.None)
{
res[TradingEvent.UptrendEnd] = Constants.UppingCoefficient;
res[TradingEvent.UptrendStart] = 1;
res[TradingEvent.DowntrendStart] = Constants.LowingCoefficient;
res[TradingEvent.DowntrendEnd] = Constants.UppingCoefficient;
//res[TradingEvent.UptrendEnd] = Constants.UppingCoefficient;
//res[TradingEvent.UptrendStart] = 1;
//res[TradingEvent.DowntrendStart] = Constants.LowingCoefficient;
//res[TradingEvent.DowntrendEnd] = Constants.UppingCoefficient;
}
if (mode == TradingMode.Growing)
{
res[TradingEvent.UptrendEnd] = Constants.PowerLowingCoefficient;
res[TradingEvent.UptrendStart] = 10;
res[TradingEvent.DowntrendStart] = Constants.BlockingCoefficient;
res[TradingEvent.DowntrendEnd] = Constants.BlockingCoefficient;
res[TradingEvent.DowntrendEnd] = Constants.PowerUppingCoefficient;
}
if (mode == TradingMode.Stable)
{
res[TradingEvent.UptrendEnd] = 1;
//res[TradingEvent.UptrendStart] = Constants.UppingCoefficient;
//res[TradingEvent.DowntrendEnd] = Constants.UppingCoefficient;
res[TradingEvent.DowntrendStart] = Constants.BlockingCoefficient;
// res[TradingEvent.DowntrendStart] = Constants.BlockingCoefficient;
}
if (mode == TradingMode.SlowDropping)
{
res[TradingEvent.UptrendEnd] = Constants.PowerUppingCoefficient;
res[TradingEvent.UptrendStart] = Constants.PowerLowingCoefficient;
//res[TradingEvent.UptrendEnd] = Constants.PowerUppingCoefficient;
//res[TradingEvent.UptrendStart] = Constants.PowerLowingCoefficient;
//res[TradingEvent.DowntrendStart] = Constants.UppingCoefficient;
//res[TradingEvent.DowntrendEnd] = Constants.UppingCoefficient;
}
@ -918,7 +962,7 @@ INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullSt
return values.ToDictionary(v => v, v => initValue);
}
private static Dictionary<TradingEvent, decimal> MergeResults(Dictionary<TradingEvent, decimal> res, ImmutableDictionary<TradingEvent, decimal> data)
private static Dictionary<TradingEvent, decimal> MergeResultsMult(Dictionary<TradingEvent, decimal> res, ImmutableDictionary<TradingEvent, decimal> data)
{
foreach (var k in res.Keys)
{
@ -928,5 +972,16 @@ INewPrice message, int windowMaxSize, decimal uptrendStartingDetectionMeanfullSt
}
return res;
}
private static Dictionary<TradingEvent, decimal> MergeResultsMax(Dictionary<TradingEvent, decimal> res, ImmutableDictionary<TradingEvent, decimal> data)
{
foreach (var k in res.Keys)
{
var valRes = res[k];
var valData = data[k];
res[k] = System.Math.Max(valRes, valData);
}
return res;
}
}
}

View File

@ -35,6 +35,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
private readonly bool _isDataRecievingAllowed = false;
private readonly Channel<object> _forSave = Channel.CreateUnbounded<object>();
private readonly SemaphoreSlim _initSemaphore = new SemaphoreSlim(1, 1);
public TraderDataProvider(InvestApiClient investApiClient, IOptions<ExchangeConfig> options, IDbContextFactory<TraderDbContext> dbContextFactory, ILogger<TraderDataProvider> logger)
@ -61,6 +62,16 @@ namespace KLHZ.Trader.Core.Exchange.Services
return ValueTask.CompletedTask;
}
public async ValueTask<decimal> GetLastPrice(string figi)
{
var res = 0m;
if (_historyCash.TryGetValue(figi, out var unit))
{
res = (await unit.GetLastValues()).price;
}
return res;
}
public async ValueTask<(DateTime[] timestamps, decimal[] prices, bool isFullIntervalExists)> GetData(string figi, TimeSpan timeSpan)
{
if (_historyCash.TryGetValue(figi, out var unit))

View File

@ -1,4 +1,5 @@
using KLHZ.Trader.Core.Common;
using KLHZ.Trader.Core.Contracts.Messaging.Dtos;
using KLHZ.Trader.Core.Contracts.Messaging.Interfaces;
using KLHZ.Trader.Core.Exchange.Services;
using Microsoft.Extensions.Logging;
@ -78,30 +79,41 @@ namespace KLHZ.Trader.Core.TG.Services
case "скинуть IMOEXF":
case "сбросить IMOEXF":
{
var assets = _portfolioWrapper.Accounts
.SelectMany(a => a.Value.Assets)
.Select(a => a.Value)
.Where(a => a.Figi == "FUTIMOEXF000");
foreach (var asset in assets)
var command = new TradeCommand()
{
await _portfolioWrapper.Accounts[asset.AccountId].ClosePosition("FUTIMOEXF000");
}
AccountId = "",
Figi = "FUTIMOEXF000",
CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.ClosePosition,
EnableMargin = true,
Count = 1
};
await _eventBus.Broadcast(command);
break;
}
case "лонг IMOEXF":
{
var acc = _portfolioWrapper.Accounts.Values.FirstOrDefault(a => !a.Assets.ContainsKey("FUTIMOEXF000"));
if (acc != null)
await acc.OpenPosition("FUTIMOEXF000", Exchange.Models.AssetsAccounting.PositionType.Long, 4, 6, 1);
var command = new TradeCommand()
{
AccountId = "",
Figi = "FUTIMOEXF000",
CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.OpenLong,
EnableMargin = true,
Count = 1
};
await _eventBus.Broadcast(command);
break;
}
case "шорт IMOEXF":
{
var acc = _portfolioWrapper.Accounts.Values.FirstOrDefault(a => !a.Assets.ContainsKey("FUTIMOEXF000"));
if (acc != null)
await acc.OpenPosition("FUTIMOEXF000", Exchange.Models.AssetsAccounting.PositionType.Short, 4, 6, 1);
var command = new TradeCommand()
{
AccountId = "",
Figi = "FUTIMOEXF000",
CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.OpenShort,
EnableMargin = true,
Count = 1
};
await _eventBus.Broadcast(command);
break;
}
case "stops":

View File

@ -34,8 +34,8 @@ namespace KLHZ.Trader.Service.Controllers
//var figi1 = "BBG004730N88";
var figi2 = "BBG004730N88";
//var figi2 = "FUTIMOEXF000";
var time1 = DateTime.UtcNow.AddDays(-shift ?? -7).Date;
//var time1 = new DateTime(2025, 9, 4, 14, 0, 0, DateTimeKind.Utc);
//var time1 = DateTime.UtcNow.AddDays(-shift ?? -7).Date;
var time1 = new DateTime(2025, 9, 23, 17, 0, 0, DateTimeKind.Utc);
//var time2 = DateTime.UtcNow.AddMinutes(18);
using var context1 = await _dbContextFactory.CreateDbContextAsync();
context1.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;