1a1dfb7a73
Aggiunti due meccanismi per selection multi-objective: 1) Helper compute_combined_fitness(fit_train, fit_oos, alpha): formula = alpha*IS + (1-alpha)*OOS, fallback a IS se OOS è None/NaN. 2) RunConfig.eval_oos_during_loop (default False) + fitness_combined_alpha (default 0.5). Quando True E wfa_train_split attivo, ogni genome con fitness IS > 0 viene rivalutato su test_ohlcv DURANTE il loop GA e la fitness usata per tournament_select/elite_select è quella combinata. 2x costo backtest engine (richiede 2 evaluation per genome). 3) CLI flags --eval-oos-during-loop e --fitness-combined-alpha. Motivazione: il run phase2-7-max7y-v2-wfa-001 ha mostrato che il top by fitness_IS (4e1be9fa, ratio OOS 0.31) NON è il top per performance OOS reale (634111992702, ratio 1.42, ret_OOS +105% / 2.2y). Selezionare durante GA con combined fitness orienta l'evoluzione verso strategie OOS-robust by design invece di filtrarle a posteriori. Backward compat: default eval_oos_during_loop=False → comportamento invariato per run senza il flag. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
109 lines
4.2 KiB
Python
109 lines
4.2 KiB
Python
"""Fitness function della Phase 1/2.
|
|
|
|
Combina :class:`FalsificationReport` (metriche di robustezza) e
|
|
:class:`AdversarialReport` (findings euristici) in uno scalare ``>= 0`` che il
|
|
GA usa per selezione e ranking.
|
|
|
|
**v1** (default, backward compat): ogni finding ``HIGH`` azzera la fitness.
|
|
Kill-switch hard a 360 gradi.
|
|
|
|
**v2** (opt-in via ``hard_kill_findings``): solo findings nel set ``hard_kill``
|
|
azzerano; gli altri HIGH applicano una penalità moltiplicativa
|
|
``1 / (1 + soft_penalty * n_soft_high)``. Restituisce gradient continuo anche
|
|
su strategie marginalmente killate da gate adversarial, permettendo
|
|
all'evoluzione di esplorare zone con 1-2 finding HIGH "soft" (es.
|
|
``fees_eat_alpha``, ``flat_too_long``, ``time_in_market_too_high``).
|
|
|
|
Formula::
|
|
|
|
sharpe_norm = 0.5 * (tanh(sharpe) + 1.0) # in [0, 1]
|
|
base = dsr_weight * dsr + sharpe_weight * sharpe_norm
|
|
dd_penalty = 1.0 / (1.0 + drawdown_penalty * max_drawdown)
|
|
adv_penalty = 1.0 (v1) o 1/(1+soft*n_soft_high) (v2)
|
|
fitness = max(0.0, base * dd_penalty * adv_penalty)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
from collections.abc import Iterable
|
|
|
|
from ..agents.adversarial import AdversarialReport, Severity
|
|
from ..agents.falsification import FalsificationReport
|
|
|
|
|
|
def compute_combined_fitness(
|
|
fitness_train: float,
|
|
fitness_oos: float | None,
|
|
alpha: float = 0.5,
|
|
) -> float:
|
|
"""Combina fitness IS e OOS in uno scalare per selection multi-objective.
|
|
|
|
Formula::
|
|
|
|
combined = alpha * fitness_train + (1 - alpha) * fitness_oos
|
|
|
|
Se ``fitness_oos`` è ``None`` o NaN, ritorna ``fitness_train`` (fallback).
|
|
alpha=1.0 → solo IS (= comportamento default). alpha=0.0 → solo OOS.
|
|
alpha=0.5 → bilanciato.
|
|
"""
|
|
if fitness_oos is None or fitness_oos != fitness_oos: # noqa: PLR0124 (NaN check)
|
|
return fitness_train
|
|
return alpha * fitness_train + (1.0 - alpha) * fitness_oos
|
|
|
|
|
|
def compute_fitness(
|
|
falsification: FalsificationReport,
|
|
adversarial: AdversarialReport,
|
|
drawdown_penalty: float = 1.0,
|
|
dsr_weight: float = 0.5,
|
|
sharpe_weight: float = 0.5,
|
|
hard_kill_findings: Iterable[str] | None = None,
|
|
adversarial_soft_penalty: float = 0.4,
|
|
) -> float:
|
|
"""Calcola la fitness scalare di una strategia.
|
|
|
|
Args:
|
|
falsification: report con DSR, Sharpe, max_drawdown, n_trades.
|
|
adversarial: report con eventuali findings euristici.
|
|
drawdown_penalty: peso del max drawdown nel denominatore della
|
|
penalita' moltiplicativa (default 1.0).
|
|
dsr_weight: peso del DSR nella base (default 0.5).
|
|
sharpe_weight: peso dello Sharpe normalizzato nella base
|
|
(default 0.5).
|
|
hard_kill_findings: nomi di findings che azzerano la fitness se
|
|
``HIGH``. ``None`` (default v1) = TUTTI gli HIGH azzerano.
|
|
Per v2 passare es. ``{"no_trades", "degenerate"}``: solo
|
|
questi azzerano, gli altri HIGH applicano soft penalty.
|
|
adversarial_soft_penalty: in v2, fattore della penalità
|
|
moltiplicativa per ogni HIGH soft (default 0.4 →
|
|
``1/(1+0.4*n)``: 1 → 0.71, 2 → 0.56, 3 → 0.45).
|
|
|
|
Returns:
|
|
Fitness ``>= 0``. Zero indica strategia da scartare (no-trade o
|
|
kill adversarial).
|
|
"""
|
|
if falsification.n_trades == 0:
|
|
return 0.0
|
|
|
|
high_findings = [f for f in adversarial.findings if f.severity == Severity.HIGH]
|
|
|
|
if hard_kill_findings is None:
|
|
# v1: tutti gli HIGH azzerano la fitness.
|
|
if high_findings:
|
|
return 0.0
|
|
adv_penalty = 1.0
|
|
else:
|
|
# v2: solo finding con name in hard_kill_findings azzerano.
|
|
hard_set = frozenset(hard_kill_findings)
|
|
if any(f.name in hard_set for f in high_findings):
|
|
return 0.0
|
|
n_soft_high = sum(1 for f in high_findings if f.name not in hard_set)
|
|
adv_penalty = 1.0 / (1.0 + adversarial_soft_penalty * n_soft_high)
|
|
|
|
dsr = max(0.0, min(1.0, float(falsification.dsr)))
|
|
sharpe_norm = 0.5 * (math.tanh(float(falsification.sharpe)) + 1.0)
|
|
base = dsr_weight * dsr + sharpe_weight * sharpe_norm
|
|
dd_penalty = 1.0 / (1.0 + drawdown_penalty * float(falsification.max_drawdown))
|
|
return max(0.0, float(base * dd_penalty * adv_penalty))
|