"""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))