From af08462e59d43f2479353dbcf6b6f3c629c90413 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Tue, 21 Oct 2025 16:38:51 +0700 Subject: [PATCH] Add save only for bundle backtest --- .../Controllers/BacktestController.cs | 2 +- .../Requests/RunBundleBacktestRequest.cs | 6 + .../Services/IBacktester.cs | 4 +- .../Backtests/Backtester.cs | 18 +-- .../Grains/PlatformSummaryGrain.cs | 131 ++++++++++++++---- .../src/generated/ManagingApi.ts | 2 + .../src/generated/ManagingApiTypes.ts | 2 + 7 files changed, 120 insertions(+), 45 deletions(-) diff --git a/src/Managing.Api/Controllers/BacktestController.cs b/src/Managing.Api/Controllers/BacktestController.cs index 797c10da..e67e7f7c 100644 --- a/src/Managing.Api/Controllers/BacktestController.cs +++ b/src/Managing.Api/Controllers/BacktestController.cs @@ -655,7 +655,7 @@ public class BacktestController : BaseController Name = request.Name }; - _backtester.InsertBundleBacktestRequestForUser(user, bundleRequest); + await _backtester.InsertBundleBacktestRequestForUserAsync(user, bundleRequest, request.SaveAsTemplate); return Ok(bundleRequest); } catch (Exception ex) diff --git a/src/Managing.Api/Models/Requests/RunBundleBacktestRequest.cs b/src/Managing.Api/Models/Requests/RunBundleBacktestRequest.cs index e34304fc..c580fa8c 100644 --- a/src/Managing.Api/Models/Requests/RunBundleBacktestRequest.cs +++ b/src/Managing.Api/Models/Requests/RunBundleBacktestRequest.cs @@ -38,4 +38,10 @@ public class RunBundleBacktestRequest /// [Required] public List TickerVariants { get; set; } = new(); + + /// + /// Save only the request as a template + /// + [Required] + public bool SaveAsTemplate { get; set; } = false; } \ No newline at end of file diff --git a/src/Managing.Application.Abstractions/Services/IBacktester.cs b/src/Managing.Application.Abstractions/Services/IBacktester.cs index 48d84e20..2c22274d 100644 --- a/src/Managing.Application.Abstractions/Services/IBacktester.cs +++ b/src/Managing.Application.Abstractions/Services/IBacktester.cs @@ -84,9 +84,7 @@ namespace Managing.Application.Abstractions.Services Task DeleteBacktestsByRequestIdAsync(Guid requestId); Task DeleteBacktestsByFiltersAsync(User user, BacktestsFilter filter); - // Bundle backtest methods - void InsertBundleBacktestRequestForUser(User user, BundleBacktestRequest bundleRequest); - Task InsertBundleBacktestRequestForUserAsync(User user, BundleBacktestRequest bundleRequest); + Task InsertBundleBacktestRequestForUserAsync(User user, BundleBacktestRequest bundleRequest, bool saveAsTemplate = false); IEnumerable GetBundleBacktestRequestsByUser(User user); Task> GetBundleBacktestRequestsByUserAsync(User user); BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, Guid id); diff --git a/src/Managing.Application/Backtests/Backtester.cs b/src/Managing.Application/Backtests/Backtester.cs index 411cb0d3..f93281e5 100644 --- a/src/Managing.Application/Backtests/Backtester.cs +++ b/src/Managing.Application/Backtests/Backtester.cs @@ -455,21 +455,15 @@ namespace Managing.Application.Backtests return (backtests, totalCount); } - // Bundle backtest methods - 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) + public async Task InsertBundleBacktestRequestForUserAsync(User user, BundleBacktestRequest bundleRequest, bool saveAsTemplate = false) { await _backtestRepository.InsertBundleBacktestRequestForUserAsync(user, bundleRequest); - // Trigger the BundleBacktestGrain to process this request - await TriggerBundleBacktestGrainAsync(bundleRequest.RequestId); + if (!saveAsTemplate) + { + // Trigger the BundleBacktestGrain to process this request + await TriggerBundleBacktestGrainAsync(bundleRequest.RequestId); + } } public IEnumerable GetBundleBacktestRequestsByUser(User user) diff --git a/src/Managing.Application/Grains/PlatformSummaryGrain.cs b/src/Managing.Application/Grains/PlatformSummaryGrain.cs index 24be8d0a..0f1e3657 100644 --- a/src/Managing.Application/Grains/PlatformSummaryGrain.cs +++ b/src/Managing.Application/Grains/PlatformSummaryGrain.cs @@ -3,7 +3,6 @@ using Managing.Application.Abstractions.Grains; using Managing.Application.Abstractions.Services; using Managing.Application.Orleans; using Managing.Domain.Candles; -using Managing.Domain.Shared.Helpers; using Managing.Domain.Trades; using Microsoft.Extensions.Logging; using static Managing.Common.Enums; @@ -174,10 +173,33 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable { if (!position.IsValidForMetrics()) continue; - // Calculate volume using the dedicated method - var positionVolume = TradingHelpers.GetVolumeForPosition(position); + // Calculate volume using the same logic as daily snapshots for consistency + // 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; + + // 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 for debugging + // Track total volume from ALL positions (this is the true cumulative volume) totalVolumeFromAllPositions += positionVolume; // 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"); - // 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 { - Date = DateTime.UtcNow.Date, + Date = today, TotalAgents = _state.State.TotalAgents, TotalStrategies = _state.State.TotalActiveStrategies, TotalVolume = _state.State.TotalPlatformVolume, @@ -388,20 +418,33 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable NetPnL = _state.State.NetPnL, TotalOpenInterest = _state.State.OpenInterest, 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 var cutoff = DateTime.UtcNow.AddDays(-60); _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 _state.State.VolumeUpdatedByEvents = false; await _state.WriteStateAsync(); + + _logger.LogInformation("Daily snapshot complete. Total snapshots: {Count}", _state.State.DailySnapshots.Count); } private bool IsDataStale() @@ -516,27 +559,31 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable // Get all positions to calculate missing snapshots 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; // Determine the start date for gap filling - var startDate = _state.State.DailySnapshots.Any() - ? _state.State.DailySnapshots.Max(s => s.Date).AddDays(1) - : earliestPositionDate; + DateTime startDate; + if (_state.State.DailySnapshots.Any()) + { + 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 - var endDate = today > latestPositionDate ? today : latestPositionDate; + // IMPORTANT: Fill snapshots up to TODAY (not just up to latest position date) + // 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(); 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 foreach (var missingDate in missingDates) { - var snapshot = - await CalculateDailySnapshotFromPositionsAsync(positions.Where(p => p.IsValidForMetrics()).ToList(), - missingDate); + var validPositions = positions.Where(p => p.IsValidForMetrics()).ToList(); + var snapshot = await CalculateDailySnapshotFromPositionsAsync(validPositions, missingDate); _state.State.DailySnapshots.Add(snapshot); _logger.LogInformation( @@ -596,7 +642,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable private async Task CalculateDailySnapshotFromPositionsAsync(List positions, 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 totalFees = 0m; var totalPnL = 0m; @@ -625,28 +671,55 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable var closingVolume = 0m; // 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) { // Stop Loss volume (if filled and on or before target date) if (position.StopLoss?.Status == TradeStatus.Filled && position.StopLoss.Date.Date <= targetDate) { 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) if (position.TakeProfit1?.Status == TradeStatus.Filled && position.TakeProfit1.Date.Date <= targetDate) { 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) if (position.TakeProfit2?.Status == TradeStatus.Filled && position.TakeProfit2.Date.Date <= targetDate) { 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; totalVolume += positionVolume; diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts index 38a6002b..180455ee 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -4501,6 +4501,7 @@ export interface RunBundleBacktestRequest { dateTimeRanges: DateTimeRange[]; moneyManagementVariants: MoneyManagementVariant[]; tickerVariants: Ticker[]; + saveAsTemplate: boolean; } export interface BundleBacktestUniversalConfig { @@ -4693,6 +4694,7 @@ export interface UpdateBotConfigRequest { export interface TickerInfos { ticker?: Ticker; imageUrl?: string | null; + name?: string | null; } export interface SpotlightOverview { diff --git a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts index 790cef43..9a59198c 100644 --- a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts +++ b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts @@ -676,6 +676,7 @@ export interface RunBundleBacktestRequest { dateTimeRanges: DateTimeRange[]; moneyManagementVariants: MoneyManagementVariant[]; tickerVariants: Ticker[]; + saveAsTemplate: boolean; } export interface BundleBacktestUniversalConfig { @@ -868,6 +869,7 @@ export interface UpdateBotConfigRequest { export interface TickerInfos { ticker?: Ticker; imageUrl?: string | null; + name?: string | null; } export interface SpotlightOverview {