This commit is contained in:
2025-11-09 02:08:31 +07:00
parent 1ed58d1a98
commit 7dba29c66f
57 changed files with 8362 additions and 359 deletions

View 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;
}
}
}