Add jobs
This commit is contained in:
254
src/Managing.Application/Backtests/BacktestJobService.cs
Normal file
254
src/Managing.Application/Backtests/BacktestJobService.cs
Normal file
@@ -0,0 +1,254 @@
|
||||
using System.Text.Json;
|
||||
using Managing.Application.Abstractions.Repositories;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Domain.Backtests;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.MoneyManagements;
|
||||
using Managing.Domain.Scenarios;
|
||||
using Managing.Domain.Strategies;
|
||||
using Managing.Domain.Users;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Backtests;
|
||||
|
||||
/// <summary>
|
||||
/// Service for creating and managing backtest jobs in the queue
|
||||
/// </summary>
|
||||
public class BacktestJobService
|
||||
{
|
||||
private readonly IBacktestJobRepository _jobRepository;
|
||||
private readonly IBacktestRepository _backtestRepository;
|
||||
private readonly IKaigenService _kaigenService;
|
||||
private readonly ILogger<BacktestJobService> _logger;
|
||||
|
||||
public BacktestJobService(
|
||||
IBacktestJobRepository jobRepository,
|
||||
IBacktestRepository backtestRepository,
|
||||
IKaigenService kaigenService,
|
||||
ILogger<BacktestJobService> logger)
|
||||
{
|
||||
_jobRepository = jobRepository;
|
||||
_backtestRepository = backtestRepository;
|
||||
_kaigenService = kaigenService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a single backtest job
|
||||
/// </summary>
|
||||
public async Task<BacktestJob> CreateJobAsync(
|
||||
TradingBotConfig config,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
User user,
|
||||
int priority = 0,
|
||||
string requestId = null)
|
||||
{
|
||||
// Debit user credits before creating job
|
||||
string creditRequestId = 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}. Job will not be created.",
|
||||
user.Name);
|
||||
throw new Exception($"Failed to debit credits: {ex.Message}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var job = new BacktestJob
|
||||
{
|
||||
UserId = user.Id,
|
||||
Status = BacktestJobStatus.Pending,
|
||||
JobType = JobType.Backtest,
|
||||
Priority = priority,
|
||||
ConfigJson = JsonSerializer.Serialize(config),
|
||||
StartDate = startDate,
|
||||
EndDate = endDate,
|
||||
BundleRequestId = null, // Single jobs are not part of a bundle
|
||||
RequestId = requestId
|
||||
};
|
||||
|
||||
var createdJob = await _jobRepository.CreateAsync(job);
|
||||
_logger.LogInformation("Created backtest job {JobId} for user {UserId}", createdJob.Id, user.Id);
|
||||
|
||||
return createdJob;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// If job creation fails, attempt to refund credits
|
||||
if (!string.IsNullOrEmpty(creditRequestId))
|
||||
{
|
||||
try
|
||||
{
|
||||
var refundSuccess = await _kaigenService.RefundUserCreditsAsync(creditRequestId, user);
|
||||
if (refundSuccess)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Successfully refunded credits for user {UserName} after job creation failure",
|
||||
user.Name);
|
||||
}
|
||||
}
|
||||
catch (Exception refundEx)
|
||||
{
|
||||
_logger.LogError(refundEx, "Error during refund attempt for user {UserName}", user.Name);
|
||||
}
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates multiple backtest jobs from bundle variants
|
||||
/// </summary>
|
||||
public async Task<List<BacktestJob>> CreateBundleJobsAsync(
|
||||
BundleBacktestRequest bundleRequest,
|
||||
List<RunBacktestRequest> backtestRequests)
|
||||
{
|
||||
var jobs = new List<BacktestJob>();
|
||||
var creditRequestId = (string?)null;
|
||||
|
||||
try
|
||||
{
|
||||
// Debit credits for all jobs upfront
|
||||
var totalJobs = backtestRequests.Count;
|
||||
creditRequestId = await _kaigenService.DebitUserCreditsAsync(bundleRequest.User, totalJobs);
|
||||
_logger.LogInformation(
|
||||
"Successfully debited {TotalJobs} credits for user {UserName} with request ID {RequestId}",
|
||||
totalJobs, bundleRequest.User.Name, creditRequestId);
|
||||
|
||||
// Create jobs for each variant
|
||||
for (int i = 0; i < backtestRequests.Count; i++)
|
||||
{
|
||||
var backtestRequest = backtestRequests[i];
|
||||
|
||||
// Map MoneyManagement
|
||||
var moneyManagement = backtestRequest.MoneyManagement;
|
||||
if (moneyManagement == null && backtestRequest.Config.MoneyManagement != null)
|
||||
{
|
||||
var mmReq = backtestRequest.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 (backtestRequest.Config.Scenario != null)
|
||||
{
|
||||
var sReq = backtestRequest.Config.Scenario;
|
||||
scenario = new LightScenario(sReq.Name, sReq.LoopbackPeriod)
|
||||
{
|
||||
Indicators = sReq.Indicators?.Select(ind => new LightIndicator(ind.Name, ind.Type)
|
||||
{
|
||||
SignalType = ind.SignalType,
|
||||
MinimumHistory = ind.MinimumHistory,
|
||||
Period = ind.Period,
|
||||
FastPeriods = ind.FastPeriods,
|
||||
SlowPeriods = ind.SlowPeriods,
|
||||
SignalPeriods = ind.SignalPeriods,
|
||||
Multiplier = ind.Multiplier,
|
||||
SmoothPeriods = ind.SmoothPeriods,
|
||||
StochPeriods = ind.StochPeriods,
|
||||
CyclePeriods = ind.CyclePeriods
|
||||
}).ToList() ?? new List<LightIndicator>()
|
||||
};
|
||||
}
|
||||
|
||||
// Create TradingBotConfig
|
||||
var backtestConfig = new TradingBotConfig
|
||||
{
|
||||
AccountName = backtestRequest.Config.AccountName,
|
||||
MoneyManagement = moneyManagement != null
|
||||
? new LightMoneyManagement
|
||||
{
|
||||
Name = moneyManagement.Name,
|
||||
Timeframe = moneyManagement.Timeframe,
|
||||
StopLoss = moneyManagement.StopLoss,
|
||||
TakeProfit = moneyManagement.TakeProfit,
|
||||
Leverage = moneyManagement.Leverage
|
||||
}
|
||||
: null,
|
||||
Ticker = backtestRequest.Config.Ticker,
|
||||
ScenarioName = backtestRequest.Config.ScenarioName,
|
||||
Scenario = scenario,
|
||||
Timeframe = backtestRequest.Config.Timeframe,
|
||||
IsForWatchingOnly = backtestRequest.Config.IsForWatchingOnly,
|
||||
BotTradingBalance = backtestRequest.Config.BotTradingBalance,
|
||||
IsForBacktest = true,
|
||||
CooldownPeriod = backtestRequest.Config.CooldownPeriod ?? 1,
|
||||
MaxLossStreak = backtestRequest.Config.MaxLossStreak,
|
||||
MaxPositionTimeHours = backtestRequest.Config.MaxPositionTimeHours,
|
||||
FlipOnlyWhenInProfit = backtestRequest.Config.FlipOnlyWhenInProfit,
|
||||
FlipPosition = backtestRequest.Config.FlipPosition,
|
||||
Name = $"{bundleRequest.Name} #{i + 1}",
|
||||
CloseEarlyWhenProfitable = backtestRequest.Config.CloseEarlyWhenProfitable,
|
||||
UseSynthApi = backtestRequest.Config.UseSynthApi,
|
||||
UseForPositionSizing = backtestRequest.Config.UseForPositionSizing,
|
||||
UseForSignalFiltering = backtestRequest.Config.UseForSignalFiltering,
|
||||
UseForDynamicStopLoss = backtestRequest.Config.UseForDynamicStopLoss
|
||||
};
|
||||
|
||||
var job = new BacktestJob
|
||||
{
|
||||
UserId = bundleRequest.User.Id,
|
||||
Status = BacktestJobStatus.Pending,
|
||||
JobType = JobType.Backtest,
|
||||
Priority = 0, // All bundle jobs have same priority
|
||||
ConfigJson = JsonSerializer.Serialize(backtestConfig),
|
||||
StartDate = backtestRequest.StartDate,
|
||||
EndDate = backtestRequest.EndDate,
|
||||
BundleRequestId = bundleRequest.RequestId,
|
||||
RequestId = bundleRequest.RequestId.ToString()
|
||||
};
|
||||
|
||||
var createdJob = await _jobRepository.CreateAsync(job);
|
||||
jobs.Add(createdJob);
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Created {JobCount} backtest jobs for bundle request {BundleRequestId}",
|
||||
jobs.Count, bundleRequest.RequestId);
|
||||
|
||||
return jobs;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// If job creation fails, attempt to refund credits
|
||||
if (!string.IsNullOrEmpty(creditRequestId))
|
||||
{
|
||||
try
|
||||
{
|
||||
var refundSuccess = await _kaigenService.RefundUserCreditsAsync(creditRequestId, bundleRequest.User);
|
||||
if (refundSuccess)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Successfully refunded credits for user {UserName} after bundle job creation failure",
|
||||
bundleRequest.User.Name);
|
||||
}
|
||||
}
|
||||
catch (Exception refundEx)
|
||||
{
|
||||
_logger.LogError(refundEx, "Error during refund attempt for user {UserName}", bundleRequest.User.Name);
|
||||
}
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user