Refactoring TradingBotBase.cs + clean architecture (#38)
* Refactoring TradingBotBase.cs + clean architecture * Fix basic tests * Fix tests * Fix workers * Fix open positions * Fix closing position stucking the grain * Fix comments * Refactor candle handling to use IReadOnlyList for chronological order preservation across various components
This commit is contained in:
@@ -129,7 +129,7 @@ public class BacktestExecutor
|
||||
/// <returns>The lightweight backtest result</returns>
|
||||
public async Task<LightBacktest> ExecuteAsync(
|
||||
TradingBotConfig config,
|
||||
HashSet<Candle> candles,
|
||||
IReadOnlyList<Candle> candles,
|
||||
User user,
|
||||
bool save = false,
|
||||
bool withCandles = false,
|
||||
@@ -166,7 +166,9 @@ public class BacktestExecutor
|
||||
|
||||
// Create a fresh TradingBotBase instance for this backtest
|
||||
var tradingBot = CreateTradingBotInstance(config);
|
||||
tradingBot.Account = user.Accounts.First();
|
||||
var account = user.Accounts.First();
|
||||
account.User = user; // Ensure Account.User is set for backtest
|
||||
tradingBot.Account = account;
|
||||
|
||||
var totalCandles = candles.Count;
|
||||
var currentCandle = 0;
|
||||
@@ -220,8 +222,9 @@ public class BacktestExecutor
|
||||
// The signal calculation depends on rolling window state and cannot be pre-calculated effectively
|
||||
|
||||
// Use optimized rolling window approach - TradingBox.GetSignal only needs last 600 candles
|
||||
// Use List<Candle> directly to preserve chronological order and enable incremental updates
|
||||
const int RollingWindowSize = 600; // TradingBox.GetSignal only needs last 600 candles
|
||||
var rollingWindowCandles = new Queue<Candle>(RollingWindowSize);
|
||||
var rollingWindowCandles = new List<Candle>(RollingWindowSize); // Pre-allocate capacity for performance
|
||||
var candlesProcessed = 0;
|
||||
|
||||
// Signal caching optimization - reduce signal update frequency for better performance
|
||||
@@ -253,21 +256,20 @@ public class BacktestExecutor
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Maintain rolling window of last 600 candles to prevent exponential memory growth
|
||||
rollingWindowCandles.Enqueue(candle);
|
||||
if (rollingWindowCandles.Count > RollingWindowSize)
|
||||
// Incremental updates: remove oldest if at capacity, then add newest
|
||||
// This preserves chronological order and avoids expensive HashSet recreation
|
||||
if (rollingWindowCandles.Count >= RollingWindowSize)
|
||||
{
|
||||
rollingWindowCandles.Dequeue(); // Remove oldest candle
|
||||
rollingWindowCandles.RemoveAt(0); // Remove oldest candle (O(n) but only 600 items max)
|
||||
}
|
||||
rollingWindowCandles.Add(candle); // Add newest candle (O(1) amortized)
|
||||
|
||||
tradingBot.LastCandle = candle;
|
||||
|
||||
// Run with optimized backtest path (minimize async calls)
|
||||
var signalUpdateStart = Stopwatch.GetTimestamp();
|
||||
// Convert rolling window to HashSet for TradingBot.UpdateSignals compatibility
|
||||
// NOTE: Recreating HashSet each iteration is necessary to maintain correct enumeration order
|
||||
// Incremental updates break business logic (changes PnL results)
|
||||
var fixedCandles = new HashSet<Candle>(rollingWindowCandles);
|
||||
await tradingBot.UpdateSignals(fixedCandles, preCalculatedIndicatorValues);
|
||||
// Pass List<Candle> directly - no conversion needed, order is preserved
|
||||
await tradingBot.UpdateSignals(rollingWindowCandles, preCalculatedIndicatorValues);
|
||||
signalUpdateTotalTime += Stopwatch.GetElapsedTime(signalUpdateStart);
|
||||
|
||||
var backtestStepStart = Stopwatch.GetTimestamp();
|
||||
@@ -542,7 +544,7 @@ public class BacktestExecutor
|
||||
throw new InvalidOperationException("Bot configuration is not initialized");
|
||||
}
|
||||
|
||||
if (!config.IsForBacktest)
|
||||
if (config.TradingType != TradingType.BacktestFutures)
|
||||
{
|
||||
throw new InvalidOperationException("BacktestExecutor can only be used for backtesting");
|
||||
}
|
||||
@@ -550,7 +552,7 @@ public class BacktestExecutor
|
||||
// Create the trading bot instance
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var logger = scope.ServiceProvider.GetRequiredService<ILogger<TradingBotBase>>();
|
||||
var tradingBot = new TradingBotBase(logger, _scopeFactory, config);
|
||||
var tradingBot = new BacktestFuturesBot(logger, _scopeFactory, config);
|
||||
return tradingBot;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,19 +42,22 @@ public class BacktestExecutorAdapter : IBacktester
|
||||
object metadata = null)
|
||||
{
|
||||
// Load candles using ExchangeService
|
||||
var candles = await _exchangeService.GetCandlesInflux(
|
||||
var candlesHashSet = await _exchangeService.GetCandlesInflux(
|
||||
TradingExchanges.Evm,
|
||||
config.Ticker,
|
||||
startDate,
|
||||
config.Timeframe,
|
||||
endDate);
|
||||
|
||||
if (candles == null || candles.Count == 0)
|
||||
if (candlesHashSet == null || candlesHashSet.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"No candles found for {config.Ticker} on {config.Timeframe} from {startDate} to {endDate}");
|
||||
}
|
||||
|
||||
// Convert to ordered List to preserve chronological order for backtest
|
||||
var candles = candlesHashSet.OrderBy(c => c.Date).ToList();
|
||||
|
||||
// Execute using BacktestExecutor
|
||||
var result = await _executor.ExecuteAsync(
|
||||
config,
|
||||
@@ -73,12 +76,15 @@ public class BacktestExecutorAdapter : IBacktester
|
||||
|
||||
public async Task<LightBacktest> RunTradingBotBacktest(
|
||||
TradingBotConfig config,
|
||||
HashSet<Candle> candles,
|
||||
HashSet<Candle> candlesHashSet,
|
||||
User user = null,
|
||||
bool withCandles = false,
|
||||
string requestId = null,
|
||||
object metadata = null)
|
||||
{
|
||||
// Convert to ordered List to preserve chronological order for backtest
|
||||
var candles = candlesHashSet.OrderBy(c => c.Date).ToList();
|
||||
|
||||
// Execute using BacktestExecutor
|
||||
var result = await _executor.ExecuteAsync(
|
||||
config,
|
||||
|
||||
@@ -197,7 +197,7 @@ public class JobService
|
||||
Timeframe = backtestRequest.Config.Timeframe,
|
||||
IsForWatchingOnly = backtestRequest.Config.IsForWatchingOnly,
|
||||
BotTradingBalance = backtestRequest.Config.BotTradingBalance,
|
||||
IsForBacktest = true,
|
||||
TradingType = TradingType.BacktestFutures,
|
||||
CooldownPeriod = backtestRequest.Config.CooldownPeriod ?? 1,
|
||||
MaxLossStreak = backtestRequest.Config.MaxLossStreak,
|
||||
MaxPositionTimeHours = backtestRequest.Config.MaxPositionTimeHours,
|
||||
|
||||
@@ -1,5 +1,332 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Trading.Commands;
|
||||
using Managing.Application.Trading.Handlers;
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Accounts;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Indicators;
|
||||
using Managing.Domain.Shared.Helpers;
|
||||
using Managing.Domain.Strategies.Base;
|
||||
using Managing.Domain.Trades;
|
||||
using Managing.Domain.Users;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Orleans.Streams;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Bots;
|
||||
|
||||
public class BacktestFuturesBot
|
||||
public class BacktestFuturesBot : TradingBotBase, ITradingBot
|
||||
{
|
||||
public BacktestFuturesBot(
|
||||
ILogger<TradingBotBase> logger,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
TradingBotConfig config,
|
||||
IStreamProvider? streamProvider = null
|
||||
) : base(logger, scopeFactory, config, streamProvider)
|
||||
{
|
||||
// Backtest-specific initialization
|
||||
Config.TradingType = TradingType.BacktestFutures;
|
||||
}
|
||||
|
||||
public override async Task Start(BotStatus previousStatus)
|
||||
{
|
||||
// Backtest mode: Skip account loading and broker initialization
|
||||
// Just log basic startup info
|
||||
await LogInformation($"🔬 Backtest Bot Started\n" +
|
||||
$"📊 Testing Setup:\n" +
|
||||
$"🎯 Ticker: `{Config.Ticker}`\n" +
|
||||
$"⏰ Timeframe: `{Config.Timeframe}`\n" +
|
||||
$"🎮 Scenario: `{Config.Scenario?.Name ?? "Unknown"}`\n" +
|
||||
$"💰 Initial Balance: `${Config.BotTradingBalance:F2}`\n" +
|
||||
$"✅ Ready to run backtest simulation");
|
||||
}
|
||||
|
||||
public override async Task Run()
|
||||
{
|
||||
// Backtest signal update is handled in BacktestExecutor loop
|
||||
// No need to call UpdateSignals() here
|
||||
|
||||
if (!Config.IsForWatchingOnly)
|
||||
await ManagePositions();
|
||||
|
||||
UpdateWalletBalances();
|
||||
|
||||
// Backtest logging - simplified, no account dependency
|
||||
ExecutionCount++;
|
||||
Logger.LogInformation(
|
||||
"[Backtest][{BotName}] Execution {ExecutionCount} - LastCandleDate: {LastCandleDate}, Signals: {SignalCount}, Positions: {PositionCount}",
|
||||
Config.Name, ExecutionCount, LastCandle?.Date, Signals.Count, Positions.Count);
|
||||
}
|
||||
|
||||
|
||||
protected override async Task<Position> GetInternalPositionForUpdate(Position position)
|
||||
{
|
||||
// In backtest mode, return the position as-is (no database lookup needed)
|
||||
return position;
|
||||
}
|
||||
|
||||
protected override async Task<List<Position>> GetBrokerPositionsForUpdate(Account account)
|
||||
{
|
||||
// In backtest mode, return empty list (no broker positions to check)
|
||||
return new List<Position>();
|
||||
}
|
||||
|
||||
protected override async Task UpdatePositionWithBrokerData(Position position, List<Position> brokerPositions)
|
||||
{
|
||||
// In backtest mode, skip broker synchronization
|
||||
return;
|
||||
}
|
||||
|
||||
protected override async Task<Candle> GetCurrentCandleForPositionClose(Account account, string ticker)
|
||||
{
|
||||
// In backtest mode, use LastCandle
|
||||
return LastCandle;
|
||||
}
|
||||
|
||||
protected override async Task<bool> CanOpenPositionWithBrokerChecks(LightSignal signal)
|
||||
{
|
||||
// In backtest mode, skip broker position checks
|
||||
return await CanOpenPosition(signal);
|
||||
}
|
||||
|
||||
protected override async Task LoadAccountAsync()
|
||||
{
|
||||
// In backtest mode, skip account loading
|
||||
return;
|
||||
}
|
||||
|
||||
protected override async Task VerifyAndUpdateBalanceAsync()
|
||||
{
|
||||
// In backtest mode, skip balance verification
|
||||
return;
|
||||
}
|
||||
|
||||
protected override async Task SendPositionToCopyTradingStream(Position position)
|
||||
{
|
||||
// In backtest mode, skip copy trading stream
|
||||
return;
|
||||
}
|
||||
|
||||
protected override async Task NotifyAgentAndPlatformAsync(NotificationEventType eventType, Position position)
|
||||
{
|
||||
// In backtest mode, skip notifications
|
||||
return;
|
||||
}
|
||||
|
||||
protected override async Task UpdatePositionInDatabaseAsync(Position position)
|
||||
{
|
||||
// In backtest mode, skip database updates
|
||||
return;
|
||||
}
|
||||
|
||||
protected override async Task SendClosedPositionToMessenger(Position position, User user)
|
||||
{
|
||||
// In backtest mode, skip messenger updates
|
||||
return;
|
||||
}
|
||||
|
||||
protected override async Task CancelAllOrdersAsync()
|
||||
{
|
||||
// In backtest mode, no orders to cancel
|
||||
return;
|
||||
}
|
||||
|
||||
protected override async Task LogInformationAsync(string message)
|
||||
{
|
||||
// In backtest mode, skip user notifications, just log to system
|
||||
if (Config.TradingType == TradingType.BacktestFutures)
|
||||
return;
|
||||
|
||||
await base.LogInformationAsync(message);
|
||||
}
|
||||
|
||||
protected override async Task LogWarningAsync(string message)
|
||||
{
|
||||
// In backtest mode, skip user notifications, just log to system
|
||||
if (Config.TradingType == TradingType.BacktestFutures)
|
||||
return;
|
||||
|
||||
await base.LogWarningAsync(message);
|
||||
}
|
||||
|
||||
protected override async Task LogDebugAsync(string message)
|
||||
{
|
||||
// In backtest mode, skip messenger debug logs
|
||||
if (Config.TradingType == TradingType.BacktestFutures)
|
||||
return;
|
||||
|
||||
await base.LogDebugAsync(message);
|
||||
}
|
||||
|
||||
protected override async Task SendTradeMessageAsync(string message, bool isBadBehavior)
|
||||
{
|
||||
// In backtest mode, skip trade messages
|
||||
return;
|
||||
}
|
||||
|
||||
protected override async Task UpdateSignalsCore(IReadOnlyList<Candle> candles,
|
||||
Dictionary<IndicatorType, IndicatorsResultBase> preCalculatedIndicatorValues = null)
|
||||
{
|
||||
// Call base implementation for common logic (flip check, cooldown check)
|
||||
await base.UpdateSignalsCore(candles, preCalculatedIndicatorValues);
|
||||
|
||||
// For backtest, if no candles provided (called from Run()), skip signal generation
|
||||
// Signals are generated in BacktestExecutor with rolling window candles
|
||||
if (candles == null || candles.Count == 0)
|
||||
return;
|
||||
|
||||
if (Config.Scenario == null)
|
||||
throw new ArgumentNullException(nameof(Config.Scenario), "Config.Scenario cannot be null");
|
||||
|
||||
// Use TradingBox.GetSignal for backtest with pre-calculated indicators
|
||||
var backtestSignal = TradingBox.GetSignal(candles, Config.Scenario, Signals, Config.Scenario.LoopbackPeriod,
|
||||
preCalculatedIndicatorValues);
|
||||
if (backtestSignal == null) return;
|
||||
|
||||
await AddSignal(backtestSignal);
|
||||
}
|
||||
|
||||
protected override async Task<decimal> GetLastPriceForPositionOpeningAsync()
|
||||
{
|
||||
// For backtest, use LastCandle close price
|
||||
return LastCandle?.Close ?? 0;
|
||||
}
|
||||
|
||||
protected override async Task<bool> CanOpenPosition(LightSignal signal)
|
||||
{
|
||||
// Backtest-specific logic: only check cooldown and loss streak
|
||||
// No broker checks, no synth risk assessment, no startup cycle check needed
|
||||
return !await IsInCooldownPeriodAsync() && await CheckLossStreak(signal);
|
||||
}
|
||||
|
||||
protected override async Task<Position> HandleFlipPosition(LightSignal signal, Position openedPosition,
|
||||
LightSignal previousSignal, decimal lastPrice)
|
||||
{
|
||||
// Backtest-specific flip logic
|
||||
if (Config.FlipPosition)
|
||||
{
|
||||
var isPositionInProfit = (openedPosition.ProfitAndLoss?.Realized ?? 0) > 0;
|
||||
var shouldFlip = !Config.FlipOnlyWhenInProfit || isPositionInProfit;
|
||||
|
||||
if (shouldFlip)
|
||||
{
|
||||
var flipReason = Config.FlipOnlyWhenInProfit
|
||||
? "current position is in profit"
|
||||
: "FlipOnlyWhenInProfit is disabled";
|
||||
|
||||
await LogInformationAsync(
|
||||
$"🔄 Position Flip Initiated\nFlipping position due to opposite signal\nReason: {flipReason}");
|
||||
await CloseTrade(previousSignal, openedPosition, openedPosition.Open, lastPrice, true);
|
||||
await SetPositionStatus(previousSignal.Identifier, PositionStatus.Flipped);
|
||||
var newPosition = await OpenPosition(signal);
|
||||
await LogInformationAsync(
|
||||
$"✅ Position Flipped\nPosition: `{previousSignal.Identifier}` → `{signal.Identifier}`\nPrice: `${lastPrice}`");
|
||||
return newPosition;
|
||||
}
|
||||
else
|
||||
{
|
||||
var currentPnl = openedPosition.ProfitAndLoss?.Realized ?? 0;
|
||||
await LogInformationAsync(
|
||||
$"💸 Flip Blocked - Not Profitable\nPosition `{previousSignal.Identifier}` PnL: `${currentPnl:F2}`\nSignal `{signal.Identifier}` will wait for profitability");
|
||||
|
||||
SetSignalStatus(signal.Identifier, SignalStatus.Expired);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await LogInformationAsync(
|
||||
$"🚫 Flip Disabled\nPosition already open for: `{previousSignal.Identifier}`\nFlipping disabled, new signal expired");
|
||||
SetSignalStatus(signal.Identifier, SignalStatus.Expired);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task<Position> ExecuteOpenPosition(LightSignal signal, decimal lastPrice)
|
||||
{
|
||||
// Backtest-specific position opening: no balance verification, no exchange calls
|
||||
if (Account == null || Account.User == null)
|
||||
{
|
||||
throw new InvalidOperationException("Account and Account.User must be set before opening a position");
|
||||
}
|
||||
|
||||
var command = new OpenPositionRequest(
|
||||
Config.AccountName,
|
||||
Config.MoneyManagement,
|
||||
signal.Direction,
|
||||
Config.Ticker,
|
||||
PositionInitiator.Bot,
|
||||
signal.Date,
|
||||
Account.User,
|
||||
Config.BotTradingBalance,
|
||||
isForPaperTrading: true, // Backtest is always paper trading
|
||||
lastPrice,
|
||||
signalIdentifier: signal.Identifier,
|
||||
initiatorIdentifier: Identifier,
|
||||
tradingType: Config.TradingType);
|
||||
|
||||
var position = await ServiceScopeHelpers
|
||||
.WithScopedServices<IExchangeService, IAccountService, ITradingService, Position>(
|
||||
_scopeFactory,
|
||||
async (exchangeService, accountService, tradingService) =>
|
||||
{
|
||||
return await new OpenPositionCommandHandler(exchangeService, accountService, tradingService)
|
||||
.Handle(command);
|
||||
});
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
public override async Task CloseTrade(LightSignal signal, Position position, Trade tradeToClose, decimal lastPrice,
|
||||
bool tradeClosingPosition = false, bool forceMarketClose = false)
|
||||
{
|
||||
await LogInformationAsync(
|
||||
$"🔧 Closing {position.OriginDirection} Trade\nTicker: `{Config.Ticker}`\nPrice: `${lastPrice}`\n📋 Type: `{tradeToClose.TradeType}`\n📊 Quantity: `{tradeToClose.Quantity:F5}`");
|
||||
|
||||
// Backtest-specific: no exchange quantity check, no grace period, direct close
|
||||
var command = new CloseBacktestFuturesPositionCommand(position, position.AccountId, lastPrice);
|
||||
try
|
||||
{
|
||||
Position closedPosition = null;
|
||||
await ServiceScopeHelpers.WithScopedServices<IExchangeService, IAccountService, ITradingService>(
|
||||
_scopeFactory, async (exchangeService, accountService, tradingService) =>
|
||||
{
|
||||
closedPosition =
|
||||
await new CloseBacktestFuturesPositionCommandHandler(exchangeService, accountService, tradingService,
|
||||
_scopeFactory)
|
||||
.Handle(command);
|
||||
});
|
||||
|
||||
if (closedPosition.Status == PositionStatus.Finished || closedPosition.Status == PositionStatus.Flipped)
|
||||
{
|
||||
if (tradeClosingPosition)
|
||||
{
|
||||
await SetPositionStatus(signal.Identifier, PositionStatus.Finished);
|
||||
}
|
||||
|
||||
await HandleClosedPosition(closedPosition, forceMarketClose ? lastPrice : (decimal?)null,
|
||||
forceMarketClose);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"Wrong position status : {closedPosition.Status}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await LogWarningAsync($"Position {signal.Identifier} not closed : {ex.Message}");
|
||||
|
||||
if (position.Status == PositionStatus.Canceled || position.Status == PositionStatus.Rejected)
|
||||
{
|
||||
// Trade close on exchange => Should close trade manually
|
||||
await SetPositionStatus(signal.Identifier, PositionStatus.Finished);
|
||||
// Ensure trade dates are properly updated even for canceled/rejected positions
|
||||
await HandleClosedPosition(position, forceMarketClose ? lastPrice : (decimal?)null,
|
||||
forceMarketClose);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -53,7 +53,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
|
||||
/// <returns>The complete backtest result</returns>
|
||||
public async Task<LightBacktest> RunBacktestAsync(
|
||||
TradingBotConfig config,
|
||||
HashSet<Candle> candles,
|
||||
IReadOnlyList<Candle> candles,
|
||||
User user = null,
|
||||
bool save = false,
|
||||
bool withCandles = false,
|
||||
@@ -67,7 +67,9 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
|
||||
|
||||
// Create a fresh TradingBotBase instance for this backtest
|
||||
var tradingBot = await CreateTradingBotInstance(config);
|
||||
tradingBot.Account = user.Accounts.First();
|
||||
var account = user.Accounts.First();
|
||||
account.User = user; // Ensure Account.User is set for backtest
|
||||
tradingBot.Account = account;
|
||||
|
||||
var totalCandles = candles.Count;
|
||||
var currentCandle = 0;
|
||||
@@ -81,7 +83,9 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
|
||||
tradingBot.WalletBalances.Add(candles.FirstOrDefault()!.Date, config.BotTradingBalance);
|
||||
var initialBalance = config.BotTradingBalance;
|
||||
|
||||
var fixedCandles = new HashSet<Candle>();
|
||||
const int RollingWindowSize = 600; // TradingBox.GetSignal only needs last 600 candles
|
||||
// Use List<Candle> directly to preserve chronological order and enable incremental updates
|
||||
var rollingWindowCandles = new List<Candle>(RollingWindowSize); // Pre-allocate capacity for performance
|
||||
var lastYieldTime = DateTime.UtcNow;
|
||||
const int yieldIntervalMs = 5000; // Yield control every 5 seconds to prevent timeout
|
||||
const int candlesPerBatch = 100; // Process in batches to allow Orleans to check for cancellation
|
||||
@@ -89,11 +93,19 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
|
||||
// Process all candles following the exact pattern from GetBacktestingResult
|
||||
foreach (var candle in candles)
|
||||
{
|
||||
fixedCandles.Add(candle);
|
||||
// Maintain rolling window: remove oldest if at capacity, then add newest
|
||||
// This preserves chronological order and avoids expensive HashSet recreation
|
||||
if (rollingWindowCandles.Count >= RollingWindowSize)
|
||||
{
|
||||
rollingWindowCandles.RemoveAt(0); // Remove oldest candle (O(n) but only 600 items max)
|
||||
}
|
||||
rollingWindowCandles.Add(candle); // Add newest candle (O(1) amortized)
|
||||
|
||||
tradingBot.LastCandle = candle;
|
||||
|
||||
// Update signals manually only for backtesting
|
||||
await tradingBot.UpdateSignals(fixedCandles);
|
||||
// Update signals manually only for backtesting with rolling window
|
||||
// Pass List<Candle> directly - no conversion needed, order is preserved
|
||||
await tradingBot.UpdateSignals(rollingWindowCandles);
|
||||
await tradingBot.Run();
|
||||
|
||||
currentCandle++;
|
||||
@@ -132,11 +144,12 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
|
||||
|
||||
_logger.LogInformation("Backtest processing completed. Calculating final results...");
|
||||
|
||||
var finalPnl = TradingBox.GetTotalNetPnL(tradingBot.Positions);
|
||||
var realizedPnl = TradingBox.GetTotalRealizedPnL(tradingBot.Positions); // PnL before fees
|
||||
var netPnl = TradingBox.GetTotalNetPnL(tradingBot.Positions); // PnL after fees
|
||||
var winRate = TradingBox.GetWinRate(tradingBot.Positions);
|
||||
var stats = TradingBox.GetStatistics(tradingBot.WalletBalances);
|
||||
var growthPercentage =
|
||||
TradingBox.GetGrowthFromInitalBalance(tradingBot.WalletBalances.FirstOrDefault().Value, finalPnl);
|
||||
TradingBox.GetGrowthFromInitalBalance(tradingBot.WalletBalances.FirstOrDefault().Value, netPnl);
|
||||
var hodlPercentage = TradingBox.GetHodlPercentage(candles.First(), candles.Last());
|
||||
|
||||
var fees = TradingBox.GetTotalFees(tradingBot.Positions);
|
||||
@@ -145,7 +158,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
|
||||
growthPercentage: (double)growthPercentage,
|
||||
hodlPercentage: (double)hodlPercentage,
|
||||
winRate: winRate,
|
||||
totalPnL: (double)finalPnl,
|
||||
totalPnL: (double)realizedPnl,
|
||||
fees: (double)fees,
|
||||
tradeCount: tradingBot.Positions.Count,
|
||||
maxDrawdownRecoveryTime: stats.MaxDrawdownRecoveryTime,
|
||||
@@ -166,7 +179,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
|
||||
// Create backtest result with conditional candles and indicators values
|
||||
var result = new Backtest(config, tradingBot.Positions, tradingBot.Signals)
|
||||
{
|
||||
FinalPnl = finalPnl,
|
||||
FinalPnl = realizedPnl, // Realized PnL before fees
|
||||
WinRate = winRate,
|
||||
GrowthPercentage = growthPercentage,
|
||||
HodlPercentage = hodlPercentage,
|
||||
@@ -180,7 +193,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
|
||||
StartDate = candles.FirstOrDefault()!.OpenTime,
|
||||
EndDate = candles.LastOrDefault()!.OpenTime,
|
||||
InitialBalance = initialBalance,
|
||||
NetPnl = finalPnl - fees,
|
||||
NetPnl = netPnl, // Net PnL after fees
|
||||
};
|
||||
|
||||
if (save && user != null)
|
||||
@@ -233,14 +246,14 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
|
||||
throw new InvalidOperationException("Bot configuration is not initialized");
|
||||
}
|
||||
|
||||
if (!config.IsForBacktest)
|
||||
if (config.TradingType != TradingType.BacktestFutures)
|
||||
{
|
||||
throw new InvalidOperationException("BacktestTradingBotGrain can only be used for backtesting");
|
||||
}
|
||||
|
||||
// Create the trading bot instance
|
||||
var logger = _scopeFactory.CreateScope().ServiceProvider.GetRequiredService<ILogger<TradingBotBase>>();
|
||||
var tradingBot = new TradingBotBase(logger, _scopeFactory, config);
|
||||
var tradingBot = new BacktestFuturesBot(logger, _scopeFactory, config);
|
||||
return tradingBot;
|
||||
}
|
||||
|
||||
@@ -284,7 +297,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
|
||||
/// Gets indicators values (following Backtester.cs pattern)
|
||||
/// </summary>
|
||||
private Dictionary<IndicatorType, IndicatorsResultBase> GetIndicatorsValues(List<LightIndicator> indicators,
|
||||
HashSet<Candle> candles)
|
||||
IReadOnlyList<Candle> candles)
|
||||
{
|
||||
var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
|
||||
throw new InvalidOperationException("Bot configuration is not properly initialized");
|
||||
}
|
||||
|
||||
if (config.IsForBacktest)
|
||||
if (config.TradingType == TradingType.BacktestFutures)
|
||||
{
|
||||
throw new InvalidOperationException("LiveTradingBotGrain cannot be used for backtesting");
|
||||
}
|
||||
@@ -531,7 +531,7 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var logger = scope.ServiceProvider.GetRequiredService<ILogger<TradingBotBase>>();
|
||||
var streamProvider = this.GetStreamProvider("ManagingStreamProvider");
|
||||
var tradingBot = new TradingBotBase(logger, _scopeFactory, config, streamProvider);
|
||||
var tradingBot = new FuturesBot(logger, _scopeFactory, config, streamProvider);
|
||||
|
||||
// Load state into the trading bot instance
|
||||
LoadStateIntoTradingBot(tradingBot);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -955,7 +955,7 @@ public class TradingBotChromosome : ChromosomeBase
|
||||
Ticker = request.Ticker,
|
||||
Timeframe = request.Timeframe,
|
||||
BotTradingBalance = request.Balance,
|
||||
IsForBacktest = true,
|
||||
TradingType = TradingType.BacktestFutures,
|
||||
IsForWatchingOnly = false,
|
||||
CooldownPeriod = Convert.ToInt32(genes[2].Value),
|
||||
MaxLossStreak = Convert.ToInt32(genes[3].Value),
|
||||
@@ -1104,7 +1104,7 @@ public class TradingBotFitness : IFitness
|
||||
_serviceScopeFactory,
|
||||
async executor => await executor.ExecuteAsync(
|
||||
config,
|
||||
_candles,
|
||||
_candles.OrderBy(c => c.Date).ToList(),
|
||||
_request.User,
|
||||
save: true,
|
||||
withCandles: false,
|
||||
|
||||
@@ -1,534 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using Managing.Application.Abstractions.Grains;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Orleans;
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Accounts;
|
||||
using Managing.Domain.Backtests;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.MoneyManagements;
|
||||
using Managing.Domain.Scenarios;
|
||||
using Managing.Domain.Strategies;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Orleans.Concurrency;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Grains;
|
||||
|
||||
/// <summary>
|
||||
/// Stateless worker grain for processing bundle backtest requests
|
||||
/// Uses the bundle request ID as the primary key (Guid)
|
||||
/// Implements IRemindable for automatic retry of failed bundles
|
||||
/// Uses custom compute placement with random fallback.
|
||||
/// </summary>
|
||||
[StatelessWorker]
|
||||
[TradingPlacement] // Use custom compute placement with random fallback
|
||||
public class BundleBacktestGrain : Grain, IBundleBacktestGrain, IRemindable
|
||||
{
|
||||
private readonly ILogger<BundleBacktestGrain> _logger;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
|
||||
// Reminder configuration
|
||||
private const string RETRY_REMINDER_NAME = "BundleBacktestRetry";
|
||||
private static readonly TimeSpan RETRY_INTERVAL = TimeSpan.FromMinutes(30);
|
||||
|
||||
public BundleBacktestGrain(
|
||||
ILogger<BundleBacktestGrain> logger,
|
||||
IServiceScopeFactory scopeFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_scopeFactory = scopeFactory;
|
||||
}
|
||||
|
||||
public async Task ProcessBundleRequestAsync()
|
||||
{
|
||||
// Get the RequestId from the grain's primary key
|
||||
var bundleRequestId = this.GetPrimaryKey();
|
||||
|
||||
try
|
||||
{
|
||||
// Create a new service scope to get fresh instances of services with scoped DbContext
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var backtester = scope.ServiceProvider.GetRequiredService<IBacktester>();
|
||||
var messengerService = scope.ServiceProvider.GetRequiredService<IMessengerService>();
|
||||
|
||||
// Get the specific bundle request by ID
|
||||
var bundleRequest = await GetBundleRequestById(backtester, bundleRequestId);
|
||||
if (bundleRequest == null)
|
||||
{
|
||||
_logger.LogError("Bundle request {RequestId} not found", bundleRequestId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Process only this specific bundle request
|
||||
await ProcessBundleRequest(bundleRequest, backtester, messengerService);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in BundleBacktestGrain for request {RequestId}", bundleRequestId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<BundleBacktestRequest> GetBundleRequestById(IBacktester backtester, Guid bundleRequestId)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get pending and failed bundle backtest requests for retry capability
|
||||
var pendingRequests =
|
||||
await backtester.GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus.Pending);
|
||||
var failedRequests =
|
||||
await backtester.GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus.Failed);
|
||||
|
||||
var allRequests = pendingRequests.Concat(failedRequests);
|
||||
return allRequests.FirstOrDefault(r => r.RequestId == bundleRequestId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to get bundle request {RequestId}", bundleRequestId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessBundleRequest(
|
||||
BundleBacktestRequest bundleRequest,
|
||||
IBacktester backtester,
|
||||
IMessengerService messengerService)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Starting to process bundle backtest request {RequestId}", bundleRequest.RequestId);
|
||||
|
||||
// Update status to running
|
||||
bundleRequest.Status = BundleBacktestRequestStatus.Running;
|
||||
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
||||
|
||||
// Generate backtest requests from variant configuration
|
||||
var backtestRequests = await GenerateBacktestRequestsFromVariants(bundleRequest);
|
||||
if (backtestRequests == null || !backtestRequests.Any())
|
||||
{
|
||||
throw new InvalidOperationException("Failed to generate backtest requests from variants");
|
||||
}
|
||||
|
||||
// Process each backtest request sequentially
|
||||
for (int i = 0; i < backtestRequests.Count; i++)
|
||||
{
|
||||
await ProcessSingleBacktest(backtester, backtestRequests[i], bundleRequest, i);
|
||||
}
|
||||
|
||||
// Update final status and send notifications
|
||||
await UpdateFinalStatus(bundleRequest, backtester, messengerService);
|
||||
|
||||
_logger.LogInformation("Completed processing bundle backtest request {RequestId} with status {Status}",
|
||||
bundleRequest.RequestId, bundleRequest.Status);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing bundle backtest request {RequestId}", bundleRequest.RequestId);
|
||||
SentrySdk.CaptureException(ex);
|
||||
await HandleBundleRequestError(bundleRequest, backtester, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates individual backtest requests from variant configuration
|
||||
/// </summary>
|
||||
private async Task<List<RunBacktestRequest>> GenerateBacktestRequestsFromVariants(
|
||||
BundleBacktestRequest bundleRequest)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Deserialize the variant configurations
|
||||
var universalConfig =
|
||||
JsonSerializer.Deserialize<BundleBacktestUniversalConfig>(bundleRequest.UniversalConfigJson);
|
||||
var dateTimeRanges = JsonSerializer.Deserialize<List<DateTimeRange>>(bundleRequest.DateTimeRangesJson);
|
||||
var moneyManagementVariants =
|
||||
JsonSerializer.Deserialize<List<MoneyManagementVariant>>(bundleRequest.MoneyManagementVariantsJson);
|
||||
var tickerVariants = JsonSerializer.Deserialize<List<Ticker>>(bundleRequest.TickerVariantsJson);
|
||||
|
||||
if (universalConfig == null || dateTimeRanges == null || moneyManagementVariants == null ||
|
||||
tickerVariants == null)
|
||||
{
|
||||
_logger.LogError("Failed to deserialize variant configurations for bundle request {RequestId}",
|
||||
bundleRequest.RequestId);
|
||||
return new List<RunBacktestRequest>();
|
||||
}
|
||||
|
||||
// Get the first account for the user using AccountService
|
||||
var firstAccount = await ServiceScopeHelpers.WithScopedService<IAccountService, Account?>(
|
||||
_scopeFactory,
|
||||
async service =>
|
||||
{
|
||||
var accounts =
|
||||
await service.GetAccountsByUserAsync(bundleRequest.User, hideSecrets: true, getBalance: false);
|
||||
return accounts.FirstOrDefault();
|
||||
});
|
||||
|
||||
if (firstAccount == null)
|
||||
{
|
||||
_logger.LogError("No accounts found for user {UserId} in bundle request {RequestId}",
|
||||
bundleRequest.User.Id, bundleRequest.RequestId);
|
||||
return new List<RunBacktestRequest>();
|
||||
}
|
||||
|
||||
var backtestRequests = new List<RunBacktestRequest>();
|
||||
|
||||
foreach (var dateRange in dateTimeRanges)
|
||||
{
|
||||
foreach (var mmVariant in moneyManagementVariants)
|
||||
{
|
||||
foreach (var ticker in tickerVariants)
|
||||
{
|
||||
var config = new TradingBotConfigRequest
|
||||
{
|
||||
AccountName = firstAccount.Name,
|
||||
Ticker = ticker,
|
||||
Timeframe = universalConfig.Timeframe,
|
||||
IsForWatchingOnly = universalConfig.IsForWatchingOnly,
|
||||
BotTradingBalance = universalConfig.BotTradingBalance,
|
||||
Name =
|
||||
$"{universalConfig.BotName}_{ticker}_{dateRange.StartDate:yyyyMMdd}_{dateRange.EndDate:yyyyMMdd}",
|
||||
FlipPosition = universalConfig.FlipPosition,
|
||||
CooldownPeriod = universalConfig.CooldownPeriod,
|
||||
MaxLossStreak = universalConfig.MaxLossStreak,
|
||||
Scenario = universalConfig.Scenario,
|
||||
ScenarioName = universalConfig.ScenarioName,
|
||||
MoneyManagement = mmVariant.MoneyManagement,
|
||||
MaxPositionTimeHours = universalConfig.MaxPositionTimeHours,
|
||||
CloseEarlyWhenProfitable = universalConfig.CloseEarlyWhenProfitable,
|
||||
FlipOnlyWhenInProfit = universalConfig.FlipOnlyWhenInProfit,
|
||||
UseSynthApi = universalConfig.UseSynthApi,
|
||||
UseForPositionSizing = universalConfig.UseForPositionSizing,
|
||||
UseForSignalFiltering = universalConfig.UseForSignalFiltering,
|
||||
UseForDynamicStopLoss = universalConfig.UseForDynamicStopLoss
|
||||
};
|
||||
|
||||
var backtestRequest = new RunBacktestRequest
|
||||
{
|
||||
Config = config,
|
||||
StartDate = dateRange.StartDate,
|
||||
EndDate = dateRange.EndDate,
|
||||
Balance = universalConfig.BotTradingBalance,
|
||||
WatchOnly = universalConfig.WatchOnly,
|
||||
Save = universalConfig.Save,
|
||||
WithCandles = false, // Bundle backtests never return candles
|
||||
MoneyManagement = mmVariant.MoneyManagement != null
|
||||
? new MoneyManagement
|
||||
{
|
||||
Name = mmVariant.MoneyManagement.Name,
|
||||
Timeframe = mmVariant.MoneyManagement.Timeframe,
|
||||
StopLoss = mmVariant.MoneyManagement.StopLoss,
|
||||
TakeProfit = mmVariant.MoneyManagement.TakeProfit,
|
||||
Leverage = mmVariant.MoneyManagement.Leverage
|
||||
}
|
||||
: null
|
||||
};
|
||||
|
||||
backtestRequests.Add(backtestRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return backtestRequests;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error generating backtest requests from variants for bundle request {RequestId}",
|
||||
bundleRequest.RequestId);
|
||||
return new List<RunBacktestRequest>();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessSingleBacktest(
|
||||
IBacktester backtester,
|
||||
RunBacktestRequest runBacktestRequest,
|
||||
BundleBacktestRequest bundleRequest,
|
||||
int index)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Calculate total count from the variant configuration
|
||||
var totalCount = bundleRequest.TotalBacktests;
|
||||
|
||||
// Update current backtest being processed
|
||||
bundleRequest.CurrentBacktest = $"Backtest {index + 1} of {totalCount}";
|
||||
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
||||
|
||||
bundleRequest.User.Accounts = await ServiceScopeHelpers.WithScopedService<IAccountService, List<Account>>(
|
||||
_scopeFactory,
|
||||
async service => { return (await service.GetAccountsByUserAsync(bundleRequest.User, true)).ToList(); });
|
||||
// Run the backtest directly with the strongly-typed request
|
||||
var backtestId = await RunSingleBacktest(backtester, runBacktestRequest, bundleRequest, index);
|
||||
if (!string.IsNullOrEmpty(backtestId))
|
||||
{
|
||||
bundleRequest.Results.Add(backtestId);
|
||||
}
|
||||
|
||||
// Update progress
|
||||
bundleRequest.CompletedBacktests++;
|
||||
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
||||
|
||||
_logger.LogInformation("Completed backtest {Index} for bundle request {RequestId}",
|
||||
index + 1, bundleRequest.RequestId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing backtest {Index} for bundle request {RequestId}",
|
||||
index + 1, bundleRequest.RequestId);
|
||||
bundleRequest.FailedBacktests++;
|
||||
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
||||
SentrySdk.CaptureException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> RunSingleBacktest(
|
||||
IBacktester backtester,
|
||||
RunBacktestRequest runBacktestRequest,
|
||||
BundleBacktestRequest bundleRequest,
|
||||
int index)
|
||||
{
|
||||
if (runBacktestRequest?.Config == null)
|
||||
{
|
||||
_logger.LogError("Invalid RunBacktestRequest in bundle (null config)");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Map MoneyManagement
|
||||
MoneyManagement moneyManagement = null;
|
||||
if (!string.IsNullOrEmpty(runBacktestRequest.Config.MoneyManagementName))
|
||||
{
|
||||
_logger.LogWarning("MoneyManagementName provided but cannot resolve in grain context: {Name}",
|
||||
runBacktestRequest.Config.MoneyManagementName);
|
||||
}
|
||||
else if (runBacktestRequest.Config.MoneyManagement != null)
|
||||
{
|
||||
var mmReq = runBacktestRequest.Config.MoneyManagement;
|
||||
moneyManagement = new MoneyManagement
|
||||
{
|
||||
Name = mmReq.Name,
|
||||
Timeframe = mmReq.Timeframe,
|
||||
StopLoss = mmReq.StopLoss,
|
||||
TakeProfit = mmReq.TakeProfit,
|
||||
Leverage = mmReq.Leverage
|
||||
};
|
||||
moneyManagement.FormatPercentage();
|
||||
}
|
||||
|
||||
// Map Scenario
|
||||
LightScenario scenario = null;
|
||||
if (runBacktestRequest.Config.Scenario != null)
|
||||
{
|
||||
var sReq = runBacktestRequest.Config.Scenario;
|
||||
scenario = new LightScenario(sReq.Name, sReq.LoopbackPeriod)
|
||||
{
|
||||
Indicators = sReq.Indicators?.Select(i => new LightIndicator(i.Name, i.Type)
|
||||
{
|
||||
MinimumHistory = i.MinimumHistory,
|
||||
Period = i.Period,
|
||||
FastPeriods = i.FastPeriods,
|
||||
SlowPeriods = i.SlowPeriods,
|
||||
SignalPeriods = i.SignalPeriods,
|
||||
Multiplier = i.Multiplier,
|
||||
SmoothPeriods = i.SmoothPeriods,
|
||||
StochPeriods = i.StochPeriods,
|
||||
CyclePeriods = i.CyclePeriods
|
||||
}).ToList() ?? new List<LightIndicator>()
|
||||
};
|
||||
}
|
||||
|
||||
// Map TradingBotConfig
|
||||
var backtestConfig = new TradingBotConfig
|
||||
{
|
||||
AccountName = runBacktestRequest.Config.AccountName,
|
||||
MoneyManagement = moneyManagement,
|
||||
Ticker = runBacktestRequest.Config.Ticker,
|
||||
ScenarioName = runBacktestRequest.Config.ScenarioName,
|
||||
Scenario = scenario,
|
||||
Timeframe = runBacktestRequest.Config.Timeframe,
|
||||
IsForWatchingOnly = runBacktestRequest.Config.IsForWatchingOnly,
|
||||
BotTradingBalance = runBacktestRequest.Config.BotTradingBalance,
|
||||
IsForBacktest = true,
|
||||
CooldownPeriod = runBacktestRequest.Config.CooldownPeriod ?? 1,
|
||||
MaxLossStreak = runBacktestRequest.Config.MaxLossStreak,
|
||||
MaxPositionTimeHours = runBacktestRequest.Config.MaxPositionTimeHours,
|
||||
FlipOnlyWhenInProfit = runBacktestRequest.Config.FlipOnlyWhenInProfit,
|
||||
FlipPosition = runBacktestRequest.Config.FlipPosition,
|
||||
Name = $"{bundleRequest.Name} #{index + 1}",
|
||||
CloseEarlyWhenProfitable = runBacktestRequest.Config.CloseEarlyWhenProfitable,
|
||||
UseSynthApi = runBacktestRequest.Config.UseSynthApi,
|
||||
UseForPositionSizing = runBacktestRequest.Config.UseForPositionSizing,
|
||||
UseForSignalFiltering = runBacktestRequest.Config.UseForSignalFiltering,
|
||||
UseForDynamicStopLoss = runBacktestRequest.Config.UseForDynamicStopLoss
|
||||
};
|
||||
|
||||
// Run the backtest
|
||||
var result = await backtester.RunTradingBotBacktest(
|
||||
backtestConfig,
|
||||
runBacktestRequest.StartDate,
|
||||
runBacktestRequest.EndDate,
|
||||
bundleRequest.User,
|
||||
true,
|
||||
runBacktestRequest.WithCandles,
|
||||
bundleRequest.RequestId.ToString()
|
||||
);
|
||||
|
||||
_logger.LogInformation("Processed backtest for bundle request {RequestId}", bundleRequest.RequestId);
|
||||
return result.Id;
|
||||
}
|
||||
|
||||
private async Task UpdateFinalStatus(
|
||||
BundleBacktestRequest bundleRequest,
|
||||
IBacktester backtester,
|
||||
IMessengerService messengerService)
|
||||
{
|
||||
if (bundleRequest.FailedBacktests == 0)
|
||||
{
|
||||
bundleRequest.Status = BundleBacktestRequestStatus.Completed;
|
||||
await NotifyUser(bundleRequest, messengerService);
|
||||
}
|
||||
else if (bundleRequest.CompletedBacktests == 0)
|
||||
{
|
||||
bundleRequest.Status = BundleBacktestRequestStatus.Failed;
|
||||
bundleRequest.ErrorMessage = "All backtests failed";
|
||||
}
|
||||
else
|
||||
{
|
||||
bundleRequest.Status = BundleBacktestRequestStatus.Completed;
|
||||
bundleRequest.ErrorMessage = $"{bundleRequest.FailedBacktests} backtests failed";
|
||||
await NotifyUser(bundleRequest, messengerService);
|
||||
}
|
||||
|
||||
bundleRequest.CompletedAt = DateTime.UtcNow;
|
||||
bundleRequest.CurrentBacktest = null;
|
||||
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
||||
|
||||
// Unregister retry reminder since bundle completed
|
||||
await UnregisterRetryReminder();
|
||||
}
|
||||
|
||||
private async Task HandleBundleRequestError(
|
||||
BundleBacktestRequest bundleRequest,
|
||||
IBacktester backtester,
|
||||
Exception ex)
|
||||
{
|
||||
bundleRequest.Status = BundleBacktestRequestStatus.Failed;
|
||||
bundleRequest.ErrorMessage = ex.Message;
|
||||
bundleRequest.CompletedAt = DateTime.UtcNow;
|
||||
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
||||
|
||||
// Register retry reminder for failed bundle
|
||||
await RegisterRetryReminder();
|
||||
}
|
||||
|
||||
private async Task NotifyUser(BundleBacktestRequest bundleRequest, IMessengerService messengerService)
|
||||
{
|
||||
if (bundleRequest.User?.TelegramChannel != null)
|
||||
{
|
||||
var message = bundleRequest.FailedBacktests == 0
|
||||
? $"✅ Bundle backtest '{bundleRequest.Name}' (ID: {bundleRequest.RequestId}) completed successfully."
|
||||
: $"⚠️ Bundle backtest '{bundleRequest.Name}' (ID: {bundleRequest.RequestId}) completed with {bundleRequest.FailedBacktests} failed backtests.";
|
||||
|
||||
await messengerService.SendMessage(message, bundleRequest.User.TelegramChannel);
|
||||
}
|
||||
}
|
||||
|
||||
#region IRemindable Implementation
|
||||
|
||||
/// <summary>
|
||||
/// Handles reminder callbacks for automatic retry of failed bundle backtests
|
||||
/// </summary>
|
||||
public async Task ReceiveReminder(string reminderName, TickStatus status)
|
||||
{
|
||||
if (reminderName != RETRY_REMINDER_NAME)
|
||||
{
|
||||
_logger.LogWarning("Unknown reminder {ReminderName} received", reminderName);
|
||||
return;
|
||||
}
|
||||
|
||||
var bundleRequestId = this.GetPrimaryKey();
|
||||
_logger.LogInformation("Retry reminder triggered for bundle request {RequestId}", bundleRequestId);
|
||||
|
||||
try
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var backtester = scope.ServiceProvider.GetRequiredService<IBacktester>();
|
||||
|
||||
// Get the bundle request
|
||||
var bundleRequest = await GetBundleRequestById(backtester, bundleRequestId);
|
||||
if (bundleRequest == null)
|
||||
{
|
||||
_logger.LogWarning("Bundle request {RequestId} not found during retry", bundleRequestId);
|
||||
await UnregisterRetryReminder();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if bundle is still failed
|
||||
if (bundleRequest.Status != BundleBacktestRequestStatus.Failed)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Bundle request {RequestId} is no longer failed (status: {Status}), unregistering reminder",
|
||||
bundleRequestId, bundleRequest.Status);
|
||||
await UnregisterRetryReminder();
|
||||
return;
|
||||
}
|
||||
|
||||
// Retry the bundle processing
|
||||
_logger.LogInformation("Retrying failed bundle request {RequestId}", bundleRequestId);
|
||||
|
||||
// Reset status to pending for retry
|
||||
bundleRequest.Status = BundleBacktestRequestStatus.Pending;
|
||||
bundleRequest.ErrorMessage = null;
|
||||
bundleRequest.CurrentBacktest = null;
|
||||
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
||||
|
||||
// Process the bundle again
|
||||
await ProcessBundleRequestAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during bundle backtest retry for request {RequestId}", bundleRequestId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a retry reminder for this bundle request
|
||||
/// </summary>
|
||||
private async Task RegisterRetryReminder()
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.RegisterOrUpdateReminder(RETRY_REMINDER_NAME, RETRY_INTERVAL, RETRY_INTERVAL);
|
||||
_logger.LogInformation("Registered retry reminder for bundle request {RequestId}", this.GetPrimaryKey());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to register retry reminder for bundle request {RequestId}",
|
||||
this.GetPrimaryKey());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters the retry reminder for this bundle request
|
||||
/// </summary>
|
||||
private async Task UnregisterRetryReminder()
|
||||
{
|
||||
try
|
||||
{
|
||||
var reminder = await this.GetReminder(RETRY_REMINDER_NAME);
|
||||
if (reminder != null)
|
||||
{
|
||||
await this.UnregisterReminder(reminder);
|
||||
_logger.LogInformation("Unregistered retry reminder for bundle request {RequestId}",
|
||||
this.GetPrimaryKey());
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to unregister retry reminder for bundle request {RequestId}",
|
||||
this.GetPrimaryKey());
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
using Managing.Application.Abstractions.Grains;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Orleans;
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Accounts;
|
||||
using Managing.Domain.Backtests;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Orleans.Concurrency;
|
||||
|
||||
namespace Managing.Application.Grains;
|
||||
|
||||
/// <summary>
|
||||
/// Stateless worker grain for processing genetic backtest requests.
|
||||
/// Uses the genetic request ID (string) as the primary key.
|
||||
/// Uses custom compute placement with random fallback.
|
||||
/// </summary>
|
||||
[StatelessWorker]
|
||||
[TradingPlacement] // Use custom compute placement with random fallback
|
||||
public class GeneticBacktestGrain : Grain, IGeneticBacktestGrain
|
||||
{
|
||||
private readonly ILogger<GeneticBacktestGrain> _logger;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
|
||||
public GeneticBacktestGrain(
|
||||
ILogger<GeneticBacktestGrain> logger,
|
||||
IServiceScopeFactory scopeFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_scopeFactory = scopeFactory;
|
||||
}
|
||||
|
||||
public async Task ProcessGeneticRequestAsync()
|
||||
{
|
||||
var requestId = this.GetPrimaryKeyString();
|
||||
|
||||
try
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var geneticService = scope.ServiceProvider.GetRequiredService<IGeneticService>();
|
||||
|
||||
// Load the request by status lists and filter by ID (Pending first, then Failed for retries)
|
||||
var pending = await geneticService.GetGeneticRequestsAsync(GeneticRequestStatus.Pending);
|
||||
var failed = await geneticService.GetGeneticRequestsAsync(GeneticRequestStatus.Failed);
|
||||
var request = pending.Concat(failed).FirstOrDefault(r => r.RequestId == requestId);
|
||||
|
||||
if (request == null)
|
||||
{
|
||||
_logger.LogWarning("[GeneticBacktestGrain] Request {RequestId} not found among pending/failed.",
|
||||
requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark running
|
||||
request.Status = GeneticRequestStatus.Running;
|
||||
await geneticService.UpdateGeneticRequestAsync(request);
|
||||
|
||||
request.User.Accounts = await ServiceScopeHelpers.WithScopedService<IAccountService, List<Account>>(
|
||||
_scopeFactory,
|
||||
async accountService => (await accountService.GetAccountsByUserAsync(request.User)).ToList());
|
||||
|
||||
// Run GA
|
||||
var result = await geneticService.RunGeneticAlgorithm(request, CancellationToken.None);
|
||||
|
||||
// Update final state
|
||||
request.Status = GeneticRequestStatus.Completed;
|
||||
request.CompletedAt = DateTime.UtcNow;
|
||||
request.BestFitness = result.BestFitness;
|
||||
request.BestIndividual = result.BestIndividual;
|
||||
request.ProgressInfo = result.ProgressInfo;
|
||||
await geneticService.UpdateGeneticRequestAsync(request);
|
||||
|
||||
_logger.LogInformation("[GeneticBacktestGrain] Completed request {RequestId}", requestId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SentrySdk.CaptureException(ex);
|
||||
|
||||
try
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var geneticService = scope.ServiceProvider.GetRequiredService<IGeneticService>();
|
||||
var running = await geneticService.GetGeneticRequestsAsync(GeneticRequestStatus.Running);
|
||||
var req = running.FirstOrDefault(r => r.RequestId == requestId) ?? new GeneticRequest(requestId);
|
||||
req.Status = GeneticRequestStatus.Failed;
|
||||
req.ErrorMessage = ex.Message;
|
||||
req.CompletedAt = DateTime.UtcNow;
|
||||
await geneticService.UpdateGeneticRequestAsync(req);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,7 +187,7 @@ namespace Managing.Application.ManageBot
|
||||
MasterBotUserId = masterBot.User.Id,
|
||||
|
||||
// Set computed/default properties
|
||||
IsForBacktest = false,
|
||||
TradingType = TradingType.Futures,
|
||||
Name = masterConfig.Name
|
||||
};
|
||||
|
||||
|
||||
@@ -61,16 +61,19 @@ public class ScenarioRunnerGrain : Grain, IScenarioRunnerGrain
|
||||
try
|
||||
{
|
||||
var candlesHashSet = await GetCandlesAsync(tradingExchanges, config);
|
||||
if (candlesHashSet.LastOrDefault()!.Date <= candle.Date)
|
||||
// Convert to ordered List to preserve chronological order for indicators
|
||||
var candlesList = candlesHashSet.OrderBy(c => c.Date).ToList();
|
||||
|
||||
if (candlesList.LastOrDefault()!.Date <= candle.Date)
|
||||
{
|
||||
_logger.LogWarning($"No new candles for {config.Ticker} for {config.Name}");
|
||||
return null; // No new candles, no need to generate a signal
|
||||
}
|
||||
|
||||
_logger.LogInformation($"Fetched {candlesHashSet.Count} candles for {config.Ticker} for {config.Name}");
|
||||
_logger.LogInformation($"Fetched {candlesList.Count} candles for {config.Ticker} for {config.Name}");
|
||||
|
||||
var signal = TradingBox.GetSignal(
|
||||
candlesHashSet,
|
||||
candlesList,
|
||||
config.Scenario,
|
||||
previousSignals,
|
||||
config.Scenario?.LoopbackPeriod ?? 1);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using Managing.Domain.Trades;
|
||||
using MediatR;
|
||||
|
||||
namespace Managing.Application.Trading.Commands
|
||||
{
|
||||
public class CloseBacktestFuturesPositionCommand : IRequest<Position>
|
||||
{
|
||||
public CloseBacktestFuturesPositionCommand(Position position, int accountId, decimal? executionPrice = null)
|
||||
{
|
||||
Position = position;
|
||||
AccountId = accountId;
|
||||
ExecutionPrice = executionPrice;
|
||||
}
|
||||
|
||||
public Position Position { get; }
|
||||
public int AccountId { get; }
|
||||
public decimal? ExecutionPrice { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using Managing.Domain.Trades;
|
||||
using MediatR;
|
||||
|
||||
namespace Managing.Application.Trading.Commands
|
||||
{
|
||||
public class CloseFuturesPositionCommand : IRequest<Position>
|
||||
{
|
||||
public CloseFuturesPositionCommand(Position position, int accountId, decimal? executionPrice = null)
|
||||
{
|
||||
Position = position;
|
||||
AccountId = accountId;
|
||||
ExecutionPrice = executionPrice;
|
||||
}
|
||||
|
||||
public Position Position { get; }
|
||||
public int AccountId { get; }
|
||||
public decimal? ExecutionPrice { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ namespace Managing.Application.Trading.Commands
|
||||
bool isForPaperTrading = false,
|
||||
decimal? price = null,
|
||||
string signalIdentifier = null,
|
||||
Guid? initiatorIdentifier = null)
|
||||
Guid? initiatorIdentifier = null,
|
||||
TradingType tradingType = TradingType.Futures)
|
||||
{
|
||||
AccountName = accountName;
|
||||
MoneyManagement = moneyManagement;
|
||||
@@ -43,6 +44,7 @@ namespace Managing.Application.Trading.Commands
|
||||
InitiatorIdentifier = initiatorIdentifier ??
|
||||
throw new ArgumentNullException(nameof(initiatorIdentifier),
|
||||
"InitiatorIdentifier is required");
|
||||
TradingType = tradingType;
|
||||
}
|
||||
|
||||
public string SignalIdentifier { get; set; }
|
||||
@@ -57,5 +59,6 @@ namespace Managing.Application.Trading.Commands
|
||||
public PositionInitiator Initiator { get; }
|
||||
public User User { get; }
|
||||
public Guid InitiatorIdentifier { get; }
|
||||
public TradingType TradingType { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Trading.Commands;
|
||||
using Managing.Common;
|
||||
using Managing.Domain.Shared.Helpers;
|
||||
using Managing.Domain.Trades;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Trading.Handlers;
|
||||
|
||||
public class CloseBacktestFuturesPositionCommandHandler(
|
||||
IExchangeService exchangeService,
|
||||
IAccountService accountService,
|
||||
ITradingService tradingService,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<CloseBacktestFuturesPositionCommandHandler> logger = null)
|
||||
: ICommandHandler<CloseBacktestFuturesPositionCommand, Position>
|
||||
{
|
||||
public async Task<Position> Handle(CloseBacktestFuturesPositionCommand request)
|
||||
{
|
||||
try
|
||||
{
|
||||
// For backtest, use execution price directly
|
||||
var lastPrice = request.ExecutionPrice.GetValueOrDefault();
|
||||
|
||||
// Calculate closing direction (opposite of opening direction)
|
||||
var direction = request.Position.OriginDirection == TradeDirection.Long
|
||||
? TradeDirection.Short
|
||||
: TradeDirection.Long;
|
||||
|
||||
// Build the closing trade directly for backtest (no exchange call needed)
|
||||
var closedTrade = exchangeService.BuildEmptyTrade(
|
||||
request.Position.Open.Ticker,
|
||||
lastPrice,
|
||||
request.Position.Open.Quantity,
|
||||
direction,
|
||||
request.Position.Open.Leverage,
|
||||
TradeType.Market,
|
||||
request.Position.Open.Date,
|
||||
TradeStatus.Filled);
|
||||
|
||||
// Update position status and calculate PnL
|
||||
request.Position.Status = PositionStatus.Finished;
|
||||
request.Position.ProfitAndLoss =
|
||||
TradingBox.GetProfitAndLoss(request.Position, closedTrade.Quantity, lastPrice,
|
||||
request.Position.Open.Leverage);
|
||||
|
||||
// Add UI fees for closing the position
|
||||
var closingPositionSizeUsd = (lastPrice * closedTrade.Quantity) * request.Position.Open.Leverage;
|
||||
var closingUiFees = TradingBox.CalculateClosingUiFees(closingPositionSizeUsd);
|
||||
request.Position.AddUiFees(closingUiFees);
|
||||
request.Position.AddGasFees(Constants.GMX.Config.GasFeePerTransaction);
|
||||
|
||||
// For backtest, skip database update
|
||||
|
||||
return request.Position;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogError(ex, "Error closing backtest futures position: {Message} \n Stacktrace : {StackTrace}", ex.Message,
|
||||
ex.StackTrace);
|
||||
|
||||
SentrySdk.CaptureException(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Trading.Commands;
|
||||
using Managing.Common;
|
||||
using Managing.Domain.Accounts;
|
||||
using Managing.Domain.Shared.Helpers;
|
||||
using Managing.Domain.Trades;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Trading.Handlers;
|
||||
|
||||
public class CloseFuturesPositionCommandHandler(
|
||||
IExchangeService exchangeService,
|
||||
IAccountService accountService,
|
||||
ITradingService tradingService,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<CloseFuturesPositionCommandHandler> logger = null)
|
||||
: ICommandHandler<CloseFuturesPositionCommand, Position>
|
||||
{
|
||||
public async Task<Position> Handle(CloseFuturesPositionCommand request)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (request.Position == null)
|
||||
{
|
||||
logger?.LogWarning("Attempted to close position but position is null for account {AccountId}", request.AccountId);
|
||||
throw new ArgumentNullException(nameof(request.Position), "Position cannot be null for closing");
|
||||
}
|
||||
|
||||
// This handler should ONLY handle live trading positions
|
||||
// Backtest/paper trading positions must use CloseBacktestFuturesPositionCommandHandler
|
||||
if (request.Position.TradingType == TradingType.BacktestFutures ||
|
||||
request.Position.Initiator == PositionInitiator.PaperTrading)
|
||||
{
|
||||
logger?.LogError(
|
||||
"CloseFuturesPositionCommandHandler received a backtest/paper trading position. " +
|
||||
"Position: {PositionId}, TradingType: {TradingType}, Initiator: {Initiator}. " +
|
||||
"Use CloseBacktestFuturesPositionCommandHandler instead.",
|
||||
request.Position.Identifier, request.Position.TradingType, request.Position.Initiator);
|
||||
throw new InvalidOperationException(
|
||||
$"CloseFuturesPositionCommandHandler cannot handle backtest/paper trading positions. " +
|
||||
$"Position {request.Position.Identifier} has TradingType={request.Position.TradingType} and Initiator={request.Position.Initiator}. " +
|
||||
$"Use CloseBacktestFuturesPositionCommandHandler instead.");
|
||||
}
|
||||
|
||||
Account account = await accountService.GetAccountById(request.AccountId, false, false);
|
||||
|
||||
// For live trading, always get price from exchange
|
||||
var lastPrice = await exchangeService.GetPrice(account, request.Position.Ticker, DateTime.UtcNow);
|
||||
|
||||
// Check if position still open on broker
|
||||
var p = (await exchangeService.GetBrokerPositions(account))
|
||||
.FirstOrDefault(x => x.Ticker == request.Position.Ticker);
|
||||
|
||||
// Position not available on the broker, so be sure to update the status
|
||||
if (p == null)
|
||||
{
|
||||
request.Position.Status = PositionStatus.Finished;
|
||||
request.Position.ProfitAndLoss =
|
||||
TradingBox.GetProfitAndLoss(request.Position, request.Position.Open.Quantity, lastPrice,
|
||||
request.Position.Open.Leverage);
|
||||
|
||||
// Add UI fees for closing the position (broker closed it)
|
||||
var closingPositionSizeUsd =
|
||||
(lastPrice * request.Position.Open.Quantity) * request.Position.Open.Leverage;
|
||||
var closingUiFees = TradingBox.CalculateClosingUiFees(closingPositionSizeUsd);
|
||||
request.Position.AddUiFees(closingUiFees);
|
||||
request.Position.AddGasFees(Constants.GMX.Config.GasFeePerTransaction);
|
||||
|
||||
await tradingService.UpdatePositionAsync(request.Position);
|
||||
return request.Position;
|
||||
}
|
||||
|
||||
var closeRequestedOrders = true; // TODO: For gmx no need to close orders since they are closed automatically
|
||||
|
||||
// Close market
|
||||
var closedPosition =
|
||||
await exchangeService.ClosePosition(account, request.Position, lastPrice);
|
||||
|
||||
if (closeRequestedOrders || closedPosition.Status == (TradeStatus.PendingOpen | TradeStatus.Filled))
|
||||
{
|
||||
request.Position.Status = PositionStatus.Finished;
|
||||
request.Position.ProfitAndLoss =
|
||||
TradingBox.GetProfitAndLoss(request.Position, closedPosition.Quantity, lastPrice,
|
||||
request.Position.Open.Leverage);
|
||||
|
||||
// Add UI fees for closing the position
|
||||
var closingPositionSizeUsd = (lastPrice * closedPosition.Quantity) * request.Position.Open.Leverage;
|
||||
var closingUiFees = TradingBox.CalculateClosingUiFees(closingPositionSizeUsd);
|
||||
request.Position.AddUiFees(closingUiFees);
|
||||
request.Position.AddGasFees(Constants.GMX.Config.GasFeePerTransaction);
|
||||
|
||||
await tradingService.UpdatePositionAsync(request.Position);
|
||||
}
|
||||
|
||||
return request.Position;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogError(ex, "Error closing futures position: {Message} \n Stacktrace : {StackTrace}", ex.Message,
|
||||
ex.StackTrace);
|
||||
|
||||
SentrySdk.CaptureException(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,6 @@ public class ClosePositionCommandHandler(
|
||||
}
|
||||
}
|
||||
|
||||
var isForPaperTrading = request.IsForBacktest;
|
||||
|
||||
var lastPrice = request.Position.Initiator == PositionInitiator.PaperTrading
|
||||
? request.ExecutionPrice.GetValueOrDefault()
|
||||
: await exchangeService.GetPrice(account, request.Position.Ticker, DateTime.UtcNow);
|
||||
@@ -72,7 +70,7 @@ public class ClosePositionCommandHandler(
|
||||
|
||||
// Close market
|
||||
var closedPosition =
|
||||
await exchangeService.ClosePosition(account, request.Position, lastPrice, isForPaperTrading);
|
||||
await exchangeService.ClosePosition(account, request.Position, lastPrice);
|
||||
|
||||
if (closeRequestedOrders || closedPosition.Status == (TradeStatus.PendingOpen | TradeStatus.Filled))
|
||||
{
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace Managing.Application.Trading.Handlers
|
||||
}
|
||||
|
||||
position.InitiatorIdentifier = request.InitiatorIdentifier;
|
||||
position.TradingType = request.TradingType;
|
||||
|
||||
// Always use BotTradingBalance directly as the balance to risk
|
||||
// Round to 2 decimal places to prevent precision errors
|
||||
|
||||
@@ -287,7 +287,7 @@ public class StatisticService : IStatisticService
|
||||
Timeframe = timeframe,
|
||||
IsForWatchingOnly = true,
|
||||
BotTradingBalance = 1000,
|
||||
IsForBacktest = true,
|
||||
TradingType = TradingType.BacktestFutures,
|
||||
CooldownPeriod = 1,
|
||||
MaxLossStreak = 0,
|
||||
FlipPosition = false,
|
||||
|
||||
@@ -280,12 +280,15 @@ public class BacktestComputeWorker : BackgroundService
|
||||
var timeoutCts = new CancellationTokenSource(TimeSpan.FromMinutes(_options.JobTimeoutMinutes));
|
||||
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
|
||||
|
||||
// Convert HashSet to List - candles are already ordered from repository
|
||||
var candlesList = candles.ToList();
|
||||
|
||||
LightBacktest result;
|
||||
try
|
||||
{
|
||||
result = await executor.ExecuteAsync(
|
||||
config,
|
||||
candles,
|
||||
candlesList,
|
||||
user,
|
||||
save: true,
|
||||
withCandles: false,
|
||||
|
||||
@@ -253,7 +253,7 @@ public class BundleBacktestWorker : BaseWorker<BundleBacktestWorker>
|
||||
Timeframe = runBacktestRequest.Config.Timeframe,
|
||||
IsForWatchingOnly = runBacktestRequest.Config.IsForWatchingOnly,
|
||||
BotTradingBalance = runBacktestRequest.Config.BotTradingBalance,
|
||||
IsForBacktest = true,
|
||||
TradingType = TradingType.BacktestFutures,
|
||||
CooldownPeriod = runBacktestRequest.Config.CooldownPeriod ?? 1,
|
||||
MaxLossStreak = runBacktestRequest.Config.MaxLossStreak,
|
||||
MaxPositionTimeHours = runBacktestRequest.Config.MaxPositionTimeHours,
|
||||
|
||||
Reference in New Issue
Block a user