Improve perf for backtests
This commit is contained in:
@@ -3,13 +3,16 @@ using Managing.Application.Abstractions.Repositories;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Bots;
|
||||
using Managing.Common;
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Backtests;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Shared.Helpers;
|
||||
using Managing.Domain.Strategies.Base;
|
||||
using Managing.Domain.Users;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Backtests;
|
||||
|
||||
@@ -76,7 +79,7 @@ public class BacktestExecutor
|
||||
}
|
||||
|
||||
// Create a fresh TradingBotBase instance for this backtest
|
||||
var tradingBot = await CreateTradingBotInstance(config);
|
||||
var tradingBot = CreateTradingBotInstance(config);
|
||||
tradingBot.Account = user.Accounts.First();
|
||||
|
||||
var totalCandles = candles.Count;
|
||||
@@ -86,6 +89,41 @@ public class BacktestExecutor
|
||||
_logger.LogInformation("Backtest requested by {UserId} with {TotalCandles} candles for {Ticker} on {Timeframe}",
|
||||
user.Id, totalCandles, config.Ticker, config.Timeframe);
|
||||
|
||||
// Pre-calculate indicator values once for all candles to optimize performance
|
||||
// This avoids recalculating indicators for every candle iteration
|
||||
Dictionary<IndicatorType, IndicatorsResultBase> preCalculatedIndicatorValues = null;
|
||||
if (config.Scenario != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Pre-calculating indicator values for {IndicatorCount} indicators",
|
||||
config.Scenario.Indicators?.Count ?? 0);
|
||||
|
||||
// Convert LightScenario to Scenario for CalculateIndicatorsValuesAsync
|
||||
var scenario = config.Scenario.ToScenario();
|
||||
|
||||
// Calculate all indicator values once with all candles
|
||||
preCalculatedIndicatorValues = await ServiceScopeHelpers.WithScopedService<ITradingService, Dictionary<IndicatorType, IndicatorsResultBase>>(
|
||||
_scopeFactory,
|
||||
async tradingService =>
|
||||
{
|
||||
return await tradingService.CalculateIndicatorsValuesAsync(scenario, candles);
|
||||
});
|
||||
|
||||
// Store pre-calculated values in trading bot for use during signal generation
|
||||
tradingBot.PreCalculatedIndicatorValues = preCalculatedIndicatorValues;
|
||||
|
||||
_logger.LogInformation("Successfully pre-calculated indicator values for {IndicatorCount} indicator types",
|
||||
preCalculatedIndicatorValues?.Count ?? 0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to pre-calculate indicator values, will calculate on-the-fly. Error: {ErrorMessage}", ex.Message);
|
||||
// Continue with normal calculation if pre-calculation fails
|
||||
preCalculatedIndicatorValues = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize wallet balance with first candle
|
||||
tradingBot.WalletBalances.Clear();
|
||||
tradingBot.WalletBalances.Add(candles.FirstOrDefault()!.Date, config.BotTradingBalance);
|
||||
@@ -239,7 +277,7 @@ public class BacktestExecutor
|
||||
/// <summary>
|
||||
/// Creates a TradingBotBase instance for backtesting
|
||||
/// </summary>
|
||||
private async Task<TradingBotBase> CreateTradingBotInstance(TradingBotConfig config)
|
||||
private TradingBotBase CreateTradingBotInstance(TradingBotConfig config)
|
||||
{
|
||||
// Validate configuration for backtesting
|
||||
if (config == null)
|
||||
|
||||
@@ -139,41 +139,6 @@ namespace Managing.Application.Backtests
|
||||
return await RunTradingBotBacktest(config, startDate, endDate, user, false, withCandles, requestId, metadata);
|
||||
}
|
||||
|
||||
// Removed RunBacktestWithCandles - backtests now run via compute workers
|
||||
// This method is kept for backward compatibility but should not be called directly
|
||||
|
||||
private async Task<HashSet<Candle>> GetCandles(Ticker ticker, Timeframe timeframe,
|
||||
DateTime startDate, DateTime endDate)
|
||||
{
|
||||
var candles = await _exchangeService.GetCandlesInflux(TradingExchanges.Evm, ticker,
|
||||
startDate, timeframe, endDate);
|
||||
|
||||
if (candles == null || candles.Count == 0)
|
||||
throw new Exception(
|
||||
$"No candles for {ticker} on {timeframe} timeframe for start {startDate} to end {endDate}");
|
||||
|
||||
return candles;
|
||||
}
|
||||
|
||||
|
||||
// Removed CreateCleanConfigForOrleans - no longer needed with job queue approach
|
||||
|
||||
private async Task SendBacktestNotificationIfCriteriaMet(Backtest backtest)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (backtest.Score > 60)
|
||||
{
|
||||
await _messengerService.SendBacktestNotification(backtest);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to send backtest notification for backtest {Id}", backtest.Id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> DeleteBacktestAsync(string id)
|
||||
{
|
||||
try
|
||||
@@ -243,7 +208,6 @@ namespace Managing.Application.Backtests
|
||||
return (backtests, totalCount);
|
||||
}
|
||||
|
||||
|
||||
public async Task<Backtest> GetBacktestByIdForUserAsync(User user, string id)
|
||||
{
|
||||
var backtest = await _backtestRepository.GetBacktestByIdForUserAsync(user, id);
|
||||
@@ -605,7 +569,5 @@ namespace Managing.Application.Backtests
|
||||
if (string.IsNullOrWhiteSpace(requestId) || response == null) return;
|
||||
await _hubContext.Clients.Group($"bundle-{requestId}").SendAsync("BundleBacktestUpdate", response);
|
||||
}
|
||||
|
||||
// Removed TriggerBundleBacktestGrain methods - bundle backtests now use job queue
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ using Managing.Domain.Indicators;
|
||||
using Managing.Domain.Scenarios;
|
||||
using Managing.Domain.Shared.Helpers;
|
||||
using Managing.Domain.Strategies;
|
||||
using Managing.Domain.Strategies.Base;
|
||||
using Managing.Domain.Synth.Models;
|
||||
using Managing.Domain.Trades;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -28,7 +29,9 @@ public class TradingBotBase : ITradingBot
|
||||
public readonly ILogger<TradingBotBase> Logger;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private const int NEW_POSITION_GRACE_SECONDS = 45; // grace window before evaluating missing orders
|
||||
private const int CLOSE_POSITION_GRACE_MS = 20000; // grace window before closing position to allow broker processing (20 seconds)
|
||||
|
||||
private const int
|
||||
CLOSE_POSITION_GRACE_MS = 20000; // grace window before closing position to allow broker processing (20 seconds)
|
||||
|
||||
public TradingBotConfig Config { get; set; }
|
||||
public Account Account { get; set; }
|
||||
@@ -42,6 +45,12 @@ public class TradingBotBase : ITradingBot
|
||||
public Candle LastCandle { get; set; }
|
||||
public DateTime? LastPositionClosingTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Pre-calculated indicator values for backtesting optimization.
|
||||
/// Key is IndicatorType, Value is the calculated indicator result.
|
||||
/// </summary>
|
||||
public Dictionary<IndicatorType, IndicatorsResultBase> PreCalculatedIndicatorValues { get; set; }
|
||||
|
||||
|
||||
public TradingBotBase(
|
||||
ILogger<TradingBotBase> logger,
|
||||
@@ -56,6 +65,7 @@ public class TradingBotBase : ITradingBot
|
||||
Positions = new Dictionary<Guid, Position>();
|
||||
WalletBalances = new Dictionary<DateTime, decimal>();
|
||||
PreloadSince = CandleHelpers.GetBotPreloadSinceFromTimeframe(config.Timeframe);
|
||||
PreCalculatedIndicatorValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
|
||||
}
|
||||
|
||||
public async Task Start(BotStatus previousStatus)
|
||||
@@ -269,7 +279,8 @@ public class TradingBotBase : ITradingBot
|
||||
if (Config.IsForBacktest && candles != null)
|
||||
{
|
||||
var backtestSignal =
|
||||
TradingBox.GetSignal(candles, Config.Scenario, Signals, Config.Scenario.LoopbackPeriod);
|
||||
TradingBox.GetSignal(candles, Config.Scenario, Signals, Config.Scenario.LoopbackPeriod,
|
||||
PreCalculatedIndicatorValues);
|
||||
if (backtestSignal == null) return;
|
||||
await AddSignal(backtestSignal);
|
||||
}
|
||||
@@ -768,7 +779,8 @@ public class TradingBotBase : ITradingBot
|
||||
await LogInformation(
|
||||
$"⏰ Time Limit Close\nClosing position due to time limit: `{Config.MaxPositionTimeHours}h` exceeded\n📈 Position Status: {profitStatus}\n💰 Entry: `${positionForSignal.Open.Price}` → Current: `${lastCandle.Close}`\n📊 Realized PNL: `${currentPnl:F2}` (`{pnlPercentage:F2}%`)");
|
||||
// Force a market close: compute PnL based on current price instead of SL/TP
|
||||
await CloseTrade(signal, positionForSignal, positionForSignal.Open, lastCandle.Close, true, true);
|
||||
await CloseTrade(signal, positionForSignal, positionForSignal.Open, lastCandle.Close, true,
|
||||
true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1355,7 +1367,8 @@ public class TradingBotBase : ITradingBot
|
||||
await SetPositionStatus(signal.Identifier, PositionStatus.Finished);
|
||||
}
|
||||
|
||||
await HandleClosedPosition(closedPosition, forceMarketClose ? lastPrice : (decimal?)null, forceMarketClose);
|
||||
await HandleClosedPosition(closedPosition, forceMarketClose ? lastPrice : (decimal?)null,
|
||||
forceMarketClose);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1371,13 +1384,15 @@ public class TradingBotBase : ITradingBot
|
||||
// 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);
|
||||
await HandleClosedPosition(position, forceMarketClose ? lastPrice : (decimal?)null,
|
||||
forceMarketClose);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleClosedPosition(Position position, decimal? forcedClosingPrice = null, bool forceMarketClose = false)
|
||||
private async Task HandleClosedPosition(Position position, decimal? forcedClosingPrice = null,
|
||||
bool forceMarketClose = false)
|
||||
{
|
||||
if (Positions.ContainsKey(position.Identifier))
|
||||
{
|
||||
|
||||
@@ -435,33 +435,37 @@ public class TradingService : ITradingService
|
||||
/// <param name="scenario">The scenario containing indicators.</param>
|
||||
/// <param name="candles">The candles to calculate indicators for.</param>
|
||||
/// <returns>A dictionary of indicator types to their calculated values.</returns>
|
||||
public Dictionary<IndicatorType, IndicatorsResultBase> CalculateIndicatorsValuesAsync(
|
||||
public async Task<Dictionary<IndicatorType, IndicatorsResultBase>> CalculateIndicatorsValuesAsync(
|
||||
Scenario scenario,
|
||||
HashSet<Candle> candles)
|
||||
{
|
||||
var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
|
||||
|
||||
if (scenario?.Indicators == null || scenario.Indicators.Count == 0)
|
||||
// Offload CPU-bound indicator calculations to thread pool
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
|
||||
|
||||
if (scenario?.Indicators == null || scenario.Indicators.Count == 0)
|
||||
{
|
||||
return indicatorsValues;
|
||||
}
|
||||
|
||||
// Build indicators from scenario
|
||||
foreach (var indicator in scenario.Indicators)
|
||||
{
|
||||
try
|
||||
{
|
||||
var buildedIndicator = ScenarioHelpers.BuildIndicator(ScenarioHelpers.BaseToLight(indicator));
|
||||
indicatorsValues[indicator.Type] = buildedIndicator.GetIndicatorValues(candles);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error calculating indicator {IndicatorName}: {ErrorMessage}",
|
||||
indicator.Name, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return indicatorsValues;
|
||||
}
|
||||
|
||||
// Build indicators from scenario
|
||||
foreach (var indicator in scenario.Indicators)
|
||||
{
|
||||
try
|
||||
{
|
||||
var buildedIndicator = ScenarioHelpers.BuildIndicator(ScenarioHelpers.BaseToLight(indicator));
|
||||
indicatorsValues[indicator.Type] = buildedIndicator.GetIndicatorValues(candles);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error calculating indicator {IndicatorName}: {ErrorMessage}",
|
||||
indicator.Name, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return indicatorsValues;
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<IndicatorBase?> GetIndicatorByNameUserAsync(string name, User user)
|
||||
|
||||
Reference in New Issue
Block a user