Add more tests + Log pnl for each backtest

This commit is contained in:
2025-11-14 13:12:04 +07:00
parent 2548e9b757
commit d341ee05c9
11 changed files with 4163 additions and 97 deletions

View File

@@ -188,27 +188,10 @@ public class AgentGrain : Grain, IAgentGrain
var positions = (await _tradingService.GetPositionByUserIdAsync((int)this.GetPrimaryKeyLong()))
.Where(p => p.IsValidForMetrics()).ToList();
// Calculate aggregated statistics from position data
var totalPnL = positions.Sum(p => p.ProfitAndLoss?.Realized ?? 0);
var totalVolume = TradingBox.GetTotalVolumeTraded(positions);
var collateral = positions.Sum(p => p.Open.Price * p.Open.Quantity);
var totalFees = positions.Sum(p => p.CalculateTotalFees());
var metrics = TradingBox.CalculateAgentSummaryMetrics(positions);
// Store total fees in grain state for caching
_state.State.TotalFees = totalFees;
// Calculate wins/losses from position PnL
var totalWins = positions.Count(p => (p.ProfitAndLoss?.Net ?? 0) > 0);
var totalLosses = positions.Count(p => (p.ProfitAndLoss?.Net ?? 0) <= 0);
// Calculate ROI based on PnL minus fees
var netPnL = totalPnL - totalFees;
var totalROI = collateral switch
{
> 0 => (netPnL / collateral) * 100,
>= 0 => 0,
_ => 0
};
_state.State.TotalFees = metrics.TotalFees;
// Calculate total balance (USDC wallet + USDC in open positions value)
decimal totalBalance = 0;
@@ -274,16 +257,16 @@ public class AgentGrain : Grain, IAgentGrain
{
UserId = (int)this.GetPrimaryKeyLong(),
AgentName = _state.State.AgentName,
TotalPnL = totalPnL, // Gross PnL before fees
NetPnL = netPnL, // Net PnL after fees
Wins = totalWins,
Losses = totalLosses,
TotalROI = totalROI,
TotalPnL = metrics.TotalPnL, // Gross PnL before fees
NetPnL = metrics.NetPnL, // Net PnL after fees
Wins = metrics.Wins,
Losses = metrics.Losses,
TotalROI = metrics.TotalROI,
Runtime = runtime,
ActiveStrategiesCount = activeStrategies.Count(),
TotalVolume = totalVolume,
TotalVolume = metrics.TotalVolume,
TotalBalance = totalBalance,
TotalFees = totalFees,
TotalFees = metrics.TotalFees,
};
// Save summary to database
@@ -294,12 +277,13 @@ public class AgentGrain : Grain, IAgentGrain
await _state.WriteStateAsync();
// Insert balance tracking data
InsertBalanceTrackingData(totalBalance, botsAllocationUsdValue, netPnL, usdcWalletValue,
InsertBalanceTrackingData(totalBalance, botsAllocationUsdValue, metrics.NetPnL, usdcWalletValue,
usdcInPositionsValue);
_logger.LogDebug(
"Updated agent summary from position data for user {UserId}: NetPnL={NetPnL}, TotalPnL={TotalPnL}, Fees={Fees}, Volume={Volume}, Wins={Wins}, Losses={Losses}",
this.GetPrimaryKeyLong(), netPnL, totalPnL, totalFees, totalVolume, totalWins, totalLosses);
this.GetPrimaryKeyLong(), metrics.NetPnL, metrics.TotalPnL, metrics.TotalFees, metrics.TotalVolume,
metrics.Wins, metrics.Losses);
}
catch (Exception ex)
{

View File

@@ -812,17 +812,11 @@ public class TradingBotBase : ITradingBot
var currentTime = Config.IsForBacktest ? lastCandle.Date : DateTime.UtcNow;
var currentPnl = positionForSignal.ProfitAndLoss?.Net ?? 0;
var pnlPercentage = positionForSignal.Open.Price * positionForSignal.Open.Quantity != 0
? Math.Round((currentPnl / (positionForSignal.Open.Price * positionForSignal.Open.Quantity)) * 100,
2)
: 0;
var pnlPercentage = TradingBox.CalculatePnLPercentage(currentPnl, positionForSignal.Open.Price, positionForSignal.Open.Quantity);
var isPositionInProfit = positionForSignal.OriginDirection == TradeDirection.Long
? lastCandle.Close > positionForSignal.Open.Price
: lastCandle.Close < positionForSignal.Open.Price;
var isPositionInProfit = TradingBox.IsPositionInProfit(positionForSignal.Open.Price, lastCandle.Close, positionForSignal.OriginDirection);
var hasExceededTimeLimit = Config.MaxPositionTimeHours.HasValue &&
HasPositionExceededTimeLimit(positionForSignal, currentTime);
var hasExceededTimeLimit = TradingBox.HasPositionExceededTimeLimit(positionForSignal.Open.Date, currentTime, Config.MaxPositionTimeHours);
if (hasExceededTimeLimit)
{
@@ -1246,29 +1240,16 @@ public class TradingBotBase : ITradingBot
.Take(Config.MaxLossStreak)
.ToList();
// If we don't have enough positions to form a streak, we can open
if (recentPositions.Count < Config.MaxLossStreak)
{
return true;
}
// Check if all recent positions were losses
var allLosses = recentPositions.All(p => p.ProfitAndLoss?.Realized < 0);
if (!allLosses)
{
return true;
}
// If we have a loss streak, check if the last position was in the same direction as the signal
var lastPosition = recentPositions.First();
if (lastPosition.OriginDirection == signal.Direction)
var canOpen = TradingBox.CheckLossStreak(recentPositions, Config.MaxLossStreak, signal.Direction);
if (!canOpen)
{
var lastPosition = recentPositions.First();
await LogWarning(
$"🔥 Loss Streak Limit\nCannot open position\nMax loss streak: `{Config.MaxLossStreak}` reached\n📉 Last `{recentPositions.Count}` trades were losses\n🎯 Last position: `{lastPosition.OriginDirection}`\nWaiting for opposite direction signal");
return false;
}
return true;
return canOpen;
}
private async Task<bool> CheckBrokerPositions()
@@ -1869,17 +1850,9 @@ public class TradingBotBase : ITradingBot
if (pnlCalculated && closingPrice > 0)
{
var entryPrice = position.Open.Price;
var positionSize = position.Open.Quantity * position.Open.Leverage;
var positionSize = TradingBox.CalculatePositionSize(position.Open.Quantity, position.Open.Leverage);
decimal pnl;
if (position.OriginDirection == TradeDirection.Long)
{
pnl = (closingPrice - entryPrice) * positionSize;
}
else
{
pnl = (entryPrice - closingPrice) * positionSize;
}
decimal pnl = TradingBox.CalculatePnL(entryPrice, closingPrice, position.Open.Quantity, position.Open.Leverage, position.OriginDirection);
if (position.ProfitAndLoss == null)
{
@@ -1901,7 +1874,7 @@ public class TradingBotBase : ITradingBot
$"Entry Price: `${entryPrice:F2}` | Exit Price: `${closingPrice:F2}`\n" +
$"Position Size: `{position.Open.Quantity:F8}` | Leverage: `{position.Open.Leverage}x`\n" +
$"Position Value: `${positionSize:F8}`\n" +
$"Price Difference: `${(position.OriginDirection == TradeDirection.Long ? closingPrice - entryPrice : entryPrice - closingPrice):F2}`\n" +
$"Price Difference: `${TradingBox.CalculatePriceDifference(entryPrice, closingPrice, position.OriginDirection):F2}`\n" +
$"Realized P&L: `${pnl:F2}`\n" +
$"Gas Fees: `${position.GasFees:F2}` | UI Fees: `${position.UiFees:F2}`\n" +
$"Total Fees: `${position.GasFees + position.UiFees:F2}`\n" +
@@ -2308,18 +2281,6 @@ public class TradingBotBase : ITradingBot
/// <param name="position">The position to check</param>
/// <param name="currentTime">The current time to compare against</param>
/// <returns>True if the position has exceeded the time limit, false otherwise</returns>
private bool HasPositionExceededTimeLimit(Position position, DateTime currentTime)
{
if (!Config.MaxPositionTimeHours.HasValue || Config.MaxPositionTimeHours.Value <= 0)
{
return false; // Time-based closure is disabled
}
var timeOpen = currentTime - position.Open.Date;
var maxTimeAllowed = TimeSpan.FromHours((double)Config.MaxPositionTimeHours.Value);
return timeOpen >= maxTimeAllowed;
}
/// <summary>
/// Updates the trading bot configuration with new settings.
@@ -2630,8 +2591,7 @@ public class TradingBotBase : ITradingBot
}
// Calculate cooldown end time based on last position closing time
var baseIntervalSeconds = CandleHelpers.GetBaseIntervalInSeconds(Config.Timeframe);
var cooldownEndTime = LastPositionClosingTime.Value.AddSeconds(baseIntervalSeconds * Config.CooldownPeriod);
var cooldownEndTime = TradingBox.CalculateCooldownEndTime(LastPositionClosingTime.Value, Config.Timeframe, Config.CooldownPeriod);
var isInCooldown = (Config.IsForBacktest ? LastCandle.Date : DateTime.UtcNow) < cooldownEndTime;
if (isInCooldown)