Files
Multi_Swarm_Coevolutive/tests/unit/test_fitness.py
T
Adriano cf42dd85f3 feat(fitness): v2 soft-kill opt-in (HIGH non hard non azzerano, applicano penalty)
Aggiunto parametro hard_kill_findings opzionale a compute_fitness.
None (default) = v1 backward-compat: ogni HIGH azzera la fitness.
tuple non vuota = v2: solo finding con name nel set azzerano; gli altri
HIGH applicano penalità moltiplicativa
adv_penalty = 1 / (1 + soft_penalty * n_soft_high)
(default soft_penalty=0.4 → 1 HIGH soft = 0.71x, 2 = 0.56x, 3 = 0.45x).

Motivazione: tutti i run Phase 2/2.5 mostrano 55-87 finding HIGH dominanti
da fees_eat_alpha + flat_too_long. La fitness v1 azzera ogni genome con
anche solo 1 HIGH → median sempre 0 perché quasi tutti i genomi sopravvivono
in modo binario (top integro vs zerati). v2 fornisce gradient continuo:
strategie 'quasi-buone' restano valutabili e il GA può evolverle.

Hard kill v2 default: {"no_trades", "degenerate"} (la strategia letteralmente
non funziona — niente da salvare). Tutti gli altri HIGH (fees_eat_alpha,
flat_too_long, time_in_market_too_high, undertrading, overtrading) → soft.

RunConfig: fitness_hard_kill_findings (None = v1, tuple = v2) +
fitness_adversarial_soft_penalty (default 0.4). CLI flag --fitness-v2 imposta
hard_kill_findings = ("no_trades", "degenerate") e
--fitness-soft-penalty per il fattore custom.

+5 test (12 totali in test_fitness, 191 totale suite):
- v2 soft HIGH non azzera
- v2 hard kill ancora azzera
- v2 cumulativo: più soft HIGH = penalty più severa
- v2 senza findings = identico a v1
- backward compat hard_kill_findings=None = v1

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 13:52:22 +02:00

155 lines
5.4 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}"
# --- Fitness v2 (soft-kill opt-in) ---
def test_fitness_v2_soft_high_not_zero() -> None:
"""v2: un finding HIGH soft NON azzera, applica solo soft penalty."""
f = make_falsification(dsr=0.5, sharpe=1.0, max_dd=0.2)
a = AdversarialReport(
findings=[Finding(name="fees_eat_alpha", severity=Severity.HIGH, detail="x")]
)
v2 = compute_fitness(f, a, hard_kill_findings=("no_trades", "degenerate"))
v1 = compute_fitness(f, a)
assert v1 == 0.0
assert v2 > 0.0
def test_fitness_v2_hard_kill_still_zero() -> None:
"""v2: finding HIGH in hard_kill_findings azzera comunque."""
f = make_falsification()
a = AdversarialReport(
findings=[Finding(name="degenerate", severity=Severity.HIGH, detail="x")]
)
v2 = compute_fitness(f, a, hard_kill_findings=("no_trades", "degenerate"))
assert v2 == 0.0
def test_fitness_v2_multiple_soft_high_penalty_increases() -> None:
"""v2: più HIGH soft → penalty cumulativa più severa."""
f = make_falsification(dsr=0.5, sharpe=1.0, max_dd=0.2)
soft = ("no_trades", "degenerate")
one_soft = AdversarialReport(
findings=[Finding(name="fees_eat_alpha", severity=Severity.HIGH, detail="x")]
)
three_soft = AdversarialReport(
findings=[
Finding(name="fees_eat_alpha", severity=Severity.HIGH, detail="x"),
Finding(name="flat_too_long", severity=Severity.HIGH, detail="x"),
Finding(name="time_in_market_too_high", severity=Severity.HIGH, detail="x"),
]
)
v_one = compute_fitness(f, one_soft, hard_kill_findings=soft)
v_three = compute_fitness(f, three_soft, hard_kill_findings=soft)
assert v_one > v_three > 0.0
def test_fitness_v2_no_findings_equals_v1() -> None:
"""v2 senza findings produce esattamente lo stesso valore di v1 (adv_penalty=1.0)."""
f = make_falsification(dsr=0.7, sharpe=1.5, max_dd=0.2)
a = AdversarialReport()
v1 = compute_fitness(f, a)
v2 = compute_fitness(f, a, hard_kill_findings=("no_trades", "degenerate"))
assert v1 == v2
def test_fitness_v2_default_v1_backward_compat() -> None:
"""Senza hard_kill_findings (None) comportamento identico a v1: tutti HIGH azzerano."""
f = make_falsification()
a = AdversarialReport(
findings=[Finding(name="fees_eat_alpha", severity=Severity.HIGH, detail="x")]
)
assert compute_fitness(f, a) == 0.0 # v1 default
assert compute_fitness(f, a, hard_kill_findings=None) == 0.0 # esplicito None = v1