from __future__ import annotations import itertools from collections.abc import Sequence from dataclasses import dataclass from typing import Any from app.backtest.backtester import Backtester from app.engine.strategies import get_strategy from app.engine.strategies.base import Strategy from app.schemas import Candle @dataclass(slots=True) class SweepRow: params: dict[str, Any] metrics: dict[str, Any] def expand_grid(grid: dict[str, Sequence[Any]]) -> list[dict[str, Any]]: """Cartesian product of a param grid into a list of concrete param dicts.""" if not grid: return [{}] names = list(grid) return [dict(zip(names, values, strict=True)) for values in itertools.product(*grid.values())] def _score(metrics: dict[str, Any]) -> float: """Rank by return, penalising drawdown. Simple, transparent, sortable.""" return float(metrics.get("total_return_pct", 0.0)) - float(metrics.get("max_drawdown_pct", 0.0)) def run_sweep( strategy_key: str, base_params: dict[str, Any], grid: dict[str, Sequence[Any]], candles: Sequence[Candle], symbol: str, timeframe: str, *, fee_bps: float = 10.0, slippage_bps: float = 5.0, starting_balance: float = 10_000.0, top_n: int = 20, ) -> list[SweepRow]: """Backtest every combination in ``grid`` (merged over ``base_params``).""" strategy_cls: type[Strategy] = get_strategy(strategy_key) backtester = Backtester( fee_bps=fee_bps, slippage_bps=slippage_bps, starting_balance=starting_balance ) rows: list[SweepRow] = [] for params in expand_grid(grid): merged = {**base_params, **params} strategy = strategy_cls(merged) result = backtester.run(candles, strategy, symbol, timeframe) rows.append(SweepRow(params=merged, metrics=result.metrics.model_dump())) rows.sort(key=lambda row: _score(row.metrics), reverse=True) return rows[:top_n]