Add jobs
This commit is contained in:
@@ -1,15 +1,14 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Grains;
|
||||
using System.Text.Json;
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Repositories;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Abstractions.Shared;
|
||||
using Managing.Application.Hubs;
|
||||
using Managing.Core;
|
||||
using Managing.Domain.Accounts;
|
||||
using Managing.Domain.Backtests;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Scenarios;
|
||||
using Managing.Domain.MoneyManagements;
|
||||
using Managing.Domain.Users;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -30,7 +29,7 @@ namespace Managing.Application.Backtests
|
||||
private readonly IMessengerService _messengerService;
|
||||
private readonly IKaigenService _kaigenService;
|
||||
private readonly IHubContext<BacktestHub> _hubContext;
|
||||
private readonly IGrainFactory _grainFactory;
|
||||
private readonly BacktestJobService _jobService;
|
||||
|
||||
public Backtester(
|
||||
IExchangeService exchangeService,
|
||||
@@ -41,8 +40,8 @@ namespace Managing.Application.Backtests
|
||||
IMessengerService messengerService,
|
||||
IKaigenService kaigenService,
|
||||
IHubContext<BacktestHub> hubContext,
|
||||
IGrainFactory grainFactory,
|
||||
IServiceScopeFactory serviceScopeFactory)
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
BacktestJobService jobService)
|
||||
{
|
||||
_exchangeService = exchangeService;
|
||||
_backtestRepository = backtestRepository;
|
||||
@@ -52,23 +51,23 @@ namespace Managing.Application.Backtests
|
||||
_messengerService = messengerService;
|
||||
_kaigenService = kaigenService;
|
||||
_hubContext = hubContext;
|
||||
_grainFactory = grainFactory;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_jobService = jobService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a trading bot backtest with the specified configuration and date range.
|
||||
/// Automatically handles different bot types based on config.BotType.
|
||||
/// Creates a backtest job and returns immediately (fire-and-forget pattern).
|
||||
/// The job will be processed by compute workers.
|
||||
/// </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="user">The user running the backtest (required)</param>
|
||||
/// <param name="save">Whether to save the backtest results</param>
|
||||
/// <param name="withCandles">Whether to include candles and indicators values in the response</param>
|
||||
/// <param name="withCandles">Whether to include candles and indicators values in the response (ignored, always false for jobs)</param>
|
||||
/// <param name="requestId">The request ID to associate with this backtest (optional)</param>
|
||||
/// <param name="metadata">Additional metadata to associate with this backtest (optional)</param>
|
||||
/// <returns>The lightweight backtest results</returns>
|
||||
/// <returns>A lightweight backtest response with job ID (result will be available later via GetJobStatus)</returns>
|
||||
public async Task<LightBacktestResponse> RunTradingBotBacktest(
|
||||
TradingBotConfig config,
|
||||
DateTime startDate,
|
||||
@@ -79,59 +78,33 @@ namespace Managing.Application.Backtests
|
||||
string requestId = null,
|
||||
object metadata = null)
|
||||
{
|
||||
string creditRequestId = null;
|
||||
|
||||
// Debit user credits before starting the backtest
|
||||
if (user != null)
|
||||
if (user == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
creditRequestId = await _kaigenService.DebitUserCreditsAsync(user, 1);
|
||||
_logger.LogInformation(
|
||||
"Successfully debited credits for user {UserName} with request ID {RequestId}",
|
||||
user.Name, creditRequestId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to debit credits for user {UserName}. Backtest will not proceed.",
|
||||
user.Name);
|
||||
throw new Exception($"Failed to debit credits: {ex.Message}");
|
||||
}
|
||||
throw new ArgumentNullException(nameof(user), "User is required for job-based backtests");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var candles = await GetCandles(config.Ticker, config.Timeframe, startDate, endDate);
|
||||
return await RunBacktestWithCandles(config, candles, user, save, withCandles, requestId, metadata);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// If backtest fails and we debited credits, attempt to refund
|
||||
if (user != null && !string.IsNullOrEmpty(creditRequestId))
|
||||
{
|
||||
try
|
||||
{
|
||||
var refundSuccess = await _kaigenService.RefundUserCreditsAsync(creditRequestId, user);
|
||||
if (refundSuccess)
|
||||
{
|
||||
_logger.LogError(
|
||||
"Successfully refunded credits for user {UserName} after backtest failure: {message}",
|
||||
user.Name, ex.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Failed to refund credits for user {UserName} after backtest failure",
|
||||
user.Name);
|
||||
}
|
||||
}
|
||||
catch (Exception refundEx)
|
||||
{
|
||||
_logger.LogError(refundEx, "Error during refund attempt for user {UserName}", user.Name);
|
||||
}
|
||||
}
|
||||
// Create a job instead of running synchronously
|
||||
var job = await _jobService.CreateJobAsync(
|
||||
config,
|
||||
startDate,
|
||||
endDate,
|
||||
user,
|
||||
priority: 0,
|
||||
requestId: requestId);
|
||||
|
||||
throw;
|
||||
}
|
||||
_logger.LogInformation(
|
||||
"Created backtest job {JobId} for user {UserId}. Job will be processed by compute workers.",
|
||||
job.Id, user.Id);
|
||||
|
||||
// Return a placeholder response with job ID
|
||||
// The actual result will be available via GetJobStatus endpoint
|
||||
return new LightBacktestResponse
|
||||
{
|
||||
Id = job.Id.ToString(),
|
||||
Config = config,
|
||||
Score = 0, // Placeholder, actual score will be available when job completes
|
||||
ScoreMessage = $"Job {job.Id} is queued for processing"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -153,67 +126,21 @@ namespace Managing.Application.Backtests
|
||||
string requestId = null,
|
||||
object metadata = null)
|
||||
{
|
||||
return await RunBacktestWithCandles(config, candles, user, false, withCandles, requestId, metadata);
|
||||
// This overload is deprecated - use the date range overload which creates a job
|
||||
// For backward compatibility, create a job with the provided candles date range
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user), "User is required");
|
||||
}
|
||||
|
||||
var startDate = candles.Min(c => c.Date);
|
||||
var endDate = candles.Max(c => c.Date);
|
||||
|
||||
return await RunTradingBotBacktest(config, startDate, endDate, user, false, withCandles, requestId, metadata);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core backtesting logic - handles the actual backtest execution with pre-loaded candles
|
||||
/// </summary>
|
||||
private async Task<LightBacktestResponse> RunBacktestWithCandles(
|
||||
TradingBotConfig config,
|
||||
HashSet<Candle> candles,
|
||||
User user = null,
|
||||
bool save = false,
|
||||
bool withCandles = false,
|
||||
string requestId = null,
|
||||
object metadata = null)
|
||||
{
|
||||
// Ensure this is a backtest configuration
|
||||
if (!config.IsForBacktest)
|
||||
{
|
||||
throw new InvalidOperationException("Backtest configuration must have IsForBacktest set to true");
|
||||
}
|
||||
|
||||
// Validate that scenario and indicators are properly loaded
|
||||
if (config.Scenario == null && string.IsNullOrEmpty(config.ScenarioName))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Backtest configuration must include either Scenario object or ScenarioName");
|
||||
}
|
||||
|
||||
if (config.Scenario == null && !string.IsNullOrEmpty(config.ScenarioName))
|
||||
{
|
||||
var fullScenario = await _scenarioService.GetScenarioByNameAndUserAsync(config.ScenarioName, user);
|
||||
config.Scenario = LightScenario.FromScenario(fullScenario);
|
||||
}
|
||||
|
||||
// Create a clean copy of the config to avoid Orleans serialization issues
|
||||
var cleanConfig = CreateCleanConfigForOrleans(config);
|
||||
|
||||
// Create Orleans grain for backtesting
|
||||
var backtestGrain = _grainFactory.GetGrain<IBacktestTradingBotGrain>(Guid.NewGuid());
|
||||
|
||||
// Run the backtest using the Orleans grain
|
||||
var result = await backtestGrain.RunBacktestAsync(cleanConfig, candles, user, save, withCandles, requestId,
|
||||
metadata);
|
||||
|
||||
// Increment backtest count for the user if user is provided
|
||||
if (user != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ServiceScopeHelpers.WithScopedService<IAgentService>(_serviceScopeFactory,
|
||||
async (agentService) => await agentService.IncrementBacktestCountAsync(user.Id));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to increment backtest count for user {UserId}", user.Id);
|
||||
// Don't throw here as the backtest was successful, just log the error
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
// 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)
|
||||
@@ -229,16 +156,7 @@ namespace Managing.Application.Backtests
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a clean copy of the trading bot config for Orleans serialization
|
||||
/// Uses LightScenario and LightIndicator to avoid FixedSizeQueue serialization issues
|
||||
/// </summary>
|
||||
private TradingBotConfig CreateCleanConfigForOrleans(TradingBotConfig originalConfig)
|
||||
{
|
||||
// Since we're now using LightScenario in TradingBotConfig, we can just return the original config
|
||||
// The conversion to LightScenario is already done when loading the scenario
|
||||
return originalConfig;
|
||||
}
|
||||
// Removed CreateCleanConfigForOrleans - no longer needed with job queue approach
|
||||
|
||||
private async Task SendBacktestNotificationIfCriteriaMet(Backtest backtest)
|
||||
{
|
||||
@@ -464,8 +382,121 @@ namespace Managing.Application.Backtests
|
||||
|
||||
if (!saveAsTemplate)
|
||||
{
|
||||
// Trigger the BundleBacktestGrain to process this request
|
||||
await TriggerBundleBacktestGrainAsync(bundleRequest.RequestId);
|
||||
// Generate backtest requests from variants (same logic as BundleBacktestGrain)
|
||||
var backtestRequests = await GenerateBacktestRequestsFromVariants(bundleRequest);
|
||||
|
||||
if (backtestRequests != null && backtestRequests.Any())
|
||||
{
|
||||
// Create jobs for all variants
|
||||
await _jobService.CreateBundleJobsAsync(bundleRequest, backtestRequests);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Created {JobCount} backtest jobs for bundle request {BundleRequestId}",
|
||||
backtestRequests.Count, bundleRequest.RequestId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
var accounts = await _accountService.GetAccountsByUserAsync(bundleRequest.User, hideSecrets: true, getBalance: false);
|
||||
var firstAccount = 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,
|
||||
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>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,64 +561,6 @@ namespace Managing.Application.Backtests
|
||||
await _hubContext.Clients.Group($"bundle-{requestId}").SendAsync("BundleBacktestUpdate", response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers the BundleBacktestGrain to process a bundle request synchronously (fire and forget)
|
||||
/// </summary>
|
||||
private void TriggerBundleBacktestGrain(Guid bundleRequestId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bundleBacktestGrain = _grainFactory.GetGrain<IBundleBacktestGrain>(bundleRequestId);
|
||||
|
||||
// Fire and forget - don't await
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await bundleBacktestGrain.ProcessBundleRequestAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error triggering BundleBacktestGrain for request {RequestId}",
|
||||
bundleRequestId);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in TriggerBundleBacktestGrain for request {RequestId}", bundleRequestId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers the BundleBacktestGrain to process a bundle request asynchronously
|
||||
/// </summary>
|
||||
private Task TriggerBundleBacktestGrainAsync(Guid bundleRequestId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bundleBacktestGrain = _grainFactory.GetGrain<IBundleBacktestGrain>(bundleRequestId);
|
||||
|
||||
// Fire and forget - don't await the actual processing
|
||||
return Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await bundleBacktestGrain.ProcessBundleRequestAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error triggering BundleBacktestGrain for request {RequestId}",
|
||||
bundleRequestId);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in TriggerBundleBacktestGrainAsync for request {RequestId}",
|
||||
bundleRequestId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
// Removed TriggerBundleBacktestGrain methods - bundle backtests now use job queue
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user