Add more tests + Log pnl for each backtest
This commit is contained in:
@@ -561,6 +561,40 @@ public static class TradingBox
|
||||
return totalVolume;
|
||||
}
|
||||
|
||||
public record AgentSummaryMetrics(
|
||||
decimal TotalPnL,
|
||||
decimal NetPnL,
|
||||
decimal TotalROI,
|
||||
decimal TotalVolume,
|
||||
int Wins,
|
||||
int Losses,
|
||||
decimal TotalFees,
|
||||
decimal Collateral);
|
||||
|
||||
public static AgentSummaryMetrics CalculateAgentSummaryMetrics(List<Position> positions)
|
||||
{
|
||||
var validPositions = positions?
|
||||
.Where(p => p.IsValidForMetrics())
|
||||
.ToList() ?? new List<Position>();
|
||||
|
||||
if (!validPositions.Any())
|
||||
{
|
||||
return new AgentSummaryMetrics(0m, 0m, 0m, 0m, 0, 0, 0m, 0m);
|
||||
}
|
||||
|
||||
var totalPnL = validPositions.Sum(p => p.ProfitAndLoss?.Realized ?? 0m);
|
||||
var totalFees = validPositions.Sum(p => p.CalculateTotalFees());
|
||||
var netPnL = totalPnL - totalFees;
|
||||
var totalVolume = GetTotalVolumeTraded(validPositions);
|
||||
var wins = validPositions.Count(p => (p.ProfitAndLoss?.Net ?? 0m) > 0m);
|
||||
var losses = validPositions.Count(p => (p.ProfitAndLoss?.Net ?? 0m) <= 0m);
|
||||
var collateral = validPositions.Sum(p => (p.Open?.Price ?? 0m) * (p.Open?.Quantity ?? 0m));
|
||||
var totalROI = collateral > 0m ? (netPnL / collateral) * 100m : 0m;
|
||||
|
||||
return new AgentSummaryMetrics(totalPnL, netPnL, totalROI, totalVolume, wins, losses, totalFees,
|
||||
collateral);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the volume traded in the last 24 hours
|
||||
/// </summary>
|
||||
@@ -749,7 +783,7 @@ public static class TradingBox
|
||||
var buildedIndicator = ScenarioHelpers.BuildIndicator(ScenarioHelpers.BaseToLight(indicator));
|
||||
indicatorsValues[indicator.Type] = buildedIndicator.GetIndicatorValues(candles);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
// Removed logging for performance in static method
|
||||
// Consider adding logging back if error handling is needed
|
||||
@@ -980,4 +1014,177 @@ public static class TradingBox
|
||||
|
||||
return totalVolume;
|
||||
}
|
||||
|
||||
#region TradingBot Calculations - Extracted from TradingBotBase for testability
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the position size (quantity * leverage)
|
||||
/// </summary>
|
||||
/// <param name="quantity">The quantity of the position</param>
|
||||
/// <param name="leverage">The leverage multiplier</param>
|
||||
/// <returns>The position size</returns>
|
||||
public static decimal CalculatePositionSize(decimal quantity, decimal leverage)
|
||||
{
|
||||
return quantity * leverage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the profit and loss for a position based on entry/exit prices, quantity, leverage, and direction
|
||||
/// </summary>
|
||||
/// <param name="entryPrice">The entry price of the position</param>
|
||||
/// <param name="exitPrice">The exit price of the position</param>
|
||||
/// <param name="quantity">The quantity of the position</param>
|
||||
/// <param name="leverage">The leverage multiplier</param>
|
||||
/// <param name="direction">The trade direction (Long or Short)</param>
|
||||
/// <returns>The calculated PnL</returns>
|
||||
public static decimal CalculatePnL(decimal entryPrice, decimal exitPrice, decimal quantity, decimal leverage, TradeDirection direction)
|
||||
{
|
||||
var positionSize = CalculatePositionSize(quantity, leverage);
|
||||
|
||||
if (direction == TradeDirection.Long)
|
||||
{
|
||||
return (exitPrice - entryPrice) * positionSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
return (entryPrice - exitPrice) * positionSize;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the price difference based on direction
|
||||
/// For Long: exitPrice - entryPrice
|
||||
/// For Short: entryPrice - exitPrice
|
||||
/// </summary>
|
||||
/// <param name="entryPrice">The entry price</param>
|
||||
/// <param name="exitPrice">The exit price</param>
|
||||
/// <param name="direction">The trade direction</param>
|
||||
/// <returns>The price difference</returns>
|
||||
public static decimal CalculatePriceDifference(decimal entryPrice, decimal exitPrice, TradeDirection direction)
|
||||
{
|
||||
if (direction == TradeDirection.Long)
|
||||
{
|
||||
return exitPrice - entryPrice;
|
||||
}
|
||||
else
|
||||
{
|
||||
return entryPrice - exitPrice;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the PnL percentage (ROI) based on current PnL, entry price, and quantity
|
||||
/// Returns 0 if entry price * quantity is 0 to avoid division by zero
|
||||
/// </summary>
|
||||
/// <param name="pnl">The current profit and loss</param>
|
||||
/// <param name="entryPrice">The entry price</param>
|
||||
/// <param name="quantity">The quantity</param>
|
||||
/// <returns>The PnL percentage rounded to 2 decimal places</returns>
|
||||
public static decimal CalculatePnLPercentage(decimal pnl, decimal entryPrice, decimal quantity)
|
||||
{
|
||||
var denominator = entryPrice * quantity;
|
||||
if (denominator == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.Round((pnl / denominator) * 100, 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a position is currently in profit based on entry price, current price, and direction
|
||||
/// </summary>
|
||||
/// <param name="entryPrice">The entry price</param>
|
||||
/// <param name="currentPrice">The current market price</param>
|
||||
/// <param name="direction">The trade direction</param>
|
||||
/// <returns>True if position is in profit, false otherwise</returns>
|
||||
public static bool IsPositionInProfit(decimal entryPrice, decimal currentPrice, TradeDirection direction)
|
||||
{
|
||||
if (direction == TradeDirection.Long)
|
||||
{
|
||||
return currentPrice > entryPrice;
|
||||
}
|
||||
else
|
||||
{
|
||||
return currentPrice < entryPrice;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the cooldown end time based on last position closing time, timeframe, and cooldown period
|
||||
/// </summary>
|
||||
/// <param name="lastClosingTime">The time when the last position was closed</param>
|
||||
/// <param name="timeframe">The trading timeframe</param>
|
||||
/// <param name="cooldownPeriod">The cooldown period in candles</param>
|
||||
/// <returns>The DateTime when the cooldown period ends</returns>
|
||||
public static DateTime CalculateCooldownEndTime(DateTime lastClosingTime, Timeframe timeframe, int cooldownPeriod)
|
||||
{
|
||||
var baseIntervalSeconds = CandleHelpers.GetBaseIntervalInSeconds(timeframe);
|
||||
return lastClosingTime.AddSeconds(baseIntervalSeconds * cooldownPeriod);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a position has exceeded the maximum time limit
|
||||
/// </summary>
|
||||
/// <param name="openDate">The date when the position was opened</param>
|
||||
/// <param name="currentTime">The current time</param>
|
||||
/// <param name="maxHours">The maximum hours the position can be open (nullable, null or 0 means no limit)</param>
|
||||
/// <returns>True if position has exceeded time limit, false otherwise</returns>
|
||||
public static bool HasPositionExceededTimeLimit(DateTime openDate, DateTime currentTime, decimal? maxHours)
|
||||
{
|
||||
if (!maxHours.HasValue || maxHours.Value <= 0)
|
||||
{
|
||||
return false; // Time-based closure is disabled
|
||||
}
|
||||
|
||||
var timeOpen = currentTime - openDate;
|
||||
var maxTimeAllowed = TimeSpan.FromHours((double)maxHours.Value);
|
||||
|
||||
return timeOpen >= maxTimeAllowed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if opening a new position should be blocked due to loss streak
|
||||
/// Returns false (block) if:
|
||||
/// - MaxLossStreak > 0 (limit is enabled)
|
||||
/// - We have at least maxLossStreak recent finished positions
|
||||
/// - All recent positions were losses
|
||||
/// - The last position was in the same direction as the signal
|
||||
/// </summary>
|
||||
/// <param name="recentPositions">List of recent finished positions, ordered by date descending (most recent first)</param>
|
||||
/// <param name="maxLossStreak">Maximum allowed loss streak (0 or negative means no limit)</param>
|
||||
/// <param name="signalDirection">The direction of the signal for the new position</param>
|
||||
/// <returns>True if position can be opened, false if blocked by loss streak</returns>
|
||||
public static bool CheckLossStreak(List<Position> recentPositions, int maxLossStreak, TradeDirection signalDirection)
|
||||
{
|
||||
// If MaxLossStreak is 0, there's no limit
|
||||
if (maxLossStreak <= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we don't have enough positions to form a streak, we can open
|
||||
if (recentPositions.Count < 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 == signalDirection)
|
||||
{
|
||||
return false; // Block same direction after loss streak
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user