feat(games): sessioni 2-3 Blind Traders (opzioni/session/grid) + gate PORT06 e tooling reset
- Gioco GRID TRADERS (sessione 3, regola STRATEGIA_GRIGLIA.md): grid_engine (backtest causale fee-aware della griglia geometrica), grid_brief (digest anonimo per dimensionare la griglia), grid_arena (torneo 100 agenti); diario docs/diary/2026-06-10-grid-traders-game3.md - Gioco OPZIONI: options_engine (BS + skew fittato + DVOL storica), options_arena, opt_calibrate (superficie premi REALE da cerbero-bite) - Gioco SESSION: session_engine/session_arena (pattern orari intraday) - arena: vincolo GAME_NO_LIVE=1 (vieta pairs e fade zscore/breakout/momentum gia' live, coercizione a trend/ma_cross) + normalize del candidato PRIMA della valutazione nel hill-climb - Gate: grid_game_gate (griglia ETH vincitrice vs PORT06, mark-to-market), pairs30m_gate (ETH/BTC 30m ridondante col 15m gia' deployato?) - reset_flatten: flatten one-shot del conto testnet per il reset portafoglio - .gitignore: data/portfolio_paper_stats/ (stato runtime sleeve paper-only) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
Grid engine — gioco "Grid Traders" (sessione 3), regola: STRATEGIA_GRIGLIA.md.
|
||||
|
||||
100 agenti ciechi ricevono due serie anonime (X=A=BTC, Y=B=ETH, mai rivelate) e
|
||||
propongono la CONFIGURAZIONE di una griglia di trading secondo la spec del
|
||||
documento STRATEGIA_GRIGLIA.md. Questo motore la backtesta in modo
|
||||
deterministico, causale e fee-aware:
|
||||
|
||||
- griglia GEOMETRICA dentro un range definito al deploy su close[i] (§3.2):
|
||||
ratio = (RANGE_HIGH/RANGE_LOW)^(1/GRID_LEVELS), livello[k] = RL * ratio^k
|
||||
Il range e' parametrizzato in PERCENTUALE attorno al prezzo di deploy
|
||||
(range_down/range_up), cosi' la griglia e' backtestabile su tutta la storia.
|
||||
- capitale suddiviso in anticipo: quote_per_livello = 1/GRID_LEVELS (§3.3)
|
||||
- VINCOLO BREAK-EVEN (§4): passo > MARGINE(1.5) x costo round-trip.
|
||||
Se violato il motore SI RIFIUTA DI PARTIRE (come da spec): spec squalificata.
|
||||
- ciclo (§5.2): compra quote_per_livello su attraversamento VERSO IL BASSO di un
|
||||
livello non riempito; vendi quel livello su attraversamento VERSO L'ALTO del
|
||||
livello successivo. Livelli FISSI per tutto l'episodio (non inseguono il prezzo).
|
||||
- guardie (§5.2/§6): STOP-LOSS sotto RANGE_LOW e TAKE-PROFIT sopra RANGE_HIGH
|
||||
hanno priorita' su tutto: liquidano l'intera posizione e fermano la griglia.
|
||||
- episodi: quando una griglia muore (SL / TP / max_bars) se ne deploya una nuova
|
||||
sul prezzo corrente (il "riavvio del bot" di §6.6, qui automatizzato).
|
||||
|
||||
Causalita': il deploy a close[i] usa solo close[i]; i fill avvengono dalle barre
|
||||
successive lungo il percorso intrabar O->L->H->C (se close>=open) o O->H->L->C.
|
||||
Fee 0.10% round-trip per livello (baseline Deribit del progetto) + slippage
|
||||
opzionale per lato (GAME_SLIP), come negli altri giochi.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from bisect import bisect_left, bisect_right
|
||||
|
||||
import numpy as np
|
||||
|
||||
from scripts.games.engine import load_anon, splits3, TF_BPM, FEE_RT
|
||||
|
||||
MIN_TRADES_PER_MONTH = 10.0
|
||||
MARGIN = 1.5 # margine di sicurezza del vincolo break-even (§4)
|
||||
|
||||
_SLIP = 0.0 # slippage per LATO (oltre alle fee), come engine.py
|
||||
|
||||
|
||||
def set_slippage(slip_per_side: float):
|
||||
global _SLIP
|
||||
_SLIP = float(slip_per_side)
|
||||
|
||||
|
||||
def cost_rt(fee: float = FEE_RT) -> float:
|
||||
"""Costo di un round-trip completo (fee RT + 2 lati di slippage)."""
|
||||
return fee + 2 * _SLIP
|
||||
|
||||
|
||||
def grid_ratio(p) -> float:
|
||||
"""Ratio geometrico della griglia: indipendente dal prezzo di deploy."""
|
||||
rd, ru, L = float(p["range_down"]), float(p["range_up"]), int(p["levels"])
|
||||
return ((1.0 + ru) / (1.0 - rd)) ** (1.0 / L)
|
||||
|
||||
|
||||
def max_levels(range_down: float, range_up: float, fee: float = FEE_RT) -> int:
|
||||
"""Massimo numero di livelli che rispetta il vincolo break-even (§4)."""
|
||||
width = math.log((1.0 + range_up) / (1.0 - range_down))
|
||||
min_step = math.log(1.0 + MARGIN * cost_rt(fee))
|
||||
return max(0, int(math.floor(width / min_step)))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Backtest della griglia (episodi deploy -> SL/TP/timeout -> redeploy)
|
||||
# --------------------------------------------------------------------------
|
||||
def _backtest_grid(o, p, fee=FEE_RT):
|
||||
"""Ritorna l'array dei net-return per trade (round-trip o liquidazione),
|
||||
in frazione del capitale dell'episodio. None se il vincolo break-even
|
||||
e' violato (il bot si rifiuta di partire, §4)."""
|
||||
op, hi, lo, cl = o["open"], o["high"], o["low"], o["close"]
|
||||
n = len(cl)
|
||||
crt = cost_rt(fee)
|
||||
L = int(p["levels"])
|
||||
rd, ru = float(p["range_down"]), float(p["range_up"])
|
||||
slb, tpb = float(p["sl_buf"]), float(p["tp_buf"])
|
||||
max_bars = max(1, int(p["max_bars"]))
|
||||
if L < 2:
|
||||
return None
|
||||
ratio = grid_ratio(p)
|
||||
step = ratio - 1.0
|
||||
if step <= MARGIN * crt:
|
||||
return None # §4: vincolo break-even violato
|
||||
lstep = math.log(ratio)
|
||||
with np.errstate(divide="ignore"):
|
||||
llo = np.log(lo)
|
||||
lhi = np.log(hi)
|
||||
qpl = 1.0 / L
|
||||
rets = []
|
||||
|
||||
i = 20 # warmup minimo (parita' con engine.py)
|
||||
while i < n - 1:
|
||||
px = float(cl[i])
|
||||
if not np.isfinite(px) or px <= 0:
|
||||
i += 1
|
||||
continue
|
||||
rl_ = px * (1.0 - rd)
|
||||
lv = [rl_ * ratio ** k for k in range(L + 1)] # lv[L] = RANGE_HIGH
|
||||
sl = rl_ * (1.0 - slb)
|
||||
tp = lv[L] * (1.0 + tpb)
|
||||
off = math.log(rl_)
|
||||
end = min(n - 1, i + max_bars)
|
||||
# indice-cella (floor) di low/high per il fast-skip delle barre quiete
|
||||
klo = np.floor((llo[i + 1:end + 1] - off) / lstep).astype(np.int64)
|
||||
khi = np.floor((lhi[i + 1:end + 1] - off) / lstep).astype(np.int64)
|
||||
slhit = lo[i + 1:end + 1] <= sl
|
||||
tphit = hi[i + 1:end + 1] >= tp
|
||||
filled = [False] * L
|
||||
n_open = 0
|
||||
cur = px
|
||||
kc = bisect_right(lv, cur) - 1
|
||||
done = False
|
||||
exit_i = end
|
||||
for j in range(i + 1, end + 1):
|
||||
jj = j - (i + 1)
|
||||
if klo[jj] == khi[jj] == kc and not slhit[jj] and not tphit[jj]:
|
||||
cur = cl[j] # barra quieta: nessun livello toccato
|
||||
continue
|
||||
pts = (op[j], lo[j], hi[j], cl[j]) if cl[j] >= op[j] \
|
||||
else (op[j], hi[j], lo[j], cl[j])
|
||||
for q in pts:
|
||||
q = float(q)
|
||||
if q == cur:
|
||||
continue
|
||||
if q < cur:
|
||||
# discesa: fill dei buy-level attraversati (alto -> basso)
|
||||
k1 = bisect_left(lv, q) # primo livello >= q
|
||||
k2 = bisect_left(lv, cur) - 1 # ultimo livello < cur
|
||||
for k in range(min(k2, L - 1), max(k1, 0) - 1, -1):
|
||||
if not filled[k]:
|
||||
filled[k] = True
|
||||
n_open += 1
|
||||
if q <= sl:
|
||||
# STOP-LOSS: vendi tutta la posizione a sl, ferma la griglia
|
||||
if n_open:
|
||||
rets.append(sum(
|
||||
qpl * (sl / lv[k] - 1.0 - crt)
|
||||
for k in range(L) if filled[k]))
|
||||
done = True
|
||||
cur = q
|
||||
break
|
||||
else:
|
||||
# salita: vendi i livelli riempiti il cui target e' attraversato
|
||||
m1 = bisect_right(lv, cur) # primo livello > cur
|
||||
m2 = bisect_right(lv, q) - 1 # ultimo livello <= q
|
||||
for m in range(max(m1, 1), min(m2, L) + 1):
|
||||
k = m - 1
|
||||
if filled[k]:
|
||||
rets.append(qpl * (lv[m] / lv[k] - 1.0 - crt))
|
||||
filled[k] = False
|
||||
n_open -= 1
|
||||
if q >= tp:
|
||||
# TAKE-PROFIT: chiudi il residuo a tp, ferma la griglia
|
||||
if n_open:
|
||||
rets.append(sum(
|
||||
qpl * (tp / lv[k] - 1.0 - crt)
|
||||
for k in range(L) if filled[k]))
|
||||
done = True
|
||||
cur = q
|
||||
break
|
||||
cur = q
|
||||
if done:
|
||||
exit_i = j
|
||||
break
|
||||
kc = bisect_right(lv, cur) - 1
|
||||
if not done:
|
||||
# timeout max_bars: liquida il residuo al close dell'ultima barra
|
||||
if n_open:
|
||||
rets.append(sum(
|
||||
qpl * (cl[end] / lv[k] - 1.0 - crt)
|
||||
for k in range(L) if filled[k]))
|
||||
exit_i = end
|
||||
i = exit_i # redeploy sul prezzo dove e' morta la griglia
|
||||
return np.array(rets) if rets else np.array([])
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Valutazione + scoring (stessa fitness degli altri giochi)
|
||||
# --------------------------------------------------------------------------
|
||||
def evaluate(data, spec, sl=None, fee=FEE_RT):
|
||||
"""spec = {series: 'A'|'B', tf, params{range_down,range_up,levels,sl_buf,
|
||||
tp_buf,max_bars}}. Ritorna dict metriche (fitness = pnl + 50*win)."""
|
||||
series = spec.get("series", "A")
|
||||
p = spec["params"]
|
||||
o = data[series]
|
||||
if sl is not None:
|
||||
s, e = sl
|
||||
o = {k: v[s:e] for k, v in o.items()}
|
||||
rets = _backtest_grid(o, p, fee)
|
||||
nbars = len(o["close"])
|
||||
months = nbars / data.get("bpm", TF_BPM["1h"])
|
||||
if rets is None:
|
||||
# il bot si rifiuta di partire (vincolo break-even §4)
|
||||
return dict(n_trades=0, win_rate=0.0, pnl_pct=0.0, tpm=0.0, sharpe=0.0,
|
||||
avg_ret=0.0, qualified=False, refused=True, fitness=-2e6)
|
||||
n_tr = len(rets)
|
||||
tpm = n_tr / months if months > 0 else 0.0
|
||||
if n_tr == 0:
|
||||
return dict(n_trades=0, win_rate=0.0, pnl_pct=0.0, tpm=0.0, sharpe=0.0,
|
||||
avg_ret=0.0, qualified=False, refused=False, fitness=-1e6)
|
||||
win_rate = float(np.mean(rets > 0))
|
||||
pnl = float(np.sum(rets)) * 100
|
||||
avg = float(np.mean(rets)) * 100
|
||||
sharpe = float(np.mean(rets) / (np.std(rets) + 1e-12) * np.sqrt(tpm * 12)) \
|
||||
if np.std(rets) > 0 else 0.0
|
||||
qualified = tpm >= MIN_TRADES_PER_MONTH
|
||||
fitness = pnl + 50.0 * win_rate
|
||||
if not qualified:
|
||||
fitness = -1e6 + pnl
|
||||
return dict(n_trades=n_tr, win_rate=win_rate, pnl_pct=pnl, tpm=tpm,
|
||||
sharpe=sharpe, avg_ret=avg, qualified=qualified, refused=False,
|
||||
fitness=fitness)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import time
|
||||
data = load_anon("1h")
|
||||
print("loaded", data["n"], "bars,", data["dt"][0], "->", data["dt"][-1])
|
||||
tr, va, te = splits3(data)
|
||||
demo = {"series": "B", "tf": "1h",
|
||||
"params": {"range_down": 0.10, "range_up": 0.10, "levels": 12,
|
||||
"sl_buf": 0.05, "tp_buf": 0.05, "max_bars": 1000}}
|
||||
t0 = time.time()
|
||||
print("TRAIN", evaluate(data, demo, tr))
|
||||
print("VALID", evaluate(data, demo, va))
|
||||
print("TEST ", evaluate(data, demo, te))
|
||||
print("FULL ", evaluate(data, demo, None))
|
||||
print(f"4 eval in {time.time()-t0:.2f}s")
|
||||
Reference in New Issue
Block a user