Добавил блокировку активов
test / deploy_trader_prod (push) Successful in 2m2s Details

dev
vlad zverzhkhovskiy 2025-09-17 11:01:27 +03:00
parent bcc084c49c
commit 0f5284f472
10 changed files with 168 additions and 289 deletions

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces
{
public interface ILockableObject
{
public Task<bool> Lock(TimeSpan duration);
public void Unlock();
}
}

View File

@ -11,5 +11,6 @@ namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces
public long Count { get; }
public string AccountId { get; }
public bool EnableMargin { get; }
public ILockableObject? ExchangeObject { get; }
}
}

View File

@ -12,5 +12,6 @@ namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos
public long Count { get; init; }
public required string AccountId { get; init; }
public bool EnableMargin { get; init; } = true;
public ILockableObject? ExchangeObject { get; init; }
}
}

View File

@ -15,7 +15,7 @@ namespace KLHZ.Trader.Core.Math.Declisions.Utils
{
return ValueAmplitudePosition.UpperThen30Decil;
}
else if (value < fftData.Mediana && System.Math.Sign(value2)>=0)
else if (value < fftData.Mediana && System.Math.Sign(value2) >= 0)
{
return ValueAmplitudePosition.LowerThenMediana;
}

View File

@ -0,0 +1,26 @@
using KLHZ.Trader.Core.Exchange.Models.AssetsAccounting;
namespace KLHZ.Trader.Core.Tests
{
public class AssetTests
{
[Test]
public void Test1()
{
var asset = new Asset() { AccountId = "", Figi = "", Ticker = "" };
var dur = TimeSpan.FromSeconds(5);
Assert.IsTrue(asset.Lock(dur).Result);
Assert.IsFalse(asset.Lock(dur).Result);
}
[Test]
public void Test2()
{
var asset = new Asset() { AccountId = "", Figi = "", Ticker = "" };
var dur = TimeSpan.FromSeconds(5);
Assert.IsTrue(asset.Lock(dur).Result);
Task.Delay(dur + dur).Wait();
Assert.IsTrue(asset.Lock(dur).Result);
}
}
}

View File

