Add save only for bundle backtest
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -455,22 +455,16 @@ 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);
|
||||||
|
|
||||||
|
if (!saveAsTemplate)
|
||||||
|
{
|
||||||
// Trigger the BundleBacktestGrain to process this request
|
// Trigger the BundleBacktestGrain to process this request
|
||||||
await TriggerBundleBacktestGrainAsync(bundleRequest.RequestId);
|
await TriggerBundleBacktestGrainAsync(bundleRequest.RequestId);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByUser(User user)
|
public IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByUser(User user)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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);
|
_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;
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user