605faf5310
Add cluster audit pipeline, united EA updates, brochure generators, and publication hygiene (gitignore, MT5 path desensitization, pre-upload scan). Remove tracked reports, models, and binary artifacts from the repo. Co-authored-by: Cursor <cursoragent@cursor.com>
127 lines
4.2 KiB
Python
127 lines
4.2 KiB
Python
"""Strategy scoring — requires ~1 trade per calendar day over the backtest period."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, datetime
|
|
|
|
from .backtest_core import BacktestReport
|
|
|
|
DEFAULT_TRADES_PER_DAY = 1.0
|
|
|
|
|
|
def period_days(start: date | datetime | str, end: date | datetime | str) -> int:
|
|
if isinstance(start, str):
|
|
start = datetime.fromisoformat(start)
|
|
if isinstance(end, str):
|
|
end = datetime.fromisoformat(end)
|
|
if isinstance(start, datetime):
|
|
start = start.date()
|
|
if isinstance(end, datetime):
|
|
end = end.date()
|
|
return max(1, (end - start).days)
|
|
|
|
|
|
def min_trades_for_period(days: int, trades_per_day: float = DEFAULT_TRADES_PER_DAY) -> int:
|
|
return max(30, int(days * trades_per_day))
|
|
|
|
|
|
def trades_per_day(report: BacktestReport, days: int) -> float:
|
|
if report.total_trades == 0 or days <= 0:
|
|
return 0.0
|
|
return report.total_trades / days
|
|
|
|
|
|
def winning_months_pct(report: BacktestReport) -> float:
|
|
if not report.monthly_returns:
|
|
return 0.0
|
|
vals = list(report.monthly_returns.values())
|
|
return 100.0 * sum(1 for v in vals if v > 0) / len(vals)
|
|
|
|
|
|
def score_report(
|
|
report: BacktestReport,
|
|
period_days_count: int,
|
|
trades_per_day_target: float = DEFAULT_TRADES_PER_DAY,
|
|
) -> float:
|
|
"""Higher is better. Hard-fails below activity + profit gates."""
|
|
min_t = min_trades_for_period(period_days_count, trades_per_day_target)
|
|
t = report.total_trades
|
|
tpd = trades_per_day(report, period_days_count)
|
|
|
|
if t < min_t or tpd < trades_per_day_target:
|
|
return float("-inf")
|
|
|
|
if report.net_profit <= 0 or report.profit_factor < 1.05:
|
|
return float("-inf")
|
|
|
|
win_mo = winning_months_pct(report) / 100.0
|
|
activity = min(tpd / (trades_per_day_target * 1.5), 1.0)
|
|
pf = min(report.profit_factor, 4.0) / 4.0
|
|
wr = min(report.win_rate, 70.0) / 70.0
|
|
|
|
return (
|
|
report.sharpe * 0.25
|
|
+ (report.net_profit / 2000.0) * 0.18
|
|
- report.max_drawdown_pct * 0.10
|
|
+ activity * 0.22
|
|
+ win_mo * 0.12
|
|
+ pf * 0.08
|
|
+ wr * 0.05
|
|
)
|
|
|
|
|
|
def score_label(
|
|
score: float,
|
|
trades: int,
|
|
period_days_count: int,
|
|
trades_per_day_target: float = DEFAULT_TRADES_PER_DAY,
|
|
) -> str:
|
|
min_t = min_trades_for_period(period_days_count, trades_per_day_target)
|
|
tpd = trades / period_days_count if period_days_count else 0
|
|
if trades < min_t:
|
|
return f"N/A ({trades}<{min_t}, need {trades_per_day_target:.1f}/day)"
|
|
if tpd < trades_per_day_target:
|
|
return f"N/A ({tpd:.2f}/day < {trades_per_day_target:.1f}/day)"
|
|
if score == float("-inf"):
|
|
return "N/A (fails profit gates)"
|
|
return f"{score:.2f}"
|
|
|
|
|
|
def acceptance(
|
|
report: BacktestReport,
|
|
period_days_count: int,
|
|
trades_per_day_target: float = DEFAULT_TRADES_PER_DAY,
|
|
) -> tuple[bool, list[str]]:
|
|
min_t = min_trades_for_period(period_days_count, trades_per_day_target)
|
|
tpd = trades_per_day(report, period_days_count)
|
|
issues: list[str] = []
|
|
|
|
if report.total_trades < min_t:
|
|
issues.append(f"trades={report.total_trades} need >={min_t} ({trades_per_day_target:.1f}/day x {period_days_count}d)")
|
|
if tpd < trades_per_day_target:
|
|
issues.append(f"trades/day={tpd:.2f} need >={trades_per_day_target:.1f}")
|
|
if report.net_profit <= 0:
|
|
issues.append(f"net=${report.net_profit:.0f} not positive")
|
|
if report.profit_factor < 1.15:
|
|
issues.append(f"pf={report.profit_factor:.2f} need >=1.15")
|
|
if report.sharpe < 0.3:
|
|
issues.append(f"sharpe={report.sharpe:.2f} need >=0.30")
|
|
if report.max_drawdown_pct > 25:
|
|
issues.append(f"dd={report.max_drawdown_pct:.1f}% too high")
|
|
|
|
win_mo = winning_months_pct(report)
|
|
if win_mo < 45:
|
|
issues.append(f"winning_months={win_mo:.0f}% need >=45%")
|
|
|
|
return len(issues) == 0, issues
|
|
|
|
|
|
def format_quality_line(report: BacktestReport, period_days_count: int) -> str:
|
|
tpd = trades_per_day(report, period_days_count)
|
|
return (
|
|
f"net=${report.net_profit:.0f} sharpe={report.sharpe:.2f} "
|
|
f"trades={report.total_trades} ({tpd:.2f}/day) pf={report.profit_factor:.2f} "
|
|
f"wr={report.win_rate:.0f}% win_mo={winning_months_pct(report):.0f}% "
|
|
f"dd={report.max_drawdown_pct:.1f}%"
|
|
)
|