@ -1,6 +1,6 @@
namespace KLHZ.Trader.Core.Exchange.Models.AssetsAccounting
{
public class Asset
public class Asset : LockableExchangeObject
{
public long? TradeId { get; init; }
public decimal BlockedItems { get; init; }

View File

@ -0,0 +1,35 @@
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces;
namespace KLHZ.Trader.Core.Exchange.Models.AssetsAccounting
{
public abstract class LockableExchangeObject : ILockableObject
{
private readonly SemaphoreSlim _sem = new SemaphoreSlim(1, 1);
public Task<bool> Lock(TimeSpan duration)
{
var lockerTask = _sem.WaitAsync(0);
_ = lockerTask.ContinueWith(async (t) =>
{
if (t.Result)
{
await Task.Delay(duration);
_sem.Release();
}
});
return lockerTask;
}
public void Unlock()
{
try
{
_sem.Release();
}
catch
{
}
}
}
}

View File

@ -2,7 +2,7 @@
namespace KLHZ.Trader.Core.Exchange.Models.AssetsAccounting
{
public class ManagedAccount
public class ManagedAccount : LockableExchangeObject
{
public readonly string AccountId;
private readonly object _locker = new();
@ -42,217 +42,5 @@ namespace KLHZ.Trader.Core.Exchange.Models.AssetsAccounting
{
AccountId = accountId;
}
// private async Task ProcessCommands()
// {
// while (await _channel.Reader.WaitToReadAsync())
// {
// var command = await _channel.Reader.ReadAsync();
// try
// {
// await ProcessMarketCommand(command);
// }
// catch (Exception ex)
// {
// _logger.LogError(ex, "Ошибка при обработке команды.");
// }
// }
// }
// internal async Task SyncPortfolio()
// {
// try
// {
// //await _semaphoreSlim.WaitAsync();
// var portfolio = await _investApiClient.Operations.GetPortfolioAsync(new PortfolioRequest()
// {
// AccountId = AccountId,
// });
// var oldAssets = Assets.Keys.ToHashSet();
// using var context = await _dbContextFactory.CreateDbContextAsync();
// context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
// var trades = await context.Trades
// .Where(t => t.AccountId == AccountId && t.ArchiveStatus == 0)
// .ToListAsync();
// foreach (var position in portfolio.Positions)
// {
// decimal price = 0;
// var trade = trades.FirstOrDefault(t => t.Figi == position.Figi);
// if (trade != null)
// {
// trades.Remove(trade);
// price = trade.Price;
// }
// else
// {
// price = position.AveragePositionPrice;
// }
//#pragma warning disable CS0612 // Тип или член устарел
// var asset = new Models.Assets.Asset()
// {
// TradeId = trade?.Id,
// AccountId = AccountId,
// Figi = position.Figi,
// Ticker = position.Ticker,
// BoughtAt = trade?.BoughtAt ?? DateTime.UtcNow,
// BoughtPrice = price,
// Type = position.InstrumentType.ParseInstrumentType(),
// Position = position.Quantity > 0 ? PositionType.Long : PositionType.Short,
// BlockedItems = position.BlockedLots,
// Count = position.Quantity,
// CountLots = position.QuantityLots,
// };
//#pragma warning restore CS0612 // Тип или член устарел
// Assets.AddOrUpdate(asset.Figi, asset, (k, v) => asset);
// oldAssets.Remove(asset.Figi);
// }
// Total = portfolio.TotalAmountPortfolio;
// Balance = portfolio.TotalAmountCurrencies;
// foreach (var asset in oldAssets)
// {
// Assets.TryRemove(asset, out _);
// }
// var ids = trades.Select(t => t.Id).ToArray();
// await context.Trades
// .Where(t => ids.Contains(t.Id))
// .ExecuteUpdateAsync(t => t.SetProperty(tr => tr.ArchiveStatus, 1));
// }
// catch (Exception ex)
// {
// _logger.LogError(ex, "Ошибка при синхранизации портфеля счёта {accountId}", AccountId);
// }
// finally
// {
// //_semaphoreSlim.Release();
// }
// }
// internal async Task<DealResult> ClosePosition(string figi)
// {
// if (!string.IsNullOrEmpty(figi) && Assets.TryGetValue(figi, out var asset))
// {
// try
// {
// var req = new PostOrderRequest()
// {
// AccountId = AccountId,
// InstrumentId = figi,
// };
// if (asset != null)
// {
// req.Direction = OrderDirection.Sell;
// req.OrderType = OrderType.Market;
// req.Quantity = (long)asset.Count;
// req.ConfirmMarginTrade = true;
// var res = await _investApiClient.Orders.PostOrderAsync(req);
// return new DealResult
// {
// Count = res.LotsExecuted,
// Price = res.ExecutedOrderPrice,
// Success = true,
// };
// }
// }
// catch (Exception ex)
// {
// _logger.LogError(ex, "Ошибка при закрытии позиции по счёту {acc}. figi: {figi}", AccountId, figi);
// }
// }
// return new DealResult
// {
// Count = 0,
// Price = 0,
// Success = false,
// };
// }
// internal async Task<DealResult> BuyAsset(string figi, decimal count, string? ticker = null, decimal? recommendedPrice = null)
// {
// try
// {
// var req = new PostOrderRequest()
// {
// AccountId = AccountId,
// InstrumentId = figi,
// Direction = OrderDirection.Buy,
// OrderType = OrderType.Market,
// Quantity = (long)count,
// };
// var res = await _investApiClient.Orders.PostOrderAsync(req);
// using var context = await _dbContextFactory.CreateDbContextAsync();
// context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
// var trade = await context.Trades.FirstOrDefaultAsync(t => t.ArchiveStatus == 0 && t.Figi == figi);
// if (trade == null)
// {
// var newTrade = new DataLayer.Entities.Trades.Trade()
// {
// AccountId = AccountId,
// Figi = figi,
// Ticker = ticker ?? string.Empty,
// BoughtAt = DateTime.UtcNow,
// Count = res.LotsExecuted,
// Price = res.ExecutedOrderPrice,
// Position = DataLayer.Entities.Trades.Enums.PositionType.Long,
// Direction = DataLayer.Entities.Trades.Enums.TradeDirection.Buy,
// Asset = DataLayer.Entities.Trades.Enums.AssetType.Common,
// };
// await context.Trades.AddAsync(newTrade);
// }
// else
// {
// var oldAmount = trade.Price * trade.Count;
// var newAmount = res.ExecutedOrderPrice * res.LotsExecuted;
// trade.Count = res.LotsExecuted + trade.Count;
// trade.Price = (oldAmount + newAmount) / trade.Count;
// context.Trades.Update(trade);
// }
// await context.SaveChangesAsync();
// return new DealResult
// {
// Count = res.LotsExecuted,
// Price = res.ExecutedOrderPrice,
// Success = true,
// };
// }
// catch (Exception ex)
// {
// _logger.LogError(ex, "Ошибка при покупке актива на счёт {acc}. figi: {figi}", AccountId, figi);
// }
// return new DealResult
// {
// Count = 0,
// Price = 0,
// Success = false,
// };
// }
// private async Task ProcessMarketCommand(TradeCommand command)
// {
// if (string.IsNullOrWhiteSpace(command.Figi)) return;
// if (command.CommandType == TradeCommandType.MarketBuy)
// {
// await BuyAsset(command.Figi, command.Count ?? 1, command.Ticker, command.RecomendPrice);
// }
// else if (command.CommandType == TradeCommandType.ForceClosePosition)
// {
// await ClosePosition(command.Figi);
// }
// else return;
// await SyncPortfolio();
// }
}
}

View File

@ -20,6 +20,7 @@ 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
@ -190,7 +191,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
try
{
if (message.Figi == "FUTIMOEXF000" && message.Direction==1)
if (message.Figi == "FUTIMOEXF000" && message.Direction == 1)
{
ProcessStops(message, currentTime);
var windowMaxSize = 2000;
@ -224,26 +225,29 @@ namespace KLHZ.Trader.Core.Exchange.Services
var assets = acc.Assets.Values.Where(a => a.Figi == message.Figi).ToArray();
foreach (var asset in assets)
{
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 < -66m)
if (await asset.Lock(TimeSpan.FromSeconds(60)))
{
var command = new TradeCommand()
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)
{
AccountId = asset.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);
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);
}
}
}
}
@ -369,7 +373,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
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;
@ -394,7 +398,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
{
if (IsBuyAllowed(acc.Value, message.Value, 1, _accountCashPartFutures, _accountCashPart))
{
if (RandomNumberGenerator.GetInt32(100) > 50)
if (RandomNumberGenerator.GetInt32(100) > 50 && await acc.Value.Lock(TimeSpan.FromSeconds(60)))
{
var command = new TradeCommand()
{
@ -403,6 +407,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
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}",
@ -431,38 +436,41 @@ namespace KLHZ.Trader.Core.Exchange.Services
.ToArray();
foreach (var asset in assetsForClose)
{
var profit = 0m;
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 (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()
if (profit > 0)
{
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);
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);
}
}
}
}
@ -482,7 +490,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
var loggedDeclisions = 0;
foreach (var acc in accounts)
{
if (BotModeSwitcher.CanSell())
if (BotModeSwitcher.CanSell() && await acc.Value.Lock(TimeSpan.FromSeconds(60)))
{
if (RandomNumberGenerator.GetInt32(100) > 50)
{
@ -493,7 +501,8 @@ namespace KLHZ.Trader.Core.Exchange.Services
CommandType = Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketSell,
Count = 1,
RecomendPrice = null,
EnableMargin = true
EnableMargin = true,
ExchangeObject = acc.Value,
};
await _dataBus.Broadcast(command);
_logger.LogWarning("Открытие шорта {figi}! id команды {commandId}. Направление сделки: {dir}; Количество активов: {count}; Разрешена ли маржиналка: {margin}",
@ -524,31 +533,34 @@ namespace KLHZ.Trader.Core.Exchange.Services
.ToArray();
foreach (var asset in assetsForClose)
{
var profit = 0m;
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()
if (assetType == AssetType.Futures)
{
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)
profit = TradingCalculator.CaclProfit(asset.BoughtPrice, message.Value,
GetComission(assetType), GetLeverage(message.Figi, asset.Count < 0), asset.Count < 0);
}
if (profit > 0)
{
loggedDeclisions++;
await LogDeclision(DeclisionTradeAction.CloseShortReal, message, profit);
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);
}
}
}
}

View File

@ -91,6 +91,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
{
_logger.LogError(ex, "Ошибка при покупке актива на счёт {acc}. figi: {figi}", tradeCommand.AccountId, tradeCommand.Figi);
}
tradeCommand.ExchangeObject?.Unlock();
}
private async Task ProcessCommands()