diff --git a/src/multi_swarm/ga/__init__.py b/src/multi_swarm/ga/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/multi_swarm/ga/fitness.py b/src/multi_swarm/ga/fitness.py new file mode 100644 index 0000000..234f085 --- /dev/null +++ b/src/multi_swarm/ga/fitness.py @@ -0,0 +1,44 @@ +"""Fitness function v0 della Phase 1. + +Combina :class:`FalsificationReport` (metriche di robustezza) e +:class:`AdversarialReport` (findings euristici) in uno scalare ``>= 0`` che il +GA usa per selezione e ranking. + +Logica deliberatamente coarse: DSR penalizzato dal max drawdown, con due +kill-switch hard (no-trade, finding HIGH adversarial) che azzerano la fitness. +La penalita' lineare sul drawdown e' un compromesso volutamente semplice; +versioni successive potranno usare Calmar o utility convessa. +""" + +from __future__ import annotations + +from ..agents.adversarial import AdversarialReport, Severity +from ..agents.falsification import FalsificationReport + + +def compute_fitness( + falsification: FalsificationReport, + adversarial: AdversarialReport, + drawdown_penalty: float = 0.5, +) -> float: + """Calcola la fitness scalare di una strategia. + + Args: + falsification: report con DSR, max_drawdown, n_trades. + adversarial: report con eventuali findings euristici. + drawdown_penalty: peso lineare sul max drawdown (default 0.5). + + Returns: + Fitness ``>= 0``. Zero indica strategia da scartare. + + Logica: + 1. ``n_trades == 0`` → 0 (nessuna evidenza, sega subito). + 2. Almeno un finding ``HIGH`` adversarial → 0 (kill). + 3. Altrimenti: ``dsr - drawdown_penalty * max_drawdown``, clamped a 0. + """ + if falsification.n_trades == 0: + return 0.0 + if any(f.severity == Severity.HIGH for f in adversarial.findings): + return 0.0 + raw = falsification.dsr - drawdown_penalty * falsification.max_drawdown + return max(0.0, float(raw)) diff --git a/tests/unit/test_fitness.py b/tests/unit/test_fitness.py new file mode 100644 index 0000000..5d9d455 --- /dev/null +++ b/tests/unit/test_fitness.py @@ -0,0 +1,45 @@ +from multi_swarm.agents.adversarial import AdversarialReport, Finding, Severity +from multi_swarm.agents.falsification import FalsificationReport +from multi_swarm.ga.fitness import compute_fitness + + +def make_falsification( + dsr: float = 0.7, max_dd: float = 0.2, n_trades: int = 30 +) -> FalsificationReport: + return FalsificationReport( + sharpe=1.5, + dsr=dsr, + dsr_pvalue=0.05, + max_drawdown=max_dd, + total_return=0.3, + n_trades=n_trades, + n_bars=500, + ) + + +def test_fitness_zero_trades_is_zero() -> None: + f = make_falsification(n_trades=0) + a = AdversarialReport() + assert compute_fitness(f, a) == 0.0 + + +def test_fitness_increases_with_dsr() -> None: + a = AdversarialReport() + f1 = make_falsification(dsr=0.5) + f2 = make_falsification(dsr=0.9) + assert compute_fitness(f2, a) > compute_fitness(f1, a) + + +def test_fitness_decreases_with_drawdown() -> None: + a = AdversarialReport() + f1 = make_falsification(max_dd=0.1) + f2 = make_falsification(max_dd=0.4) + assert compute_fitness(f1, a) > compute_fitness(f2, a) + + +def test_fitness_zeroed_by_high_severity_finding() -> None: + f = make_falsification() + a = AdversarialReport( + findings=[Finding(name="degenerate", severity=Severity.HIGH, detail="x")] + ) + assert compute_fitness(f, a) == 0.0