Files
managing-apps/src/Managing.Application/Backtesting/Backtester.cs
Oda a547c4a040 Add synthApi (#27)
* Add synthApi

* Put confidence for Synth proba

* Update the code

* Update readme

* Fix bootstraping

* fix github build

* Update the endpoints for scenario

* Add scenario and update backtest modal

* Update bot modal

* Update interfaces for synth

* add synth to backtest

* Add Kelly criterion and better signal

* Update signal confidence

* update doc

* save leaderboard and prediction

* Update nswag to generate ApiClient in the correct path

* Unify the trading modal

* Save miner and prediction

* Update messaging and block new signal until position not close when flipping off

* Rename strategies to indicators

* Update doc

* Update chart + add signal name

* Fix signal direction

* Update docker webui

* remove crypto npm

* Clean
2025-07-03 00:13:42 +07:00

439 lines
16 KiB
C#

using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Core.FixedSizedQueue;
using Managing.Domain.Accounts;
using Managing.Domain.Backtests;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers;
using Managing.Domain.Strategies;
using Managing.Domain.Strategies.Base;
using Managing.Domain.Users;
using Managing.Domain.Workflows;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Application.Backtesting
{
public class Backtester : IBacktester
{
private readonly IBacktestRepository _backtestRepository;
private readonly ILogger<Backtester> _logger;
private readonly IExchangeService _exchangeService;
private readonly IBotFactory _botFactory;
private readonly IScenarioService _scenarioService;
private readonly IAccountService _accountService;
public Backtester(
IExchangeService exchangeService,
IBotFactory botFactory,
IBacktestRepository backtestRepository,
ILogger<Backtester> logger,
IScenarioService scenarioService,
IAccountService accountService)
{
_exchangeService = exchangeService;
_botFactory = botFactory;
_backtestRepository = backtestRepository;
_logger = logger;
_scenarioService = scenarioService;
_accountService = accountService;
}
public Backtest RunSimpleBotBacktest(Workflow workflow, bool save = false)
{
var simplebot = _botFactory.CreateSimpleBot("scenario", workflow);
Backtest result = null;
if (save && result != null)
{
// Simple bot backtest not implemented yet, would need user
// _backtestRepository.InsertBacktestForUser(null, result);
}
return result;
}
/// <summary>
/// Runs a trading bot backtest with the specified configuration and date range.
/// Automatically handles different bot types based on config.BotType.
/// </summary>
/// <param name="config">The trading bot configuration (must include Scenario object or ScenarioName)</param>
/// <param name="startDate">The start date for the backtest</param>
/// <param name="endDate">The end date for the backtest</param>
/// <param name="user">The user running the backtest (optional)</param>
/// <param name="save">Whether to save the backtest results</param>
/// <returns>The backtest results</returns>
public async Task<Backtest> RunTradingBotBacktest(
TradingBotConfig config,
DateTime startDate,
DateTime endDate,
User user = null,
bool save = false)
{
var account = await GetAccountFromConfig(config);
var candles = GetCandles(account, config.Ticker, config.Timeframe, startDate, endDate);
var result = await RunBacktestWithCandles(config, candles, user);
// Set start and end dates
result.StartDate = startDate;
result.EndDate = endDate;
if (save && user != null)
{
_backtestRepository.InsertBacktestForUser(user, result);
}
return result;
}
/// <summary>
/// Runs a trading bot backtest with pre-loaded candles.
/// Automatically handles different bot types based on config.BotType.
/// </summary>
/// <param name="config">The trading bot configuration (must include Scenario object or ScenarioName)</param>
/// <param name="candles">The candles to use for backtesting</param>
/// <param name="user">The user running the backtest (optional)</param>
/// <returns>The backtest results</returns>
public async Task<Backtest> RunTradingBotBacktest(
TradingBotConfig config,
List<Candle> candles,
User user = null)
{
return await RunBacktestWithCandles(config, candles, user);
}
/// <summary>
/// Core backtesting logic - handles the actual backtest execution with pre-loaded candles
/// </summary>
private async Task<Backtest> RunBacktestWithCandles(
TradingBotConfig config,
List<Candle> candles,
User user = null)
{
// Set FlipPosition based on BotType
config.FlipPosition = config.BotType == BotType.FlippingBot;
var tradingBot = _botFactory.CreateBacktestTradingBot(config);
// Load scenario - prefer Scenario object over ScenarioName
if (config.Scenario != null)
{
tradingBot.LoadScenario(config.Scenario);
}
else if (!string.IsNullOrEmpty(config.ScenarioName))
{
tradingBot.LoadScenario(config.ScenarioName);
}
else
{
throw new ArgumentException(
"Either Scenario object or ScenarioName must be provided in TradingBotConfig");
}
tradingBot.User = user;
await tradingBot.LoadAccount();
var result = GetBacktestingResult(config, tradingBot, candles);
if (user != null)
{
result.User = user;
}
return result;
}
private async Task<Account> GetAccountFromConfig(TradingBotConfig config)
{
var account = await _accountService.GetAccount(config.AccountName, false, false);
if (account != null)
{
return account;
}
return new Account
{
Name = config.AccountName,
Exchange = TradingExchanges.GmxV2
};
}
private List<Candle> GetCandles(Account account, Ticker ticker, Timeframe timeframe,
DateTime startDate, DateTime endDate)
{
var candles = _exchangeService.GetCandlesInflux(account.Exchange, ticker,
startDate, timeframe, endDate).Result;
if (candles == null || candles.Count == 0)
throw new Exception($"No candles for {ticker} on {account.Exchange}");
return candles;
}
private Backtest GetBacktestingResult(
TradingBotConfig config,
ITradingBot bot,
List<Candle> candles)
{
if (candles == null || candles.Count == 0)
{
throw new Exception("No candle to backtest");
}
var totalCandles = candles.Count;
var currentCandle = 0;
var lastLoggedPercentage = 0;
_logger.LogInformation("Starting backtest with {TotalCandles} candles for {Ticker} on {Timeframe}",
totalCandles, config.Ticker, config.Timeframe);
bot.WalletBalances.Add(candles.FirstOrDefault().Date, config.BotTradingBalance);
foreach (var candle in candles)
{
bot.OptimizedCandles.Enqueue(candle);
bot.Candles.Add(candle);
bot.Run();
currentCandle++;
// Log progress every 10% or every 1000 candles, whichever comes first
var currentPercentage = (int)((double)currentCandle / totalCandles * 100);
var shouldLog = currentPercentage >= lastLoggedPercentage + 10 ||
currentCandle % 1000 == 0 ||
currentCandle == totalCandles;
if (shouldLog && currentPercentage > lastLoggedPercentage)
{
_logger.LogInformation(
"Backtest progress: {CurrentCandle}/{TotalCandles} ({Percentage}%) - Processing candle from {CandleDate}",
currentCandle, totalCandles, currentPercentage, candle.Date.ToString("yyyy-MM-dd HH:mm"));
lastLoggedPercentage = currentPercentage;
}
}
_logger.LogInformation("Backtest processing completed. Calculating final results...");
bot.Candles = new HashSet<Candle>(candles);
// bot.UpdateIndicatorsValues();
var indicatorsValues = GetIndicatorsValues(bot.Config.Scenario.Indicators, candles);
var finalPnl = bot.GetProfitAndLoss();
var winRate = bot.GetWinRate();
var optimizedMoneyManagement =
TradingBox.GetBestMoneyManagement(candles, bot.Positions, config.MoneyManagement);
var stats = TradingHelpers.GetStatistics(bot.WalletBalances);
var growthPercentage = TradingHelpers.GetGrowthFromInitalBalance(config.BotTradingBalance, finalPnl);
var hodlPercentage = TradingHelpers.GetHodlPercentage(candles[0], candles.Last());
var fees = bot.GetTotalFees();
var scoringParams = new BacktestScoringParams(
sharpeRatio: (double)stats.SharpeRatio,
maxDrawdownPc: (double)stats.MaxDrawdownPc,
growthPercentage: (double)growthPercentage,
hodlPercentage: (double)hodlPercentage,
winRate: winRate,
totalPnL: (double)finalPnl,
fees: (double)fees,
tradeCount: bot.Positions.Count,
maxDrawdownRecoveryTime: stats.MaxDrawdownRecoveryTime
);
var score = BacktestScorer.CalculateTotalScore(scoringParams);
var result = new Backtest(config, bot.Positions, bot.Signals.ToList(), candles)
{
FinalPnl = finalPnl,
WinRate = winRate,
GrowthPercentage = growthPercentage,
HodlPercentage = hodlPercentage,
Fees = fees,
WalletBalances = bot.WalletBalances.ToList(),
Statistics = stats,
OptimizedMoneyManagement = optimizedMoneyManagement,
IndicatorsValues = AggregateValues(indicatorsValues, bot.IndicatorsValues),
Score = score
};
return result;
}
private Dictionary<IndicatorType, IndicatorsResultBase> AggregateValues(
Dictionary<IndicatorType, IndicatorsResultBase> indicatorsValues,
Dictionary<IndicatorType, IndicatorsResultBase> botStrategiesValues)
{
// Foreach strategy type, only retrieve the values where the strategy is not present already in the bot
// Then, add the values to the bot values
var result = new Dictionary<IndicatorType, IndicatorsResultBase>();
foreach (var indicator in indicatorsValues)
{
// if (!botStrategiesValues.ContainsKey(strategy.Key))
// {
// result[strategy.Key] = strategy.Value;
// }else
// {
// result[strategy.Key] = botStrategiesValues[strategy.Key];
// }
result[indicator.Key] = indicator.Value;
}
return result;
}
private Dictionary<IndicatorType, IndicatorsResultBase> GetIndicatorsValues(List<Indicator> indicators,
List<Candle> candles)
{
var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
var fixedCandles = new FixedSizeQueue<Candle>(10000);
foreach (var candle in candles)
{
fixedCandles.Enqueue(candle);
}
foreach (var indicator in indicators)
{
try
{
var s = ScenarioHelpers.BuildIndicator(indicator, 10000);
s.Candles = fixedCandles;
indicatorsValues[indicator.Type] = s.GetIndicatorValues();
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
return indicatorsValues;
}
public bool DeleteBacktest(string id)
{
try
{
_backtestRepository.DeleteBacktestByIdForUser(null, id);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
public bool DeleteBacktests()
{
try
{
_backtestRepository.DeleteAllBacktestsForUser(null);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
public async Task<IEnumerable<Backtest>> GetBacktestsByUser(User user)
{
var backtests = _backtestRepository.GetBacktestsByUser(user).ToList();
foreach (var backtest in backtests)
{
if (backtest.Candles == null || backtest.Candles.Count == 0 || backtest.Candles.Count < 10)
{
try
{
var candles = await _exchangeService.GetCandlesInflux(
user.Accounts.First().Exchange,
backtest.Config.Ticker,
backtest.StartDate,
backtest.Config.Timeframe,
backtest.EndDate);
if (candles != null && candles.Count > 0)
{
backtest.Candles = candles;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to retrieve candles for backtest {Id}", backtest.Id);
}
}
}
return backtests;
}
public Backtest GetBacktestByIdForUser(User user, string id)
{
var backtest = _backtestRepository.GetBacktestByIdForUser(user, id);
if (backtest == null)
return null;
if (backtest.Candles == null || backtest.Candles.Count == 0 || backtest.Candles.Count < 10)
{
try
{
var account = new Account
{ Name = backtest.Config.AccountName, Exchange = TradingExchanges.Evm };
var candles = _exchangeService.GetCandlesInflux(
account.Exchange,
backtest.Config.Ticker,
backtest.StartDate,
backtest.Config.Timeframe,
backtest.EndDate).Result;
if (candles != null && candles.Count > 0)
{
backtest.Candles = candles;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to retrieve candles for backtest {Id}", id);
}
}
return backtest;
}
public bool DeleteBacktestByUser(User user, string id)
{
try
{
_backtestRepository.DeleteBacktestByIdForUser(user, id);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
public bool DeleteBacktestsByUser(User user)
{
try
{
_backtestRepository.DeleteAllBacktestsForUser(user);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
}
}