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>
This commit is contained in:
@@ -1,30 +1,32 @@
|
||||
"""Fitness function v1 della Phase 1.
|
||||
"""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.
|
||||
|
||||
Versione v1: rispetto alla v0 (DSR meno penalita' lineare di drawdown, clamp
|
||||
a zero) la formula e' continua e quasi sempre strettamente positiva, in modo
|
||||
da fornire un gradient anche su strategie mediocri o con Sharpe negativo.
|
||||
Restano due kill-switch hard (no-trade, finding HIGH adversarial) che azzerano
|
||||
la fitness.
|
||||
**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
|
||||
penalty = 1.0 / (1.0 + drawdown_penalty * max_drawdown)
|
||||
fitness = max(0.0, base * penalty)
|
||||
|
||||
Con i default ``dsr_weight = sharpe_weight = 0.5`` la base e' in ``[0, 1]`` e
|
||||
``penalty`` in ``(0, 1]``: fitness e' bounded in ``[0, 1]`` per input sani e
|
||||
mai esattamente zero finche' Sharpe e' finito e ``max_dd`` finito.
|
||||
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
|
||||
@@ -36,36 +38,51 @@ def compute_fitness(
|
||||
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 (v1, continua).
|
||||
"""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). Valori piu' alti
|
||||
penalizzano piu' severamente strategie con DD alto.
|
||||
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). Valori tipici per strategie sane: ``[0.05, 1.0]``.
|
||||
|
||||
Logica:
|
||||
1. ``n_trades == 0`` → 0 (nessuna evidenza, sega subito).
|
||||
2. Almeno un finding ``HIGH`` adversarial → 0 (kill).
|
||||
3. Altrimenti combina DSR e ``tanh(sharpe)`` normalizzato in
|
||||
``[0, 1]``, modulato da una penalita' continua del drawdown
|
||||
``1 / (1 + k * max_dd)``.
|
||||
kill adversarial).
|
||||
"""
|
||||
if falsification.n_trades == 0:
|
||||
return 0.0
|
||||
if any(f.severity == Severity.HIGH for f in adversarial.findings):
|
||||
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
|
||||
penalty = 1.0 / (1.0 + drawdown_penalty * float(falsification.max_drawdown))
|
||||
return max(0.0, float(base * penalty))
|
||||
dd_penalty = 1.0 / (1.0 + drawdown_penalty * float(falsification.max_drawdown))
|
||||
return max(0.0, float(base * dd_penalty * adv_penalty))
|
||||
|
||||
@@ -52,6 +52,10 @@ class RunConfig:
|
||||
prompt_mutation_weight: float = 0.0 # Phase 2.5: opt-in LLM mutator
|
||||
fees_eat_alpha_threshold: float = 0.5 # adversarial gate, allenta verso 0.7-0.8
|
||||
flat_too_long_threshold: float = 0.95 # adversarial gate, allenta verso 0.98-0.99
|
||||
# Fitness v2: tuple non vuota → soft-kill (solo findings listate azzerano).
|
||||
# None/empty → v1 (tutti HIGH azzerano, backward compat).
|
||||
fitness_hard_kill_findings: tuple[str, ...] | None = None
|
||||
fitness_adversarial_soft_penalty: float = 0.4
|
||||
|
||||
|
||||
def run_phase1(
|
||||
@@ -152,7 +156,11 @@ def run_phase1(
|
||||
severity=finding.severity.value,
|
||||
detail=finding.detail,
|
||||
)
|
||||
fit = compute_fitness(fals, adv)
|
||||
fit = compute_fitness(
|
||||
fals, adv,
|
||||
hard_kill_findings=cfg.fitness_hard_kill_findings,
|
||||
adversarial_soft_penalty=cfg.fitness_adversarial_soft_penalty,
|
||||
)
|
||||
repo.save_evaluation(
|
||||
run_id=run_id,
|
||||
genome_id=genome.id,
|
||||
|
||||
Reference in New Issue
Block a user