Compare commits
2 Commits
56a631f38a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 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.
|
||||
|
||||
## Repository
|
||||
|
||||
Gitea Tielogic (privato, accesso SSH):
|
||||
|
||||
```bash
|
||||
git clone ssh://git@git.tielogic.xyz:222/Adriano/Multi_Swarm_Coevolutive.git
|
||||
```
|
||||
|
||||
## 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.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:
|
||||
|
||||
- [Decisione strategica](docs/superpowers/specs/2026-05-09-decisione-strategica-design.md) — perché Phase 1 prima, Phase 2 poi, Phase 3 forward-test.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Adversarial agent: ispeziona una :class:`Strategy` con check euristici
|
||||
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:
|
||||
|
||||
@@ -11,10 +12,13 @@ falsificazione, ma sega presto i casi degeneri (es. ``gt close -1e9`` →
|
||||
sempre long) che inquinerebbero il leaderboard del swarm.
|
||||
|
||||
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:
|
||||
``flat_too_long`` (signal flat >95% delle bar) e ``fees_eat_alpha``
|
||||
(fees > 50% del gross_pnl positivo). Killano le strategie "lucky shot"
|
||||
e quelle con margine sottile non sostenibile in produzione.
|
||||
e undertrading (HIGH se n_trades < 10), piu' tre nuovi check HIGH:
|
||||
``flat_too_long`` (signal flat >95% delle bar),
|
||||
``time_in_market_too_high`` (signal long/short >80% delle bar, di fatto
|
||||
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
|
||||
@@ -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.
|
||||
# La strategia ha edge teorico ma il margine viene mangiato dai
|
||||
# costi di transazione: non sostenibile in produzione.
|
||||
|
||||
@@ -338,3 +338,97 @@ def test_fees_eat_alpha_flagged(monkeypatch: pytest.MonkeyPatch,
|
||||
f.name == "fees_eat_alpha" and f.severity == Severity.HIGH
|
||||
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