Update scoring system
This commit is contained in:
109
README.md
109
README.md
@@ -381,6 +381,115 @@ This configuration allows for more aggressive trading strategies while maintaini
|
|||||||
- **Initial Balance**: Starting capital for backtest
|
- **Initial Balance**: Starting capital for backtest
|
||||||
- **Advanced Config**: All bot parameters (time limits, profit control, etc.)
|
- **Advanced Config**: All bot parameters (time limits, profit control, etc.)
|
||||||
|
|
||||||
|
### Backtest Scoring System
|
||||||
|
|
||||||
|
The backtest scoring system evaluates strategy performance using a comprehensive multi-factor approach with weighted components and dynamic penalties. The final score ranges from 0-100, where 100 represents optimal performance.
|
||||||
|
|
||||||
|
#### Scoring Components (Weighted Distribution)
|
||||||
|
|
||||||
|
| Component | Weight | Description |
|
||||||
|
|-----------|--------|-------------|
|
||||||
|
| **Growth Percentage** | 25% | Primary performance metric based on total return |
|
||||||
|
| **Sharpe Ratio** | 15% | Risk-adjusted return measure |
|
||||||
|
| **Max Drawdown (USD)** | 12% | Maximum capital loss in absolute terms |
|
||||||
|
| **Win Rate** | 15% | Percentage of profitable trades (weighted by trade count) |
|
||||||
|
| **Profitability Bonus** | 8% | Additional reward for positive returns |
|
||||||
|
| **Hodl Comparison** | 5% | Performance vs buy-and-hold strategy |
|
||||||
|
| **Trade Count** | 5% | Sufficient trading activity validation |
|
||||||
|
| **Recovery Time** | 2% | Time to recover from maximum drawdown |
|
||||||
|
| **Test Duration** | 3% | Adequate testing period validation |
|
||||||
|
| **Fees Impact** | 2% | Trading cost efficiency |
|
||||||
|
|
||||||
|
#### Component Scoring Details
|
||||||
|
|
||||||
|
**Growth Percentage (25%)**
|
||||||
|
- **Negative Returns**: Linear penalty (20 + growth% × 1.5)
|
||||||
|
- **0-5%**: Linear scale (0-40 points)
|
||||||
|
- **5-10%**: Accelerated scale (40-100 points)
|
||||||
|
- **10%+**: Full score (100 points)
|
||||||
|
|
||||||
|
**Sharpe Ratio (15%)**
|
||||||
|
- **Negative**: 0 points
|
||||||
|
- **0-4**: Linear scale (0-100 points)
|
||||||
|
- **4+**: Full score (100 points)
|
||||||
|
|
||||||
|
**Max Drawdown USD (12%)**
|
||||||
|
- **0-30%**: Exponential penalty (100 - (drawdown%/30 × 100)^1.5)
|
||||||
|
- **30%+**: 0 points
|
||||||
|
|
||||||
|
**Win Rate (15%)**
|
||||||
|
- **Base Score**: Win rate percentage
|
||||||
|
- **Trade Count Factor**: Full significance at 55+ trades, reduced for fewer trades
|
||||||
|
- **Minimum Trade Penalty**: 50% penalty for <10 trades
|
||||||
|
|
||||||
|
**Profitability Bonus (8%)**
|
||||||
|
- **Positive Returns**: Logarithmic bonus (50 × (1 - 1/(1 + growth%/30)))
|
||||||
|
- **Negative Returns**: 0 points
|
||||||
|
|
||||||
|
**Hodl Comparison (5%)**
|
||||||
|
- **Outperforms Hodl**: 0-80 points based on margin
|
||||||
|
- **Underperforms Hodl**: 0-20 points based on underperformance
|
||||||
|
|
||||||
|
**Trade Count (5%)**
|
||||||
|
- **<5 trades**: 0 points
|
||||||
|
- **5-10 trades**: Linear scale (0-50 points)
|
||||||
|
- **10-50 trades**: Linear scale (50-100 points)
|
||||||
|
- **50+ trades**: 100 points
|
||||||
|
|
||||||
|
**Recovery Time (2%)**
|
||||||
|
- **Timeframe-adjusted expectations**:
|
||||||
|
- 5m: 3 days max recovery
|
||||||
|
- 15m: 5 days max recovery
|
||||||
|
- 30m: 10 days max recovery
|
||||||
|
- 1h: 15 days max recovery
|
||||||
|
- 4h: 30 days max recovery
|
||||||
|
- 1d: 90 days max recovery
|
||||||
|
|
||||||
|
**Test Duration (3%)**
|
||||||
|
- **Timeframe-adjusted minimums**:
|
||||||
|
- 5m: 14 days minimum
|
||||||
|
- 15m: 28 days minimum
|
||||||
|
- 30m: 56 days minimum
|
||||||
|
- 1h: 84 days minimum
|
||||||
|
- 4h: 120 days minimum
|
||||||
|
- 1d: 90 days minimum
|
||||||
|
- **Optimal duration**: 3× minimum duration
|
||||||
|
|
||||||
|
**Fees Impact (2%)**
|
||||||
|
- **0-2% fees**: Linear penalty (100-50 points)
|
||||||
|
- **2-5% fees**: Linear penalty (50-0 points)
|
||||||
|
- **5%+ fees**: 0 points
|
||||||
|
- **Fees > PnL**: 0 points
|
||||||
|
|
||||||
|
#### Dynamic Penalty System
|
||||||
|
|
||||||
|
The scoring system applies dynamic penalties based on performance thresholds:
|
||||||
|
|
||||||
|
**Profitability Rules**
|
||||||
|
- **Negative Growth**: 10% penalty per 1% loss
|
||||||
|
- **Negative Absolute PnL**: 70% penalty
|
||||||
|
- **Low Win Rate**: 50% penalty per 10% below 30% (for 10+ trades)
|
||||||
|
- **Low Profit**: 10% penalty per 1% below 2% (for 5+ trades)
|
||||||
|
- **High Drawdown**: 2% penalty per 1% above 20%
|
||||||
|
- **Short Test Duration**: 2% penalty per day below 30 days
|
||||||
|
|
||||||
|
**Special Rules**
|
||||||
|
- **No Positions**: Automatic 0 score
|
||||||
|
- **Score Clamping**: Final score clamped between 0-100
|
||||||
|
- **Error Handling**: Returns 0 for any calculation errors
|
||||||
|
|
||||||
|
#### Scoring Philosophy
|
||||||
|
|
||||||
|
The system prioritizes:
|
||||||
|
1. **Consistent profitability** over high-risk gains
|
||||||
|
2. **Risk management** through drawdown control
|
||||||
|
3. **Statistical significance** through adequate trade counts
|
||||||
|
4. **Timeframe-appropriate** expectations for recovery and duration
|
||||||
|
5. **Cost efficiency** through fee management
|
||||||
|
6. **Realistic performance** through dynamic penalties
|
||||||
|
|
||||||
|
This comprehensive approach ensures that high-scoring strategies demonstrate robust, sustainable performance across multiple dimensions rather than relying on single metrics or short-term luck.
|
||||||
|
|
||||||
### RunBacktestRequest Structure
|
### RunBacktestRequest Structure
|
||||||
|
|
||||||
The backtest request supports both saved and dynamic configurations:
|
The backtest request supports both saved and dynamic configurations:
|
||||||
|
|||||||
@@ -682,7 +682,13 @@ namespace Managing.Application.Tests
|
|||||||
totalPnL: (double)backtestResult.FinalPnl,
|
totalPnL: (double)backtestResult.FinalPnl,
|
||||||
fees: (double)backtestResult.Fees,
|
fees: (double)backtestResult.Fees,
|
||||||
tradeCount: backtestResult.Positions?.Count ?? 0,
|
tradeCount: backtestResult.Positions?.Count ?? 0,
|
||||||
maxDrawdownRecoveryTime: backtestResult.Statistics?.MaxDrawdownRecoveryTime ?? TimeSpan.Zero
|
maxDrawdownRecoveryTime: backtestResult.Statistics?.MaxDrawdownRecoveryTime ?? TimeSpan.Zero,
|
||||||
|
maxDrawdown: backtestResult.Statistics?.MaxDrawdown ?? 0,
|
||||||
|
initialBalance: config.BotTradingBalance,
|
||||||
|
startDate: backtestResult.StartDate,
|
||||||
|
endDate: backtestResult.EndDate,
|
||||||
|
feesPaid: backtestResult.Fees,
|
||||||
|
timeframe: config.Timeframe
|
||||||
);
|
);
|
||||||
|
|
||||||
var scenarioResult = new ScenarioBacktestResult
|
var scenarioResult = new ScenarioBacktestResult
|
||||||
|
|||||||
@@ -143,7 +143,8 @@ namespace Managing.Application.Backtesting
|
|||||||
tradingBot.User = user;
|
tradingBot.User = user;
|
||||||
await tradingBot.LoadAccount();
|
await tradingBot.LoadAccount();
|
||||||
|
|
||||||
var result = await GetBacktestingResult(config, tradingBot, candles, user, withCandles, requestId, metadata);
|
var result =
|
||||||
|
await GetBacktestingResult(config, tradingBot, candles, user, withCandles, requestId, metadata);
|
||||||
|
|
||||||
if (user != null)
|
if (user != null)
|
||||||
{
|
{
|
||||||
@@ -255,7 +256,12 @@ namespace Managing.Application.Backtesting
|
|||||||
totalPnL: (double)finalPnl,
|
totalPnL: (double)finalPnl,
|
||||||
fees: (double)fees,
|
fees: (double)fees,
|
||||||
tradeCount: bot.Positions.Count,
|
tradeCount: bot.Positions.Count,
|
||||||
maxDrawdownRecoveryTime: stats.MaxDrawdownRecoveryTime
|
maxDrawdownRecoveryTime: stats.MaxDrawdownRecoveryTime,
|
||||||
|
maxDrawdown: stats.MaxDrawdown,
|
||||||
|
initialBalance: config.BotTradingBalance,
|
||||||
|
startDate: candles[0].Date,
|
||||||
|
endDate: candles.Last().Date,
|
||||||
|
timeframe: config.Timeframe
|
||||||
);
|
);
|
||||||
|
|
||||||
var score = BacktestScorer.CalculateTotalScore(scoringParams);
|
var score = BacktestScorer.CalculateTotalScore(scoringParams);
|
||||||
|
|||||||
@@ -813,7 +813,7 @@ public class TradingBotFitness : IFitness
|
|||||||
).Result;
|
).Result;
|
||||||
|
|
||||||
// Calculate multi-objective fitness based on backtest results
|
// Calculate multi-objective fitness based on backtest results
|
||||||
var fitness = CalculateMultiObjectiveFitness(backtest, config);
|
var fitness = CalculateFitness(backtest, config);
|
||||||
|
|
||||||
return fitness;
|
return fitness;
|
||||||
}
|
}
|
||||||
@@ -824,38 +824,13 @@ public class TradingBotFitness : IFitness
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private double CalculateMultiObjectiveFitness(Backtest backtest, TradingBotConfig config)
|
private double CalculateFitness(Backtest backtest, TradingBotConfig config)
|
||||||
{
|
{
|
||||||
if (backtest == null || backtest.Statistics == null)
|
if (backtest == null || backtest.Statistics == null)
|
||||||
return 0.1;
|
return 0.1;
|
||||||
|
|
||||||
var stats = backtest.Statistics;
|
// Use the comprehensive backtest score directly as fitness
|
||||||
|
// The BacktestScorer already includes all important metrics with proper weighting
|
||||||
// Multi-objective fitness function (matching frontend)
|
return backtest.Score;
|
||||||
var pnlScore = Math.Max(0, (double)stats.TotalPnL / 1000); // Normalize PnL
|
|
||||||
var winRateScore = backtest.WinRate / 100.0; // Normalize win rate
|
|
||||||
var riskRewardScore =
|
|
||||||
Math.Min(2, (double)stats.WinningTrades / Math.Max(1, Math.Abs((double)stats.LoosingTrades)));
|
|
||||||
var consistencyScore = 1 - Math.Abs((double)stats.TotalPnL - (double)backtest.FinalPnl) /
|
|
||||||
Math.Max(1, Math.Abs((double)stats.TotalPnL));
|
|
||||||
|
|
||||||
// Risk-reward ratio bonus
|
|
||||||
var riskRewardRatio = (double)(config.MoneyManagement.TakeProfit / config.MoneyManagement.StopLoss);
|
|
||||||
var riskRewardBonus = Math.Min(0.2, (riskRewardRatio - 1.1) * 0.1);
|
|
||||||
|
|
||||||
// Drawdown score (normalized to 0-1, where lower drawdown is better)
|
|
||||||
var maxDrawdownPc = Math.Abs((double)stats.MaxDrawdownPc);
|
|
||||||
var drawdownScore = Math.Max(0, 1 - (maxDrawdownPc / 50));
|
|
||||||
|
|
||||||
// Weighted combination
|
|
||||||
var fitness =
|
|
||||||
pnlScore * 0.3 +
|
|
||||||
winRateScore * 0.2 +
|
|
||||||
riskRewardScore * 0.2 +
|
|
||||||
consistencyScore * 0.1 +
|
|
||||||
riskRewardBonus * 0.1 +
|
|
||||||
drawdownScore * 0.1;
|
|
||||||
|
|
||||||
return Math.Max(0, fitness);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
namespace Managing.Domain.Backtests;
|
namespace Managing.Domain.Backtests;
|
||||||
|
|
||||||
public class BacktestScoringParams
|
public class BacktestScoringParams
|
||||||
@@ -11,6 +13,14 @@ public class BacktestScoringParams
|
|||||||
public double Fees { get; }
|
public double Fees { get; }
|
||||||
public int TradeCount { get; }
|
public int TradeCount { get; }
|
||||||
public TimeSpan MaxDrawdownRecoveryTime { get; }
|
public TimeSpan MaxDrawdownRecoveryTime { get; }
|
||||||
|
|
||||||
|
// New properties for enhanced scoring
|
||||||
|
public decimal MaxDrawdown { get; }
|
||||||
|
public decimal InitialBalance { get; }
|
||||||
|
public DateTime StartDate { get; }
|
||||||
|
public DateTime EndDate { get; }
|
||||||
|
public decimal FeesPaid { get; }
|
||||||
|
public Timeframe Timeframe { get; }
|
||||||
|
|
||||||
public BacktestScoringParams(
|
public BacktestScoringParams(
|
||||||
double sharpeRatio,
|
double sharpeRatio,
|
||||||
@@ -21,7 +31,12 @@ public class BacktestScoringParams
|
|||||||
double totalPnL,
|
double totalPnL,
|
||||||
double fees,
|
double fees,
|
||||||
int tradeCount,
|
int tradeCount,
|
||||||
TimeSpan maxDrawdownRecoveryTime)
|
TimeSpan maxDrawdownRecoveryTime,
|
||||||
|
decimal maxDrawdown = 0,
|
||||||
|
decimal initialBalance = 0,
|
||||||
|
DateTime startDate = default,
|
||||||
|
DateTime endDate = default,
|
||||||
|
Timeframe timeframe = Timeframe.OneHour)
|
||||||
{
|
{
|
||||||
SharpeRatio = sharpeRatio;
|
SharpeRatio = sharpeRatio;
|
||||||
MaxDrawdownPc = maxDrawdownPc;
|
MaxDrawdownPc = maxDrawdownPc;
|
||||||
@@ -32,5 +47,10 @@ public class BacktestScoringParams
|
|||||||
Fees = fees;
|
Fees = fees;
|
||||||
TradeCount = tradeCount;
|
TradeCount = tradeCount;
|
||||||
MaxDrawdownRecoveryTime = maxDrawdownRecoveryTime;
|
MaxDrawdownRecoveryTime = maxDrawdownRecoveryTime;
|
||||||
|
MaxDrawdown = maxDrawdown;
|
||||||
|
InitialBalance = initialBalance;
|
||||||
|
StartDate = startDate;
|
||||||
|
EndDate = endDate;
|
||||||
|
Timeframe = timeframe;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,33 @@
|
|||||||
using Managing.Domain.Backtests;
|
using Managing.Domain.Backtests;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
public class BacktestScorer
|
public class BacktestScorer
|
||||||
{
|
{
|
||||||
// Updated weights without ProfitEfficiency
|
// Updated weights with more balanced distribution
|
||||||
private static readonly Dictionary<string, double> Weights = new Dictionary<string, double>
|
private static readonly Dictionary<string, double> Weights = new Dictionary<string, double>
|
||||||
{
|
{
|
||||||
{ "GrowthPercentage", 0.28 },
|
{ "GrowthPercentage", 0.25 },
|
||||||
{ "SharpeRatio", 0.18 },
|
{ "SharpeRatio", 0.15 },
|
||||||
{ "MaxDrawdownPc", 0.15 },
|
{ "MaxDrawdownUsd", 0.12 },
|
||||||
{ "HodlComparison", 0.05 },
|
{ "HodlComparison", 0.05 },
|
||||||
{ "WinRate", 0.18 },
|
{ "WinRate", 0.15 },
|
||||||
{ "ProfitabilityBonus", 0.11 },
|
{ "ProfitabilityBonus", 0.08 },
|
||||||
{ "TradeCount", 0.03 },
|
{ "TradeCount", 0.05 },
|
||||||
{ "RecoveryTime", 0.02 }
|
{ "RecoveryTime", 0.02 },
|
||||||
|
{ "TestDuration", 0.03 },
|
||||||
|
{ "FeesImpact", 0.02 }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
public static double CalculateTotalScore(BacktestScoringParams p)
|
public static double CalculateTotalScore(BacktestScoringParams p)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Early exit for no positions
|
||||||
|
if (p.TradeCount == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
var baseScore = CalculateBaseScore(p);
|
var baseScore = CalculateBaseScore(p);
|
||||||
var finalScore = ApplyProfitabilityRules(baseScore, p);
|
var finalScore = ApplyProfitabilityRules(baseScore, p);
|
||||||
|
|
||||||
@@ -37,12 +45,14 @@ public class BacktestScorer
|
|||||||
{
|
{
|
||||||
{ "GrowthPercentage", CalculateGrowthScore(p.GrowthPercentage) },
|
{ "GrowthPercentage", CalculateGrowthScore(p.GrowthPercentage) },
|
||||||
{ "SharpeRatio", CalculateSharpeScore(p.SharpeRatio) },
|
{ "SharpeRatio", CalculateSharpeScore(p.SharpeRatio) },
|
||||||
{ "MaxDrawdownPc", CalculateDrawdownScore(p.MaxDrawdownPc) },
|
{ "MaxDrawdownUsd", CalculateDrawdownUsdScore(p.MaxDrawdown, p.InitialBalance) },
|
||||||
{ "HodlComparison", CalculateHodlComparisonScore(p.GrowthPercentage, p.HodlPercentage) },
|
{ "HodlComparison", CalculateHodlComparisonScore(p.GrowthPercentage, p.HodlPercentage) },
|
||||||
{ "WinRate", CalculateWinRateScore(p.WinRate, p.TradeCount) },
|
{ "WinRate", CalculateWinRateScore(p.WinRate, p.TradeCount) },
|
||||||
{ "ProfitabilityBonus", CalculateProfitabilityBonus(p.GrowthPercentage) },
|
{ "ProfitabilityBonus", CalculateProfitabilityBonus(p.GrowthPercentage) },
|
||||||
{ "TradeCount", CalculateTradeCountScore(p.TradeCount) },
|
{ "TradeCount", CalculateTradeCountScore(p.TradeCount) },
|
||||||
{ "RecoveryTime", CalculateRecoveryScore(p.MaxDrawdownRecoveryTime) }
|
{ "RecoveryTime", CalculateRecoveryScore(p.MaxDrawdownRecoveryTime, p.Timeframe) },
|
||||||
|
{ "TestDuration", CalculateTestDurationScore(p.StartDate, p.EndDate, p.Timeframe) },
|
||||||
|
{ "FeesImpact", CalculateFeesImpactScore(p.FeesPaid, p.InitialBalance, (decimal)p.TotalPnL) }
|
||||||
};
|
};
|
||||||
|
|
||||||
return componentScores.Sum(kvp => kvp.Value * Weights[kvp.Key]);
|
return componentScores.Sum(kvp => kvp.Value * Weights[kvp.Key]);
|
||||||
@@ -50,58 +60,78 @@ public class BacktestScorer
|
|||||||
|
|
||||||
private static double ApplyProfitabilityRules(double baseScore, BacktestScoringParams p)
|
private static double ApplyProfitabilityRules(double baseScore, BacktestScoringParams p)
|
||||||
{
|
{
|
||||||
// 1. Negative PnL Penalty (Core Rule)
|
var penaltyMultiplier = 1.0;
|
||||||
|
|
||||||
|
// 1. Negative PnL Penalty (Dynamic)
|
||||||
if (p.GrowthPercentage < 0)
|
if (p.GrowthPercentage < 0)
|
||||||
{
|
{
|
||||||
baseScore = Math.Min(baseScore, 70) * GetNegativePnLMultiplier(p.GrowthPercentage);
|
var negativePenalty = Math.Abs(p.GrowthPercentage) * 0.1; // 10% penalty per 1% loss
|
||||||
|
penaltyMultiplier *= Math.Max(0.1, 1 - negativePenalty);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Absolute PnL Validation (Additional Recommendation)
|
// 2. Absolute PnL Validation (Dynamic)
|
||||||
if (p.TotalPnL <= 0)
|
if (p.TotalPnL <= 0)
|
||||||
{
|
{
|
||||||
baseScore = Math.Min(baseScore, 50);
|
penaltyMultiplier *= 0.3; // 70% penalty for negative absolute PnL
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Win Rate Validation (Additional Recommendation)
|
// 3. Win Rate Validation (Dynamic)
|
||||||
if (p.WinRate < 0.3 && p.TradeCount > 10)
|
if (p.WinRate < 0.3 && p.TradeCount > 10)
|
||||||
{
|
{
|
||||||
baseScore = Math.Min(baseScore, 60);
|
var winRatePenalty = (0.3 - p.WinRate) * 0.5; // 50% penalty per 10% below 30%
|
||||||
|
penaltyMultiplier *= Math.Max(0.2, 1 - winRatePenalty);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Minimum Profit Threshold (Additional Recommendation)
|
// 4. Minimum Profit Threshold (Dynamic)
|
||||||
if (p.GrowthPercentage < 2 && p.TradeCount > 5)
|
if (p.GrowthPercentage < 2 && p.TradeCount > 5)
|
||||||
{
|
{
|
||||||
baseScore = Math.Min(baseScore, 80);
|
var profitPenalty = (2 - p.GrowthPercentage) * 0.1; // 10% penalty per 1% below 2%
|
||||||
|
penaltyMultiplier *= Math.Max(0.5, 1 - profitPenalty);
|
||||||
}
|
}
|
||||||
|
|
||||||
return baseScore;
|
// 5. Drawdown Penalty (Dynamic)
|
||||||
|
if (p.MaxDrawdownPc > 20)
|
||||||
|
{
|
||||||
|
var drawdownPenalty = (p.MaxDrawdownPc - 20) * 0.02; // 2% penalty per 1% above 20%
|
||||||
|
penaltyMultiplier *= Math.Max(0.3, 1 - drawdownPenalty);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Test Duration Penalty (Dynamic)
|
||||||
|
var testDurationDays = (p.EndDate - p.StartDate).TotalDays;
|
||||||
|
if (testDurationDays < 30)
|
||||||
|
{
|
||||||
|
var durationPenalty = (30 - testDurationDays) * 0.02; // 2% penalty per day below 30
|
||||||
|
penaltyMultiplier *= Math.Max(0.5, 1 - durationPenalty);
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseScore * penaltyMultiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double CalculateGrowthScore(double growthPercentage)
|
private static double CalculateGrowthScore(double growthPercentage)
|
||||||
{
|
{
|
||||||
// More aggressive penalty for negative growth
|
// More aggressive scoring - harder to reach 100
|
||||||
if (growthPercentage < 0)
|
if (growthPercentage < 0)
|
||||||
{
|
{
|
||||||
return Math.Max(0, 40 + (growthPercentage * 2)); // -10% → 20, -20% → 0
|
return Math.Max(0, 20 + (growthPercentage * 1.5)); // -10% → 5, -20% → 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Require minimum 5% growth for full score
|
// Require minimum 10% growth for full score (increased from 5%)
|
||||||
return growthPercentage switch
|
return growthPercentage switch
|
||||||
{
|
{
|
||||||
< 5 => growthPercentage * 15, // 2% → 30, 4% → 60
|
< 5 => growthPercentage * 8, // 2% → 16, 4% → 32
|
||||||
|
< 10 => 40 + (growthPercentage - 5) * 12, // 5% → 40, 7% → 64, 9% → 88
|
||||||
_ => 100
|
_ => 100
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Existing multiplier calculation
|
|
||||||
private static double GetNegativePnLMultiplier(double growthPercentage)
|
private static double GetNegativePnLMultiplier(double growthPercentage)
|
||||||
{
|
{
|
||||||
return growthPercentage switch
|
return growthPercentage switch
|
||||||
{
|
{
|
||||||
> -5 => 0.8,
|
> -5 => 0.6,
|
||||||
> -10 => 0.6,
|
> -10 => 0.4,
|
||||||
> -20 => 0.4,
|
> -20 => 0.2,
|
||||||
_ => 0.2
|
_ => 0.1
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,42 +139,49 @@ public class BacktestScorer
|
|||||||
{
|
{
|
||||||
return growthPercentage switch
|
return growthPercentage switch
|
||||||
{
|
{
|
||||||
> 0 => 100 * (1 - 1 / (1 + growthPercentage / 50)), // Diminishing returns
|
> 0 => 50 * (1 - 1 / (1 + growthPercentage / 30)), // Reduced max bonus to 50
|
||||||
_ => 0
|
_ => 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsInactiveStrategy(BacktestScoringParams p)
|
|
||||||
{
|
|
||||||
// Detect strategies with no economic value
|
|
||||||
return (p.GrowthPercentage <= p.HodlPercentage &&
|
|
||||||
p.TotalPnL <= 0) ||
|
|
||||||
p.TradeCount < 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double CalculateSharpeScore(double sharpeRatio)
|
private static double CalculateSharpeScore(double sharpeRatio)
|
||||||
{
|
{
|
||||||
return sharpeRatio switch
|
return sharpeRatio switch
|
||||||
{
|
{
|
||||||
< 0 => 0,
|
< 0 => 0,
|
||||||
> 3 => 100,
|
> 4 => 100, // Increased threshold from 3 to 4
|
||||||
_ => (sharpeRatio / 3) * 100
|
_ => (sharpeRatio / 4) * 100
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double CalculateDrawdownScore(double maxDrawdownPc)
|
|
||||||
|
|
||||||
|
private static double CalculateDrawdownUsdScore(decimal maxDrawdown, decimal initialBalance)
|
||||||
{
|
{
|
||||||
return maxDrawdownPc switch
|
if (initialBalance <= 0) return 0;
|
||||||
|
|
||||||
|
var drawdownPercentage = (double)(maxDrawdown / initialBalance * 100);
|
||||||
|
return drawdownPercentage switch
|
||||||
{
|
{
|
||||||
> 90 => 0,
|
> 30 => 0, // 30% drawdown in USD = 0 score
|
||||||
_ => 100 - Math.Pow(maxDrawdownPc / 90 * 100, 2) / 100
|
_ => 100 - Math.Pow(drawdownPercentage / 30 * 100, 1.5) / 100
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double CalculateWinRateScore(double winRate, int tradeCount)
|
private static double CalculateWinRateScore(double winRate, int tradeCount)
|
||||||
{
|
{
|
||||||
|
// Base win rate score
|
||||||
var baseScore = winRate * 100;
|
var baseScore = winRate * 100;
|
||||||
var significanceFactor = Math.Min(1, tradeCount / 100.0);
|
|
||||||
|
// Significance factor - more aggressive
|
||||||
|
var significanceFactor = Math.Min(1, (tradeCount - 5) / 50.0); // Start at 5 trades, full significance at 55 trades
|
||||||
|
|
||||||
|
// Additional penalty for very few trades
|
||||||
|
if (tradeCount < 10)
|
||||||
|
{
|
||||||
|
significanceFactor *= 0.5; // 50% penalty for less than 10 trades
|
||||||
|
}
|
||||||
|
|
||||||
return baseScore * significanceFactor;
|
return baseScore * significanceFactor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,31 +190,87 @@ public class BacktestScorer
|
|||||||
var difference = strategyGrowth - hodlGrowth;
|
var difference = strategyGrowth - hodlGrowth;
|
||||||
return difference switch
|
return difference switch
|
||||||
{
|
{
|
||||||
> 0 => 100 - (100 / (1 + difference / 5)),
|
> 0 => 80 - (80 / (1 + difference / 3)), // Reduced max to 80
|
||||||
_ => Math.Max(0, 30 + difference * 3)
|
_ => Math.Max(0, 20 + difference * 2) // Reduced base score
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double CalculateTradeCountScore(int tradeCount)
|
private static double CalculateTradeCountScore(int tradeCount)
|
||||||
{
|
{
|
||||||
return Math.Min(100, Math.Max(0, (tradeCount - 10) * 0.5));
|
return tradeCount switch
|
||||||
}
|
|
||||||
|
|
||||||
private static double CalculateRecoveryScore(TimeSpan recoveryTime)
|
|
||||||
{
|
|
||||||
var days = recoveryTime.TotalDays;
|
|
||||||
return days switch
|
|
||||||
{
|
{
|
||||||
< 0 => 100,
|
< 5 => 0,
|
||||||
> 365 => 0,
|
< 10 => (tradeCount - 5) * 10, // 5-10 trades: 0-50 points
|
||||||
_ => 100 - (days / 365 * 100)
|
< 50 => 50 + (tradeCount - 10) * 1.25, // 10-50 trades: 50-100 points
|
||||||
|
_ => 100
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double CalculateRiskAdjustedGrowthScore(double growth, double drawdown)
|
private static double CalculateRecoveryScore(TimeSpan recoveryTime, Timeframe timeframe)
|
||||||
{
|
{
|
||||||
if (drawdown == 0) return 100;
|
var days = recoveryTime.TotalDays;
|
||||||
var ratio = growth / drawdown;
|
|
||||||
return Math.Min(ratio * 10, 100);
|
// Adjust recovery expectations based on timeframe
|
||||||
|
var maxRecoveryDays = timeframe switch
|
||||||
|
{
|
||||||
|
Timeframe.FiveMinutes => 3.0, // 1 week for 5m
|
||||||
|
Timeframe.FifteenMinutes => 5.0, // 2 weeks for 15m
|
||||||
|
Timeframe.ThirtyMinutes => 10.0, // 3 weeks for 30m
|
||||||
|
Timeframe.OneHour => 15.0, // 1 month for 1h
|
||||||
|
Timeframe.FourHour => 30.0, // 2 months for 4h
|
||||||
|
Timeframe.OneDay => 90.0, // 6 months for 1d
|
||||||
|
_ => 30.0 // Default to 1 month
|
||||||
|
};
|
||||||
|
|
||||||
|
if (days < 0) return 100;
|
||||||
|
if (days > maxRecoveryDays) return 0;
|
||||||
|
return 100 - (days / maxRecoveryDays * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double CalculateTestDurationScore(DateTime startDate, DateTime endDate, Timeframe timeframe)
|
||||||
|
{
|
||||||
|
var durationDays = (endDate - startDate).TotalDays;
|
||||||
|
|
||||||
|
// Adjust minimum test duration based on timeframe
|
||||||
|
var minTestDays = timeframe switch
|
||||||
|
{
|
||||||
|
Timeframe.FiveMinutes => 14.0, // 3 days for 5m
|
||||||
|
Timeframe.FifteenMinutes => 28.0, // 1 week for 15m
|
||||||
|
Timeframe.ThirtyMinutes => 56.0, // 2 weeks for 30m
|
||||||
|
Timeframe.OneHour => 84.0, // 3 weeks for 1h
|
||||||
|
Timeframe.FourHour => 120.0, // 1 month for 4h
|
||||||
|
Timeframe.OneDay => 90.0, // 3 months for 1d
|
||||||
|
_ => 21.0 // Default to 3 weeks
|
||||||
|
};
|
||||||
|
|
||||||
|
var optimalTestDays = minTestDays * 3; // Optimal is 3x minimum
|
||||||
|
|
||||||
|
if (durationDays < minTestDays) return 0;
|
||||||
|
if (durationDays < optimalTestDays) return (durationDays / optimalTestDays) * 100;
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double CalculateFeesImpactScore(decimal feesPaid, decimal initialBalance, decimal totalPnL)
|
||||||
|
{
|
||||||
|
if (initialBalance <= 0) return 0;
|
||||||
|
|
||||||
|
var feesPercentage = (double)(feesPaid / initialBalance * 100);
|
||||||
|
var pnlPercentage = (double)(totalPnL / initialBalance * 100);
|
||||||
|
|
||||||
|
// If fees are higher than PnL, heavy penalty
|
||||||
|
if (feesPaid > totalPnL && totalPnL > 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fee efficiency score
|
||||||
|
var feeEfficiency = feesPercentage switch
|
||||||
|
{
|
||||||
|
> 5 => 0, // More than 5% fees = 0
|
||||||
|
> 2 => 50 - (feesPercentage - 2) * 16.67, // 2-5%: 50-0 points
|
||||||
|
_ => 100 - feesPercentage * 25 // 0-2%: 100-50 points
|
||||||
|
};
|
||||||
|
|
||||||
|
return feeEfficiency;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user