Add save only for bundle backtest

This commit is contained in:
2025-10-21 16:38:51 +07:00
parent d144ae73ca
commit af08462e59
7 changed files with 120 additions and 45 deletions

View File

@@ -655,7 +655,7 @@ public class BacktestController : BaseController
Name = request.Name Name = request.Name
}; };
_backtester.InsertBundleBacktestRequestForUser(user, bundleRequest); await _backtester.InsertBundleBacktestRequestForUserAsync(user, bundleRequest, request.SaveAsTemplate);
return Ok(bundleRequest); return Ok(bundleRequest);
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -38,4 +38,10 @@ public class RunBundleBacktestRequest
/// </summary> /// </summary>
[Required] [Required]
public List<Ticker> TickerVariants { get; set; } = new(); public List<Ticker> TickerVariants { get; set; } = new();
/// <summary>
/// Save only the request as a template
/// </summary>
[Required]
public bool SaveAsTemplate { get; set; } = false;
} }

View File

@@ -84,9 +84,7 @@ namespace Managing.Application.Abstractions.Services
Task<bool> DeleteBacktestsByRequestIdAsync(Guid requestId); Task<bool> DeleteBacktestsByRequestIdAsync(Guid requestId);
Task<int> DeleteBacktestsByFiltersAsync(User user, BacktestsFilter filter); Task<int> DeleteBacktestsByFiltersAsync(User user, BacktestsFilter filter);
// Bundle backtest methods Task InsertBundleBacktestRequestForUserAsync(User user, BundleBacktestRequest bundleRequest, bool saveAsTemplate = false);
void InsertBundleBacktestRequestForUser(User user, BundleBacktestRequest bundleRequest);
Task InsertBundleBacktestRequestForUserAsync(User user, BundleBacktestRequest bundleRequest);
IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByUser(User user); IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByUser(User user);
Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByUserAsync(User user); Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByUserAsync(User user);
BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, Guid id); BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, Guid id);

View File

@@ -455,21 +455,15 @@ namespace Managing.Application.Backtests
return (backtests, totalCount); return (backtests, totalCount);
} }
// Bundle backtest methods public async Task InsertBundleBacktestRequestForUserAsync(User user, BundleBacktestRequest bundleRequest, bool saveAsTemplate = false)
public void InsertBundleBacktestRequestForUser(User user, BundleBacktestRequest bundleRequest)
{
_backtestRepository.InsertBundleBacktestRequestForUser(user, bundleRequest);
// Trigger the BundleBacktestGrain to process this request
TriggerBundleBacktestGrain(bundleRequest.RequestId);
}
public async Task InsertBundleBacktestRequestForUserAsync(User user, BundleBacktestRequest bundleRequest)
{ {
await _backtestRepository.InsertBundleBacktestRequestForUserAsync(user, bundleRequest); await _backtestRepository.InsertBundleBacktestRequestForUserAsync(user, bundleRequest);
// Trigger the BundleBacktestGrain to process this request if (!saveAsTemplate)
await TriggerBundleBacktestGrainAsync(bundleRequest.RequestId); {
// Trigger the BundleBacktestGrain to process this request
await TriggerBundleBacktestGrainAsync(bundleRequest.RequestId);
}
} }
public IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByUser(User user) public IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByUser(User user)

View File

