Compare commits
4 Commits
56a631f38a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a171acfb2 | |||
| 9d0deb3ae0 | |||
| d3662f6098 | |||
| 23c9e37f94 |
@@ -2,10 +2,20 @@
|
|||||||
|
|
||||||
Proof-of-concept di sistema co-evolutivo multi-agente per trading quantitativo. Un genetic algorithm fa evolvere una popolazione di agenti LLM (Hypothesis swarm) che generano strategie di trading espresse in JSON strutturato; un layer Falsification deterministico le backtesta su dati storici BTC-PERPETUAL via Cerbero MCP; un layer Adversarial euristico le sottopone a red-team checks; la fitness combina Deflated Sharpe Ratio (Bailey & López 2014), Sharpe normalizzato e penalizzazione di drawdown. Il tutto è ispirato alla filosofia di Renaissance Technologies adattata a un contesto retail single-author con LLM agents.
|
Proof-of-concept di sistema co-evolutivo multi-agente per trading quantitativo. Un genetic algorithm fa evolvere una popolazione di agenti LLM (Hypothesis swarm) che generano strategie di trading espresse in JSON strutturato; un layer Falsification deterministico le backtesta su dati storici BTC-PERPETUAL via Cerbero MCP; un layer Adversarial euristico le sottopone a red-team checks; la fitness combina Deflated Sharpe Ratio (Bailey & López 2014), Sharpe normalizzato e penalizzazione di drawdown. Il tutto è ispirato alla filosofia di Renaissance Technologies adattata a un contesto retail single-author con LLM agents.
|
||||||
|
|
||||||
|
## Repository
|
||||||
|
|
||||||
|
Gitea Tielogic (privato, accesso SSH):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone ssh://git@git.tielogic.xyz:222/Adriano/Multi_Swarm_Coevolutive.git
|
||||||
|
```
|
||||||
|
|
||||||
## Stato del progetto
|
## Stato del progetto
|
||||||
|
|
||||||
**Phase 1 (lean spike) completata** il 10 maggio 2026 con tutti i 5 hard gate passati (loop convergence, parse success 100%, top-5 ratio 1116x, entropy 0.914, costo $0.069 vs cap $700). Decisione strategica: **GO Phase 2** con tre aggiustamenti (Adversarial soglie più strette, speciation, walk-forward 70/30).
|
**Phase 1 (lean spike) completata** il 10 maggio 2026 con tutti i 5 hard gate passati (loop convergence, parse success 100%, top-5 ratio 1116x, entropy 0.914, costo $0.069 vs cap $700). Decisione strategica: **GO Phase 2** con tre aggiustamenti (Adversarial soglie più strette, speciation, walk-forward 70/30).
|
||||||
|
|
||||||
|
**Phase 1.5 (tactical hardening) in corso**: Adversarial layer rinforzato con soglie più strette (`overtrading` a `n_bars/20`, `undertrading` HIGH se `n<10`) e due nuovi check HIGH (`flat_too_long` se signal flat >95% bar, `fees_eat_alpha` se fees > 50% del gross PnL). Killa le strategie degeneri del run v5 (top-1 era flat 99.8% del tempo e ha sottoperformato BTC B&H di −103 punti percentuali).
|
||||||
|
|
||||||
Documenti chiave:
|
Documenti chiave:
|
||||||
|
|
||||||
- [Decisione strategica](docs/superpowers/specs/2026-05-09-decisione-strategica-design.md) — perché Phase 1 prima, Phase 2 poi, Phase 3 forward-test.
|
- [Decisione strategica](docs/superpowers/specs/2026-05-09-decisione-strategica-design.md) — perché Phase 1 prima, Phase 2 poi, Phase 3 forward-test.
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
# Phase 1.5 — Run nemotron tier C — Decision Memo
|
||||||
|
|
||||||
|
**Data**: 11 maggio 2026
|
||||||
|
**Run di riferimento**: `phase1.5-nemotron-001` (id `434c417e2b6f42bb8cf32514e5d0db1d`)
|
||||||
|
**Tier LLM**: C → `nvidia/nemotron-3-super-120b-a12b:free`
|
||||||
|
**Durata wallclock**: 2 h 26 min (08:15 → 10:11 UTC, gen 0 → gen 9)
|
||||||
|
**Spesa totale**: $0.1244 (price-table tier C; il modello effettivo è `:free` su OpenRouter, ma il cost tracker applica la pricing nominale del tier)
|
||||||
|
**Status**: ✅ Completato, ma esito strategico **NO-GO** sulla configurazione corrente
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Premessa
|
||||||
|
|
||||||
|
Il run `phase1.5-nemotron-001` è la prima esecuzione end-to-end del loop GA con:
|
||||||
|
|
||||||
|
- l'Adversarial layer aggiornato in Phase 1.5 (commits `56a631f` + `d3662f6`), con tre nuovi check HIGH (`flat_too_long`, `fees_eat_alpha`, `time_in_market_too_high`) più i due esistenti rinforzati;
|
||||||
|
- il tier C ribindato a `nvidia/nemotron-3-super-120b-a12b:free`, modello scelto in benchmark contro sette alternative per stabilità JSON e costo nullo;
|
||||||
|
- il fix `EmptyCompletionError` su `llm/client.py` (commit `9d0deb3`) introdotto durante la stessa sessione per gestire le risposte vuote che alcuni provider `:free` ritornano sporadicamente.
|
||||||
|
|
||||||
|
L'obiettivo dichiarato del run era verificare se il nuovo budget di vincoli adversarial — più stretto del v5 — fosse compatibile con la capacità generativa di nemotron, e se la popolazione riuscisse a esplorare una zona di fitness positiva non degenere.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Hard gate Phase 1 — ripercorrenza
|
||||||
|
|
||||||
|
I 5 hard gate originali (definiti nello spec strategico di Phase 1) sono stati rivalutati su questo run come sanity check, non come passaggio formale di gate.
|
||||||
|
|
||||||
|
| # | Gate | Soglia | Misura | Esito |
|
||||||
|
|---|------|--------|--------|-------|
|
||||||
|
| 1 | Loop converge | mediana cresce ≥3 gen consecutive | Gen 0→8: median oscilla tra 0.0 e 0.0073 senza crescita strutturale | ❌ FAIL |
|
||||||
|
| 2 | Parse success | ≥80% proposte LLM parse-OK | 81/89 = **91.0%** | ✅ PASS |
|
||||||
|
| 3 | Top-5 ratio | top-5 fitness ≥10× mediana | top-5 = 0.0162–0.0215; mediana ≈ 0 → ratio indefinito | ⚠️ N/A |
|
||||||
|
| 4 | Entropy | ≥0.5 a fine run | 0.845 alla gen 9 | ✅ PASS |
|
||||||
|
| 5 | Budget | costo ≤ cap | $0.1244 vs cap $700 (0.02%) | ✅ PASS |
|
||||||
|
|
||||||
|
Il gate critico è il numero 1. La popolazione non converge: il `max_fitness` resta inchiodato a `0.0215` dalla generazione 0 fino alla 9, segnale che l'elite preservation cattura un singolo genoma poco peggiore degli altri ma altrettanto inadatto, mentre il resto della popolazione non riesce a superarlo. La mediana è zero in 9 generazioni su 10 (singolo picco a 0.0073 in gen 8).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Lettura dei top genomi
|
||||||
|
|
||||||
|
I cinque genomi a fitness più alta hanno tutti caratteristiche economicamente disastrose:
|
||||||
|
|
||||||
|
| Genome ID | Fitness | DSR | Sharpe | Total return | n_trades |
|
||||||
|
|-----------|---------|-----|--------|--------------|----------|
|
||||||
|
| `0e1f9d7af25cfd6a` | 0.0215 | 0.000 | −1.083 | −115.9% | 385 |
|
||||||
|
| `85a8116ab2cd2735` | 0.0215 | 0.000 | −1.083 | −115.9% | 385 |
|
||||||
|
| `92aae563277b6f21` | 0.0193 | 0.000 | −1.129 | −131.0% | 597 |
|
||||||
|
| `01d0ca99bbdd7320` | 0.0180 | 0.000 | −1.112 | −131.7% | 602 |
|
||||||
|
| `194b096f7edab53c` | 0.0162 | 0.000 | −1.154 | −150.7% | 369 |
|
||||||
|
|
||||||
|
Il fatto che **DSR sia zero per tutti i top-5** indica che nessuna strategia passa il deflation test di Bailey & López 2014: il loop non sta generando proposte con edge statistico anche solo apparente. Il valore di fitness positivo che li seleziona deriva interamente dal termine `tanh(sharpe) × penalty(dd)` della fitness v1, che resta debolmente non nullo anche per Sharpe negativi grazie alla penalty di drawdown e a saturazioni numeriche. I primi due genomi hanno fitness identico a 0.0215 e total return identico — verosimilmente lo stesso elite riproposto a generazioni adiacenti.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Adversarial findings — il sistema fa il suo lavoro
|
||||||
|
|
||||||
|
Il layer Adversarial Phase 1.5 ha emesso 98 finding sul run:
|
||||||
|
|
||||||
|
| Severità | Check | Conteggio |
|
||||||
|
|----------|-------|-----------|
|
||||||
|
| HIGH | `fees_eat_alpha` (nuovo P1.5) | 35 |
|
||||||
|
| MEDIUM | `overtrading` | 19 |
|
||||||
|
| HIGH | `no_trades` | 16 |
|
||||||
|
| HIGH | `flat_too_long` (nuovo P1.5) | 15 |
|
||||||
|
| HIGH | `time_in_market_too_high` (nuovo P1.5) | 8 |
|
||||||
|
| HIGH | `undertrading` | 4 |
|
||||||
|
| HIGH | `degenerate` | 1 |
|
||||||
|
|
||||||
|
Il dato saliente è che i tre check introdotti in Phase 1.5 — `fees_eat_alpha`, `flat_too_long`, `time_in_market_too_high` — sono effettivamente attivi e killano strategie. In particolare `fees_eat_alpha` è la categoria più popolata: 35 occorrenze HIGH. Esempi tipici dai detail dei finding:
|
||||||
|
|
||||||
|
- `Fees $17073.82 = 2032.6% of gross $840.00`;
|
||||||
|
- `Fees $70646.03 = 12671.9% of gross $557.50`;
|
||||||
|
- `Signal flat for 98.8% of bars (>95% threshold)`.
|
||||||
|
|
||||||
|
Il messaggio è netto: il pool di strategie generato da nemotron, ai prompt e ai gradi di libertà attuali, oscilla tra due estremi degeneri — strategie inattive (flat 98%+) e strategie iperattive (overtrading + fee che divorano l'alpha lordo). Phase 1.5 cattura entrambi gli estremi, ma il loop GA non ha materiale di partenza sano da cui evolvere.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Decisione
|
||||||
|
|
||||||
|
**Esito**: NO-GO sulla combinazione `tier C = nemotron` + `Phase 1.5 adversarial` come configurazione di Phase 2.
|
||||||
|
|
||||||
|
Le ragioni a supporto della decisione sono tre.
|
||||||
|
|
||||||
|
Primo, la convergenza è assente per nove generazioni consecutive, non un plateau di selezione raggiunto dopo una fase di salita. Non si tratta cioè di un loop che ha già trovato il suo ottimo e lo conserva, ma di un loop che non ne ha trovato uno.
|
||||||
|
|
||||||
|
Secondo, la distanza dal baseline Phase 1 v5 è di un ordine di grandezza: max fitness `0.0215` qui contro `0.3347` nel run di gate Phase 1, mediana che oscilla sullo zero contro una mediana attorno a `0.005`–`0.09`. Nemotron, in questa configurazione, sta producendo proposte qualitativamente più povere di qwen-2.5-72b nello stesso schema operativo.
|
||||||
|
|
||||||
|
Terzo, i finding adversarial non puntano a un bug del sistema ma a una mancanza di edge nelle proposte. Il loop sta sanzionando correttamente — il problema è a monte, nella generazione.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Tre direzioni per Phase 2
|
||||||
|
|
||||||
|
Tre opzioni si configurano per il passo successivo. Vanno valutate prima di una nuova esecuzione, non in parallelo a essa.
|
||||||
|
|
||||||
|
**Direzione A — Riportare tier C a `qwen/qwen-2.5-72b-instruct`** (configurazione di gate Phase 1). Il run di riferimento `phase1-real-005` è già un baseline noto: max fitness `0.3347`, top genome problematico (flat 99.8%) ma generato sotto Phase 1 adversarial. Rilanciare lo stesso pool con Phase 1.5 adversarial isolerebbe l'effetto del solo hardening sul medesimo motore generativo, senza confondere variabili. Questo è il percorso più informativo nel breve.
|
||||||
|
|
||||||
|
**Direzione B — Mantenere nemotron ma rilassare i prompt di Hypothesis**. L'ipotesi alternativa è che il prompting attuale, calibrato su qwen, sia troppo terso o troppo vincolato per la modalità di ragionamento di nemotron. Iterare due o tre versioni del prompt — più esempi few-shot, vincoli espliciti su `n_trades` minimo e `time_in_market` target — può cambiare radicalmente la qualità dell'output senza cambiare il modello.
|
||||||
|
|
||||||
|
**Direzione C — Sostituire il tier C con un modello a pagamento di fascia comparabile**. Tra i benchmark precedenti, `deepseek/deepseek-v4-flash` è già usato come tier A/B nel file `.env`; promuoverlo a tier C significa accettare una spesa marginale (stima $1–3 per run di 10 gen × 20 pop) in cambio di una qualità generativa nota.
|
||||||
|
|
||||||
|
La preferenza dell'operatore per modelli cost-conscious orienta verso A o B. La direzione C resta utile come benchmark di controllo se A e B fallissero a loro volta.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Operazioni di pulizia eseguite contestualmente
|
||||||
|
|
||||||
|
- Il run zombie `phase1-real-008` (id `6ebcff9f7f6544c18ced50313cf72ca9`, marcato `running` da 07:11 UTC senza processo associato) è stato chiuso a `status='failed'` direttamente in `runs.db`, per evitare contaminazione delle query di dashboard.
|
||||||
|
- Il commit `9d0deb3` (`fix(llm): handle empty completions + missing usage`) è già su `main`. Il `client.py` ora tratta `resp.choices == []` e `resp.usage is None` come errori retryable invece che assertion failure: precondizione necessaria per qualsiasi run successivo su provider `:free`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Note per chi legge
|
||||||
|
|
||||||
|
Questo memo è un documento di decisione, non un rapporto tecnico completo. Il rapporto tecnico esteso del run può essere ricostruito da `runs.db` interrogando le tabelle `runs`, `generations`, `evaluations`, `adversarial_findings`, `cost_records` con `run_id='434c417e2b6f42bb8cf32514e5d0db1d'`. Il design Phase 1.5 e le motivazioni delle soglie adversarial restano definiti nel commit `56a631f` e nei suoi file di test.
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Adversarial agent: ispeziona una :class:`Strategy` con check euristici
|
"""Adversarial agent: ispeziona una :class:`Strategy` con check euristici
|
||||||
hand-crafted per scovare patologie note (degenerate, no-trade, over/under
|
hand-crafted per scovare patologie note (degenerate, no-trade, over/under
|
||||||
trading, flat-too-long, fees-eat-alpha) prima del training vero e proprio.
|
trading, flat-too-long, time-in-market-too-high, fees-eat-alpha) prima
|
||||||
|
del training vero e proprio.
|
||||||
|
|
||||||
Pipeline:
|
Pipeline:
|
||||||
|
|
||||||
@@ -11,10 +12,13 @@ falsificazione, ma sega presto i casi degeneri (es. ``gt close -1e9`` →
|
|||||||
sempre long) che inquinerebbero il leaderboard del swarm.
|
sempre long) che inquinerebbero il leaderboard del swarm.
|
||||||
|
|
||||||
Phase 1.5 hardening: soglie strette per overtrading (n_trades > n_bars/20)
|
Phase 1.5 hardening: soglie strette per overtrading (n_trades > n_bars/20)
|
||||||
e undertrading (HIGH se n_trades < 10), piu' due nuovi check HIGH:
|
e undertrading (HIGH se n_trades < 10), piu' tre nuovi check HIGH:
|
||||||
``flat_too_long`` (signal flat >95% delle bar) e ``fees_eat_alpha``
|
``flat_too_long`` (signal flat >95% delle bar),
|
||||||
(fees > 50% del gross_pnl positivo). Killano le strategie "lucky shot"
|
``time_in_market_too_high`` (signal long/short >80% delle bar, di fatto
|
||||||
e quelle con margine sottile non sostenibile in produzione.
|
leveraged buy-and-hold con funding/tail-risk cumulato) e
|
||||||
|
``fees_eat_alpha`` (fees > 50% del gross_pnl positivo). Killano le
|
||||||
|
strategie "lucky shot", le sempre-in-market e quelle con margine sottile
|
||||||
|
non sostenibile in produzione.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -133,6 +137,26 @@ class AdversarialAgent:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Time-in-market-too-high: signal LONG o SHORT >80% delle bar.
|
||||||
|
# Simmetrico opposto di flat_too_long: una strategia sempre-in-market
|
||||||
|
# e' di fatto leveraged buy-and-hold, esposta a funding cumulato su
|
||||||
|
# perp (paid ogni 8h), tail risk eventi notturni/weekend, nessuna
|
||||||
|
# opportunity-cost flexibility. Sweet spot fitness positiva: 5-80%
|
||||||
|
# time in market (combinato con flat_too_long).
|
||||||
|
active_ratio = n_active / n_bars if n_bars > 0 else 0.0
|
||||||
|
if active_ratio > 0.80:
|
||||||
|
report.findings.append(
|
||||||
|
Finding(
|
||||||
|
name="time_in_market_too_high",
|
||||||
|
severity=Severity.HIGH,
|
||||||
|
detail=(
|
||||||
|
f"Signal long/short for {active_ratio * 100:.1f}% of bars "
|
||||||
|
"(>80% threshold); esposizione cumulativa funding + tail risk, "
|
||||||
|
"di fatto leveraged B&H"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Fees-eat-alpha: gross_pnl > 0 ma fees > 50% del lordo.
|
# Fees-eat-alpha: gross_pnl > 0 ma fees > 50% del lordo.
|
||||||
# La strategia ha edge teorico ma il margine viene mangiato dai
|
# La strategia ha edge teorico ma il margine viene mangiato dai
|
||||||
# costi di transazione: non sostenibile in produzione.
|
# costi di transazione: non sostenibile in produzione.
|
||||||
|
|||||||
@@ -21,11 +21,16 @@ MODEL_TIER_C = "qwen/qwen-2.5-72b-instruct"
|
|||||||
MODEL_TIER_D = "meta-llama/llama-3.3-70b-instruct"
|
MODEL_TIER_D = "meta-llama/llama-3.3-70b-instruct"
|
||||||
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
|
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
|
||||||
|
|
||||||
|
class EmptyCompletionError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Errori transient: retry. RateLimit/Auth/InvalidRequest: NO retry.
|
# Errori transient: retry. RateLimit/Auth/InvalidRequest: NO retry.
|
||||||
_RETRYABLE_EXCEPTIONS: tuple[type[BaseException], ...] = (
|
_RETRYABLE_EXCEPTIONS: tuple[type[BaseException], ...] = (
|
||||||
openai.APIConnectionError,
|
openai.APIConnectionError,
|
||||||
openai.APITimeoutError,
|
openai.APITimeoutError,
|
||||||
openai.InternalServerError,
|
openai.InternalServerError,
|
||||||
|
EmptyCompletionError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -88,12 +93,13 @@ class LLMClient:
|
|||||||
top_p=genome.top_p,
|
top_p=genome.top_p,
|
||||||
max_tokens=max_tokens,
|
max_tokens=max_tokens,
|
||||||
)
|
)
|
||||||
|
if not resp.choices or resp.choices[0].message.content is None:
|
||||||
|
raise EmptyCompletionError(f"empty response from {model}")
|
||||||
usage = resp.usage
|
usage = resp.usage
|
||||||
assert usage is not None
|
|
||||||
return CompletionResult(
|
return CompletionResult(
|
||||||
text=resp.choices[0].message.content or "",
|
text=resp.choices[0].message.content,
|
||||||
input_tokens=usage.prompt_tokens,
|
input_tokens=usage.prompt_tokens if usage else 0,
|
||||||
output_tokens=usage.completion_tokens,
|
output_tokens=usage.completion_tokens if usage else 0,
|
||||||
tier=genome.model_tier,
|
tier=genome.model_tier,
|
||||||
model=model,
|
model=model,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -338,3 +338,97 @@ def test_fees_eat_alpha_flagged(monkeypatch: pytest.MonkeyPatch,
|
|||||||
f.name == "fees_eat_alpha" and f.severity == Severity.HIGH
|
f.name == "fees_eat_alpha" and f.severity == Severity.HIGH
|
||||||
for f in report.findings
|
for f in report.findings
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_time_in_market_too_high_flagged(monkeypatch: pytest.MonkeyPatch,
|
||||||
|
ohlcv: pd.DataFrame) -> None:
|
||||||
|
"""Signal LONG per >80% delle bar -> HIGH time_in_market_too_high."""
|
||||||
|
n_bars = len(ohlcv)
|
||||||
|
# 90% LONG, 10% FLAT iniziali (warmup-like) per evitare degenerate.
|
||||||
|
n_flat = int(n_bars * 0.10)
|
||||||
|
sig_values = [Side.FLAT] * n_flat + [Side.LONG] * (n_bars - n_flat)
|
||||||
|
fake_signals = pd.Series(sig_values, index=ohlcv.index, dtype=object)
|
||||||
|
# 15 trade per evitare undertrading HIGH.
|
||||||
|
fake_trades = [
|
||||||
|
_make_trade(
|
||||||
|
ohlcv.index[i * 30],
|
||||||
|
ohlcv.index[i * 30 + 1],
|
||||||
|
entry_price=100.0,
|
||||||
|
exit_price=101.0,
|
||||||
|
)
|
||||||
|
for i in range(15)
|
||||||
|
]
|
||||||
|
|
||||||
|
def fake_run(self, ohlcv: pd.DataFrame, signals: pd.Series) -> BacktestResult: # type: ignore[no-untyped-def]
|
||||||
|
return BacktestResult(
|
||||||
|
equity_curve=pd.Series([0.0] * len(ohlcv), index=ohlcv.index, name="equity"),
|
||||||
|
returns=pd.Series([0.0] * len(ohlcv), index=ohlcv.index, name="returns"),
|
||||||
|
trades=fake_trades,
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_compile(strategy): # type: ignore[no-untyped-def]
|
||||||
|
return lambda df: fake_signals
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"multi_swarm.agents.adversarial.BacktestEngine.run", fake_run
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"multi_swarm.agents.adversarial.compile_strategy", fake_compile
|
||||||
|
)
|
||||||
|
|
||||||
|
src = _MINIMAL_STRATEGY_SRC
|
||||||
|
ast = parse_strategy(src)
|
||||||
|
agent = AdversarialAgent()
|
||||||
|
report = agent.review(ast, ohlcv)
|
||||||
|
assert any(
|
||||||
|
f.name == "time_in_market_too_high" and f.severity == Severity.HIGH
|
||||||
|
for f in report.findings
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_reasonable_balanced_strategy_not_flagged(monkeypatch: pytest.MonkeyPatch,
|
||||||
|
ohlcv: pd.DataFrame) -> None:
|
||||||
|
"""Mix ~50% flat, ~25% long, ~25% short: no HIGH sui gate temporali."""
|
||||||
|
n_bars = len(ohlcv)
|
||||||
|
# Pattern ciclico: 2 flat, 1 long, 1 short per ogni gruppo da 4 bar.
|
||||||
|
# Risultato: ~50% FLAT, ~25% LONG, ~25% SHORT. flat_ratio=0.5 < 0.95,
|
||||||
|
# active_ratio=0.5 < 0.80.
|
||||||
|
pattern = [Side.FLAT, Side.FLAT, Side.LONG, Side.SHORT]
|
||||||
|
sig_values = [pattern[i % 4] for i in range(n_bars)]
|
||||||
|
fake_signals = pd.Series(sig_values, index=ohlcv.index, dtype=object)
|
||||||
|
# 15 trade per evitare undertrading HIGH.
|
||||||
|
fake_trades = [
|
||||||
|
_make_trade(
|
||||||
|
ohlcv.index[i * 30],
|
||||||
|
ohlcv.index[i * 30 + 1],
|
||||||
|
entry_price=100.0,
|
||||||
|
exit_price=101.0,
|
||||||
|
)
|
||||||
|
for i in range(15)
|
||||||
|
]
|
||||||
|
|
||||||
|
def fake_run(self, ohlcv: pd.DataFrame, signals: pd.Series) -> BacktestResult: # type: ignore[no-untyped-def]
|
||||||
|
return BacktestResult(
|
||||||
|
equity_curve=pd.Series([0.0] * len(ohlcv), index=ohlcv.index, name="equity"),
|
||||||
|
returns=pd.Series([0.0] * len(ohlcv), index=ohlcv.index, name="returns"),
|
||||||
|
trades=fake_trades,
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_compile(strategy): # type: ignore[no-untyped-def]
|
||||||
|
return lambda df: fake_signals
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"multi_swarm.agents.adversarial.BacktestEngine.run", fake_run
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"multi_swarm.agents.adversarial.compile_strategy", fake_compile
|
||||||
|
)
|
||||||
|
|
||||||
|
src = _MINIMAL_STRATEGY_SRC
|
||||||
|
ast = parse_strategy(src)
|
||||||
|
agent = AdversarialAgent()
|
||||||
|
report = agent.review(ast, ohlcv)
|
||||||
|
# I due gate temporali non devono triggerare.
|
||||||
|
names = [f.name for f in report.findings]
|
||||||
|
assert "flat_too_long" not in names
|
||||||
|
assert "time_in_market_too_high" not in names
|
||||||
|
|||||||
Reference in New Issue
Block a user