chore(reset): v2.0.0 — storico certificato Deribit mainnet, ripartenza pulita
Reset del progetto su fondamenta verificate dopo la scoperta che l'intera libreria "validata OOS" era artefatto di feed contaminato (print fantasma del feed Cerbero TESTNET + storico Binance/USDT). - Storico ricostruito da Deribit MAINNET (ccxt pubblico, tokenless) e CERTIFICATO (certify_feed.py): BTC/ETH puliti su TUTTA la storia (mediana 2-6 bps vs Coinbase USD), integrita' OHLC + coerenza resample (maxΔ 0.00) + cross-venue OK. Alt esclusi (illiquidi/divergenti: LTC/DOGE 50-82% barre flat; XRP/BNB non certificabili). - Verdetto sul feed pulito: FADE / PAIRS / XS01 / TSM01 morti (ogni portafoglio Sharpe -2.3..-3.0, DD ~40%); solo SH01 e frammenti HONEST con segnale residuo, da ri-validare in isolamento. - Cleanup "restart pulito": strategie, stack live (src/live, src/portfolio, runner/executor, yml, docker), ~100 script ricerca/gate, waste/games/ portfolios, dati non certificati + cache e 60+ diari -> archiviati in Old/ (preservati, non cancellati). Diario consolidato in un unico documento. - Skeleton ricerca tenuto: Strategy ABC + indicatori + src/fractal + src/backtest/engine + load_data; tool dati certificati (rebuild_history, certify_feed, audit_feed, multi_source_check). - Universo dati ATTIVO: solo BTC/ETH (5m/15m/1h); guardrail fisico (load_data su alt -> FileNotFoundError). Esecuzione DISABILITATA, conto flat. Co-Authored-By: Claude Opus 4.8 (1M context) <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