Files
Finlytic/FinlyticSimulation/Services/ReliabilityMatrixCalculator.cs
T

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);
}
}