@@ -3,7 +3,6 @@ using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Application.Orleans; using Managing.Application.Orleans;
using Managing.Domain.Candles; using Managing.Domain.Candles;
using Managing.Domain.Shared.Helpers;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static Managing.Common.Enums; using static Managing.Common.Enums;
@@ -174,10 +173,33 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
{ {
if (!position.IsValidForMetrics()) continue; if (!position.IsValidForMetrics()) continue;
// Calculate volume using the dedicated method // Calculate volume using the same logic as daily snapshots for consistency
var positionVolume = TradingHelpers.GetVolumeForPosition(position); // Opening volume is always counted (for positions opened on or before today)
var openVolume = position.Open.Price * position.Open.Quantity * position.Open.Leverage;
var closingVolume = 0m;
// Track total volume from ALL positions for debugging // Only include closing volume from trades that are filled
if (position.Status == PositionStatus.Finished || position.Status == PositionStatus.Flipped)
{
if (position.StopLoss?.Status == TradeStatus.Filled)
{
closingVolume += position.StopLoss.Price * position.StopLoss.Quantity * position.StopLoss.Leverage;
}
if (position.TakeProfit1?.Status == TradeStatus.Filled)
{
closingVolume += position.TakeProfit1.Price * position.TakeProfit1.Quantity * position.TakeProfit1.Leverage;
}
if (position.TakeProfit2?.Status == TradeStatus.Filled)
{
closingVolume += position.TakeProfit2.Price * position.TakeProfit2.Quantity * position.TakeProfit2.Leverage;
}
}
var positionVolume = openVolume + closingVolume;
// Track total volume from ALL positions (this is the true cumulative volume)
totalVolumeFromAllPositions += positionVolume; totalVolumeFromAllPositions += positionVolume;
// For cumulative volume: only add volume from positions created AFTER last snapshot // For cumulative volume: only add volume from positions created AFTER last snapshot
@@ -377,10 +399,18 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
{ {
_logger.LogInformation("Taking daily snapshot"); _logger.LogInformation("Taking daily snapshot");
// Add daily snapshot // Before taking today's snapshot, fill any missing snapshots from previous days
// This ensures we don't have gaps in the historical data
await FillMissingDailySnapshotsAsync();
// Refresh data to get the latest metrics
await RefreshDataAsync();
// Add daily snapshot for today
var today = DateTime.UtcNow.Date;
var dailySnapshot = new DailySnapshot var dailySnapshot = new DailySnapshot
{ {
Date = DateTime.UtcNow.Date, Date = today,
TotalAgents = _state.State.TotalAgents, TotalAgents = _state.State.TotalAgents,
TotalStrategies = _state.State.TotalActiveStrategies, TotalStrategies = _state.State.TotalActiveStrategies,
TotalVolume = _state.State.TotalPlatformVolume, TotalVolume = _state.State.TotalPlatformVolume,
@@ -388,20 +418,33 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
NetPnL = _state.State.NetPnL, NetPnL = _state.State.NetPnL,
TotalOpenInterest = _state.State.OpenInterest, TotalOpenInterest = _state.State.OpenInterest,
TotalLifetimePositionCount = _state.State.TotalLifetimePositionCount, TotalLifetimePositionCount = _state.State.TotalLifetimePositionCount,
TotalPlatformFees = (int)_state.State.TotalPlatformFees,
}; };
_state.State.DailySnapshots.Add(dailySnapshot); // Only add if we don't already have a snapshot for today
if (!_state.State.DailySnapshots.Any(s => s.Date.Date == today))
{
_state.State.DailySnapshots.Add(dailySnapshot);
_logger.LogInformation("Created daily snapshot for {Date}: Volume={Volume}, PnL={PnL}, Positions={Positions}",
today, dailySnapshot.TotalVolume, dailySnapshot.TotalPnL, dailySnapshot.TotalLifetimePositionCount);
}
else
{
_logger.LogWarning("Daily snapshot for {Date} already exists, skipping", today);
}
// Keep only last 60 days // Keep only last 60 days
var cutoff = DateTime.UtcNow.AddDays(-60); var cutoff = DateTime.UtcNow.AddDays(-60);
_state.State.DailySnapshots.RemoveAll(s => s.Date < cutoff); _state.State.DailySnapshots.RemoveAll(s => s.Date < cutoff);
_state.State.LastSnapshot = DateTime.UtcNow; _state.State.LastSnapshot = today;
// Reset the volume updated by events flag daily to allow periodic refresh from strategies // Reset the volume updated by events flag daily to allow periodic refresh from strategies
_state.State.VolumeUpdatedByEvents = false; _state.State.VolumeUpdatedByEvents = false;
await _state.WriteStateAsync(); await _state.WriteStateAsync();
_logger.LogInformation("Daily snapshot complete. Total snapshots: {Count}", _state.State.DailySnapshots.Count);
} }
private bool IsDataStale() private bool IsDataStale()
@@ -516,27 +559,31 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
// Get all positions to calculate missing snapshots // Get all positions to calculate missing snapshots
var positions = await _tradingService.GetAllDatabasePositionsAsync(); var positions = await _tradingService.GetAllDatabasePositionsAsync();
if (!positions.Any())
{
_logger.LogInformation("No positions found, skipping gap filling");
return;
}
// Find the date range we need to cover
var earliestPositionDate = positions.Min(p => p.Date).Date;
var latestPositionDate = positions.Max(p => p.Date).Date;
var today = DateTime.UtcNow.Date; var today = DateTime.UtcNow.Date;
// Determine the start date for gap filling // Determine the start date for gap filling
var startDate = _state.State.DailySnapshots.Any() DateTime startDate;
? _state.State.DailySnapshots.Max(s => s.Date).AddDays(1) if (_state.State.DailySnapshots.Any())
: earliestPositionDate; {
startDate = _state.State.DailySnapshots.Max(s => s.Date).Date.AddDays(1);
}
else if (positions.Any())
{
// Start from the first position date
startDate = positions.Min(p => p.Date).Date;
}
else
{
// No positions and no snapshots - start from today
_logger.LogInformation("No positions and no snapshots found, starting from today");
startDate = today;
}
// Don't go beyond today // IMPORTANT: Fill snapshots up to TODAY (not just up to latest position date)
var endDate = today > latestPositionDate ? today : latestPositionDate; // This ensures we have daily snapshots even for days with no trading activity
var endDate = today;
_logger.LogInformation("Gap filling from {StartDate} to {EndDate}", startDate, endDate); _logger.LogInformation("Gap filling from {StartDate} to {EndDate} (today)", startDate, endDate);
var missingDates = new List<DateTime>(); var missingDates = new List<DateTime>();
for (var date = startDate; date <= endDate; date = date.AddDays(1)) for (var date = startDate; date <= endDate; date = date.AddDays(1))
@@ -558,9 +605,8 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
// Calculate and add missing snapshots // Calculate and add missing snapshots
foreach (var missingDate in missingDates) foreach (var missingDate in missingDates)
{ {
var snapshot = var validPositions = positions.Where(p => p.IsValidForMetrics()).ToList();
await CalculateDailySnapshotFromPositionsAsync(positions.Where(p => p.IsValidForMetrics()).ToList(), var snapshot = await CalculateDailySnapshotFromPositionsAsync(validPositions, missingDate);
missingDate);
_state.State.DailySnapshots.Add(snapshot); _state.State.DailySnapshots.Add(snapshot);
_logger.LogInformation( _logger.LogInformation(
@@ -596,7 +642,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
private async Task<DailySnapshot> CalculateDailySnapshotFromPositionsAsync(List<Position> positions, private async Task<DailySnapshot> CalculateDailySnapshotFromPositionsAsync(List<Position> positions,
DateTime targetDate) DateTime targetDate)
{ {
// Calculate CUMULATIVE metrics: sum of ALL volume/PnL from positions opened on or before target date // Calculate CUMULATIVE metrics: sum of ALL volume/PnL from positions with activity on or before target date
var totalVolume = 0m; var totalVolume = 0m;
var totalFees = 0m; var totalFees = 0m;
var totalPnL = 0m; var totalPnL = 0m;
@@ -625,28 +671,55 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
var closingVolume = 0m; var closingVolume = 0m;
// ClosingVolume = Sum of all filled closing trades (SL, TP1, TP2) that happened on or before target date // ClosingVolume = Sum of all filled closing trades (SL, TP1, TP2) that happened on or before target date
// IMPORTANT: Only count closing volume from trades that were filled on or BEFORE the target date
if (position.Status == PositionStatus.Finished || position.Status == PositionStatus.Flipped) if (position.Status == PositionStatus.Finished || position.Status == PositionStatus.Flipped)
{ {
// Stop Loss volume (if filled and on or before target date) // Stop Loss volume (if filled and on or before target date)
if (position.StopLoss?.Status == TradeStatus.Filled && position.StopLoss.Date.Date <= targetDate) if (position.StopLoss?.Status == TradeStatus.Filled && position.StopLoss.Date.Date <= targetDate)
{ {
closingVolume += position.StopLoss.Price * position.StopLoss.Quantity * position.StopLoss.Leverage; closingVolume += position.StopLoss.Price * position.StopLoss.Quantity * position.StopLoss.Leverage;
_logger.LogDebug("Position {PositionId}: Including SL closing volume {Volume} (filled on {Date})",
position.Identifier, position.StopLoss.Price * position.StopLoss.Quantity * position.StopLoss.Leverage, position.StopLoss.Date.Date);
}
else if (position.StopLoss?.Status == TradeStatus.Filled)
{
_logger.LogDebug("Position {PositionId}: Excluding SL closing volume (filled on {Date} > target {TargetDate})",
position.Identifier, position.StopLoss.Date.Date, targetDate);
} }
// Take Profit 1 volume (if filled and on or before target date) // Take Profit 1 volume (if filled and on or before target date)
if (position.TakeProfit1?.Status == TradeStatus.Filled && position.TakeProfit1.Date.Date <= targetDate) if (position.TakeProfit1?.Status == TradeStatus.Filled && position.TakeProfit1.Date.Date <= targetDate)
{ {
closingVolume += position.TakeProfit1.Price * position.TakeProfit1.Quantity * position.TakeProfit1.Leverage; closingVolume += position.TakeProfit1.Price * position.TakeProfit1.Quantity * position.TakeProfit1.Leverage;
_logger.LogDebug("Position {PositionId}: Including TP1 closing volume {Volume} (filled on {Date})",
position.Identifier, position.TakeProfit1.Price * position.TakeProfit1.Quantity * position.TakeProfit1.Leverage, position.TakeProfit1.Date.Date);
}
else if (position.TakeProfit1?.Status == TradeStatus.Filled)
{
_logger.LogDebug("Position {PositionId}: Excluding TP1 closing volume (filled on {Date} > target {TargetDate})",
position.Identifier, position.TakeProfit1.Date.Date, targetDate);
} }
// Take Profit 2 volume (if filled and on or before target date) // Take Profit 2 volume (if filled and on or before target date)
if (position.TakeProfit2?.Status == TradeStatus.Filled && position.TakeProfit2.Date.Date <= targetDate) if (position.TakeProfit2?.Status == TradeStatus.Filled && position.TakeProfit2.Date.Date <= targetDate)
{ {
closingVolume += position.TakeProfit2.Price * position.TakeProfit2.Quantity * position.TakeProfit2.Leverage; closingVolume += position.TakeProfit2.Price * position.TakeProfit2.Quantity * position.TakeProfit2.Leverage;
_logger.LogDebug("Position {PositionId}: Including TP2 closing volume {Volume} (filled on {Date})",
position.Identifier, position.TakeProfit2.Price * position.TakeProfit2.Quantity * position.TakeProfit2.Leverage, position.TakeProfit2.Date.Date);
}
else if (position.TakeProfit2?.Status == TradeStatus.Filled)
{
_logger.LogDebug("Position {PositionId}: Excluding TP2 closing volume (filled on {Date} > target {TargetDate})",
position.Identifier, position.TakeProfit2.Date.Date, targetDate);
} }
} }
// For positions that are still open, no closing volume yet
else if (position.IsOpen())
{
_logger.LogDebug("Position {PositionId}: Still open, no closing volume", position.Identifier);
}
// Total volume for this position = opening + closing // Total volume for this position = opening + closing (only what happened by target date)
var positionVolume = openVolume + closingVolume; var positionVolume = openVolume + closingVolume;
totalVolume += positionVolume; totalVolume += positionVolume;

View File

@@ -4501,6 +4501,7 @@ export interface RunBundleBacktestRequest {
dateTimeRanges: DateTimeRange[]; dateTimeRanges: DateTimeRange[];
moneyManagementVariants: MoneyManagementVariant[]; moneyManagementVariants: MoneyManagementVariant[];
tickerVariants: Ticker[]; tickerVariants: Ticker[];
saveAsTemplate: boolean;
} }
export interface BundleBacktestUniversalConfig { export interface BundleBacktestUniversalConfig {
@@ -4693,6 +4694,7 @@ export interface UpdateBotConfigRequest {
export interface TickerInfos { export interface TickerInfos {
ticker?: Ticker; ticker?: Ticker;
imageUrl?: string | null; imageUrl?: string | null;
name?: string | null;
} }
export interface SpotlightOverview { export interface SpotlightOverview {

View File

@@ -676,6 +676,7 @@ export interface RunBundleBacktestRequest {
dateTimeRanges: DateTimeRange[]; dateTimeRanges: DateTimeRange[];
moneyManagementVariants: MoneyManagementVariant[]; moneyManagementVariants: MoneyManagementVariant[];
tickerVariants: Ticker[]; tickerVariants: Ticker[];
saveAsTemplate: boolean;
} }
export interface BundleBacktestUniversalConfig { export interface BundleBacktestUniversalConfig {
@@ -868,6 +869,7 @@ export interface UpdateBotConfigRequest {
export interface TickerInfos { export interface TickerInfos {
ticker?: Ticker; ticker?: Ticker;
imageUrl?: string | null; imageUrl?: string | null;
name?: string | null;
} }
export interface SpotlightOverview { export interface SpotlightOverview {