Move workers

This commit is contained in:
2025-08-05 17:53:19 +07:00
parent 7d92031059
commit 3d3f71ac7a
26 changed files with 81 additions and 118 deletions

View File

@@ -0,0 +1,484 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Application.Hubs;
using Managing.Domain.Accounts;
using Managing.Domain.Backtests;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.Scenarios;
using Managing.Domain.Users;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
using LightBacktestResponse = Managing.Domain.Backtests.LightBacktest; // Use the domain model for notification
namespace Managing.Application.Backtests
{
public class Backtester : IBacktester
{
private readonly IBacktestRepository _backtestRepository;
private readonly ILogger<Backtester> _logger;
private readonly IExchangeService _exchangeService;
private readonly IScenarioService _scenarioService;
private readonly IAccountService _accountService;
private readonly IMessengerService _messengerService;
private readonly IKaigenService _kaigenService;
private readonly IHubContext<BacktestHub> _hubContext;
private readonly IGrainFactory _grainFactory;
public Backtester(
IExchangeService exchangeService,
IBacktestRepository backtestRepository,
ILogger<Backtester> logger,
IScenarioService scenarioService,
IAccountService accountService,
IMessengerService messengerService,
IKaigenService kaigenService,
IHubContext<BacktestHub> hubContext,
IGrainFactory grainFactory)
{
_exchangeService = exchangeService;
_backtestRepository = backtestRepository;
_logger = logger;
_scenarioService = scenarioService;
_accountService = accountService;
_messengerService = messengerService;
_kaigenService = kaigenService;
_hubContext = hubContext;
_grainFactory = grainFactory;
}
/// <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>
/// <param name="withCandles">Whether to include candles and indicators values in the response</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>
public async Task<LightBacktestResponse> RunTradingBotBacktest(
TradingBotConfig config,
DateTime startDate,
DateTime endDate,
User user = null,
bool save = false,
bool withCandles = false,
string requestId = null,
object metadata = null)
{
string creditRequestId = null;
// Debit user credits before starting the backtest
if (user != null)
{
try
{
creditRequestId = await _kaigenService.DebitUserCreditsAsync(user, 3);
_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}");
}
}
try
{
var candles = 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);
}
}
throw;
}
}
/// <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>
/// <param name="withCandles">Whether to include candles and indicators values in the response</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>
public async Task<LightBacktestResponse> RunTradingBotBacktest(
TradingBotConfig config,
HashSet<Candle> candles,
User user = null,
bool withCandles = false,
string requestId = null,
object metadata = null)
{
return await RunBacktestWithCandles(config, candles, 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 and return LightBacktest directly
return await backtestGrain.RunBacktestAsync(cleanConfig, candles, user, save, withCandles, requestId,
metadata);
}
private async Task<Account> GetAccountFromConfig(TradingBotConfig config)
{
return await _accountService.GetAccountByAccountName(config.AccountName, false, false);
}
private HashSet<Candle> GetCandles(Ticker ticker, Timeframe timeframe,
DateTime startDate, DateTime endDate)
{
var candles = _exchangeService.GetCandlesInflux(TradingExchanges.Evm, ticker,
startDate, timeframe, endDate).Result;
if (candles == null || candles.Count == 0)
throw new Exception($"No candles for {ticker} on {timeframe} timeframe");
return candles;
}
/// <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;
}
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
{
await _backtestRepository.DeleteBacktestByIdForUserAsync(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 IEnumerable<Backtest> GetBacktestsByUser(User user)
{
var backtests = _backtestRepository.GetBacktestsByUser(user).ToList();
return backtests;
}
public async Task<IEnumerable<Backtest>> GetBacktestsByUserAsync(User user)
{
var backtests = await _backtestRepository.GetBacktestsByUserAsync(user);
return backtests;
}
public IEnumerable<Backtest> GetBacktestsByRequestId(string requestId)
{
var backtests = _backtestRepository.GetBacktestsByRequestId(requestId).ToList();
return backtests;
}
public async Task<IEnumerable<Backtest>> GetBacktestsByRequestIdAsync(string requestId)
{
var backtests = await _backtestRepository.GetBacktestsByRequestIdAsync(requestId);
return backtests;
}
public (IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId,
int page, int pageSize, string sortBy = "score", string sortOrder = "desc")
{
var (backtests, totalCount) =
_backtestRepository.GetBacktestsByRequestIdPaginated(requestId, page, pageSize, sortBy, sortOrder);
return (backtests, totalCount);
}
public async Task<(IEnumerable<LightBacktest> Backtests, int TotalCount)> GetBacktestsByRequestIdPaginatedAsync(
string requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc")
{
var (backtests, totalCount) =
await _backtestRepository.GetBacktestsByRequestIdPaginatedAsync(requestId, page, pageSize, sortBy,
sortOrder);
return (backtests, totalCount);
}
public async Task<Backtest> GetBacktestByIdForUserAsync(User user, string id)
{
var backtest = await _backtestRepository.GetBacktestByIdForUserAsync(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 = await _exchangeService.GetCandlesInflux(
account.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}", id);
}
}
return backtest;
}
public async Task<bool> DeleteBacktestByUserAsync(User user, string id)
{
try
{
await _backtestRepository.DeleteBacktestByIdForUserAsync(user, id);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
public async Task<bool> DeleteBacktestsByIdsForUserAsync(User user, IEnumerable<string> ids)
{
try
{
await _backtestRepository.DeleteBacktestsByIdsForUserAsync(user, ids);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to delete backtests for user {UserName}", user.Name);
return false;
}
}
public bool DeleteBacktestsByUser(User user)
{
try
{
_backtestRepository.DeleteAllBacktestsForUser(user);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
public async Task<bool> DeleteBacktestsByRequestIdAsync(string requestId)
{
try
{
await _backtestRepository.DeleteBacktestsByRequestIdAsync(requestId);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to delete backtests for request ID {RequestId}", requestId);
return false;
}
}
public (IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page,
int pageSize, string sortBy = "score", string sortOrder = "desc")
{
var (backtests, totalCount) =
_backtestRepository.GetBacktestsByUserPaginated(user, page, pageSize, sortBy, sortOrder);
return (backtests, totalCount);
}
public async Task<(IEnumerable<LightBacktest> Backtests, int TotalCount)> GetBacktestsByUserPaginatedAsync(
User user, int page, int pageSize, string sortBy = "score", string sortOrder = "desc")
{
var (backtests, totalCount) =
await _backtestRepository.GetBacktestsByUserPaginatedAsync(user, page, pageSize, sortBy, sortOrder);
return (backtests, totalCount);
}
// Bundle backtest methods
public void InsertBundleBacktestRequestForUser(User user, BundleBacktestRequest bundleRequest)
{
_backtestRepository.InsertBundleBacktestRequestForUser(user, bundleRequest);
}
public IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByUser(User user)
{
return _backtestRepository.GetBundleBacktestRequestsByUser(user);
}
public async Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByUserAsync(User user)
{
return await _backtestRepository.GetBundleBacktestRequestsByUserAsync(user);
}
public BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, string id)
{
return _backtestRepository.GetBundleBacktestRequestByIdForUser(user, id);
}
public async Task<BundleBacktestRequest?> GetBundleBacktestRequestByIdForUserAsync(User user, string id)
{
return await _backtestRepository.GetBundleBacktestRequestByIdForUserAsync(user, id);
}
public void UpdateBundleBacktestRequest(BundleBacktestRequest bundleRequest)
{
_backtestRepository.UpdateBundleBacktestRequest(bundleRequest);
}
public async Task UpdateBundleBacktestRequestAsync(BundleBacktestRequest bundleRequest)
{
await _backtestRepository.UpdateBundleBacktestRequestAsync(bundleRequest);
}
public void DeleteBundleBacktestRequestByIdForUser(User user, string id)
{
_backtestRepository.DeleteBundleBacktestRequestByIdForUser(user, id);
}
public async Task DeleteBundleBacktestRequestByIdForUserAsync(User user, string id)
{
await _backtestRepository.DeleteBundleBacktestRequestByIdForUserAsync(user, id);
}
public IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByStatus(BundleBacktestRequestStatus status)
{
// Use the repository method to get all bundles, then filter by status
return _backtestRepository.GetBundleBacktestRequestsByStatus(status);
}
public async Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByStatusAsync(
BundleBacktestRequestStatus status)
{
return await _backtestRepository.GetBundleBacktestRequestsByStatusAsync(status);
}
/// <summary>
/// Sends a LightBacktestResponse to all SignalR subscribers of a bundle request.
/// </summary>
public async Task SendBundleBacktestUpdateAsync(string requestId, LightBacktestResponse response)
{
if (string.IsNullOrWhiteSpace(requestId) || response == null) return;
await _hubContext.Clients.Group($"bundle-{requestId}").SendAsync("BundleBacktestUpdate", response);
}
}
}