d159075182
Phase 1 v0 usava `max(0, dsr - 0.5*max_dd)` che azzerava brutalmente la fitness quando max_dd > 2*dsr. Real run v4 aveva 55/55 strategie a fitness=0 (DSR ~0.001, max_dd > 0.5), zero pressione selettiva sul GA. v1: base = 0.5*dsr + 0.5*0.5*(tanh(sharpe)+1) in [0,1], modulata da penalty moltiplicativa 1/(1+k*max_dd) in (0,1]. Hard kill (no-trade, HIGH adversarial) preservati. Fitness sempre >0 per strategie con almeno 1 trade -> il GA puo' preferire "meno cattivo" a "catastrofico" anche su sharpe negativo. Tests: +3 nuovi (continuous mediocre, bounded, monotonic drawdown), 4 esistenti restano verdi. Suite 138 -> 141 passed. ruff + mypy strict puliti. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
92 lines
2.9 KiB
Python
92 lines
2.9 KiB
Python
from itertools import pairwise
|
|
|
|
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,
|
|
sharpe: float = 1.5,
|
|
) -> FalsificationReport:
|
|
return FalsificationReport(
|
|
sharpe=sharpe,
|
|
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
|
|
|
|
|
|
def test_fitness_continuous_signal_for_mediocre() -> None:
|
|
"""Strategie mediocri (DSR ~0, Sharpe negativo) hanno comunque fitness>0
|
|
e la meno cattiva e' preferita."""
|
|
a = AdversarialReport()
|
|
less_bad = make_falsification(dsr=0.001, sharpe=-0.5, max_dd=0.3)
|
|
worse = make_falsification(dsr=0.001, sharpe=-2.0, max_dd=0.3)
|
|
f_less = compute_fitness(less_bad, a)
|
|
f_worse = compute_fitness(worse, a)
|
|
assert f_less > 0.0
|
|
assert f_worse > 0.0
|
|
assert f_less > f_worse
|
|
|
|
|
|
def test_fitness_bounded() -> None:
|
|
"""Fitness e' bounded in [0, 2.0] per input tipici."""
|
|
a = AdversarialReport()
|
|
cases = [
|
|
make_falsification(dsr=0.0, sharpe=-5.0, max_dd=0.0),
|
|
make_falsification(dsr=0.0, sharpe=0.0, max_dd=0.0),
|
|
make_falsification(dsr=0.5, sharpe=1.0, max_dd=0.2),
|
|
make_falsification(dsr=0.9, sharpe=2.0, max_dd=0.15),
|
|
make_falsification(dsr=1.0, sharpe=5.0, max_dd=0.0),
|
|
make_falsification(dsr=1.0, sharpe=10.0, max_dd=5.0),
|
|
]
|
|
for f in cases:
|
|
v = compute_fitness(f, a)
|
|
assert 0.0 <= v <= 2.0, f"fitness {v} fuori range per {f}"
|
|
|
|
|
|
def test_fitness_normalizes_drawdown() -> None:
|
|
"""Con DSR e Sharpe fissi, fitness e' monotona decrescente in max_dd."""
|
|
a = AdversarialReport()
|
|
dds = [0.0, 0.1, 0.5, 1.0, 2.0, 5.0]
|
|
fitnesses = [
|
|
compute_fitness(make_falsification(dsr=0.5, sharpe=1.0, max_dd=dd), a)
|
|
for dd in dds
|
|
]
|
|
for prev, curr in pairwise(fitnesses):
|
|
assert prev > curr, f"non monotona: {fitnesses}"
|