52 lines
2.8 KiB
C#
52 lines
2.8 KiB
C#
using System;
|
|
using FinlyticCore.Dtos.Simulation;
|
|
|
|
namespace FinlyticSimulation.Services;
|
|
|
|
/// <summary>
|
|
/// Pure scoring function for FinlyticSimulation's backtest-reliability matrix, extracted out of
|
|
/// <c>QuantSimulationEngine.RunBacktestAsync</c> (which previously mixed candle-fetch-with-fallback, replay
|
|
/// orchestration, DB persistence, AND this scoring math into one large method with inline magic numbers). No
|
|
/// I/O, no DB access - just <see cref="BacktestReportDto"/> + threshold settings in, a verdict out, so this is
|
|
/// independently unit-testable without spinning up a DbContext or a real backtest.
|
|
/// </summary>
|
|
public static class ReliabilityMatrixCalculator
|
|
{
|
|
/// <param name="ReliabilityScore">0-100, a blend of profit factor (max 1.5x weight, capped) and win rate.</param>
|
|
/// <param name="IsApproved">
|
|
/// Whether <see cref="Scoring.ICompositeOpportunityScorer"/>-style consumers should trust this
|
|
/// strategy/asset combination. Defaults to approved when there isn't yet enough sample data to judge it
|
|
/// (Rules.md §4: "not enough data" must never read the same as "actively vetoed").
|
|
/// </param>
|
|
/// <param name="RecommendedAction">"BOOST_SCORE" / "NEUTRAL" / "VETO_DISABLE" - see <see cref="Calculate"/>.</param>
|
|
public record Result(decimal ReliabilityScore, bool IsApproved, string RecommendedAction);
|
|
|
|
/// <summary>
|
|
/// Scores a single completed backtest report against the given approval thresholds
|
|
/// (<c>SimulationSettingKeys.MinSampleTradesForApproval</c>/<c>HighProfitFactorThreshold</c>/<c>LowProfitFactorThreshold</c>).
|
|
/// </summary>
|
|
public static Result Calculate(
|
|
BacktestReportDto report,
|
|
decimal minSampleTrades,
|
|
decimal highProfitFactorThreshold,
|
|
decimal lowProfitFactorThreshold)
|
|
{
|
|
// 0..100 blend: profit factor contributes up to 75 points (capped at PF=3.0 -> 1.5 * 50), win rate
|
|
// contributes up to 50 points (100% WR * 0.5) - deliberately not a simple average, since a high win
|
|
// rate with a poor profit factor (many tiny wins, rare huge losses) should not score as "reliable".
|
|
decimal rawScore = (Math.Clamp(report.ProfitFactor / 2.0m, 0m, 1.5m) * 50m) + (report.WinRatePercent * 0.5m);
|
|
decimal reliabilityScore = Math.Clamp(Math.Round(rawScore, 2), 0m, 100m);
|
|
|
|
bool isApproved = report.ProfitFactor >= lowProfitFactorThreshold || report.TotalTrades < minSampleTrades;
|
|
string recommendedAction = "NEUTRAL";
|
|
|
|
if (report.TotalTrades >= minSampleTrades)
|
|
{
|
|
if (report.ProfitFactor >= highProfitFactorThreshold) recommendedAction = "BOOST_SCORE";
|
|
else if (report.ProfitFactor < lowProfitFactorThreshold) recommendedAction = "VETO_DISABLE";
|
|
}
|
|
|
|
return new Result(reliabilityScore, isApproved, recommendedAction);
|
|
}
|
|
}
|