merge lavoro/goldrake-u9 (secondo giro 27/09)
This commit is contained in:
+4
-4
@@ -4,8 +4,8 @@ TP01 e SKH01 tradano lo STESSO strumento (BTC/ETH _USDC-PERPETUAL). Su Deribit e
|
||||
posizione netta per strumento per conto -> non si possono tenere due gambe separate: si combinano in
|
||||
software in un unico target netto, e si manda UN ordine per asset per raggiungerlo.
|
||||
|
||||
Formula (preserva il budget di rischio $300/asset, split 75/25; coerente col blend di ritorni
|
||||
deribit_book_sleeves = 0.75*TP01 + 0.25*SKH01):
|
||||
Formula (preserva il budget di rischio per-asset di config (CAP sotto: `max_notional_per_asset_usd`
|
||||
o `_frac`), split 75/25; coerente col blend di ritorni deribit_book_sleeves = 0.75*TP01 + 0.25*SKH01):
|
||||
|
||||
net_target_usd[asset] = clamp( WEIGHT * E * (W_TP01 * tp_frac + W_SKH * skh_sign), ±CAP )
|
||||
|
||||
@@ -13,8 +13,8 @@ deribit_book_sleeves = 0.75*TP01 + 0.25*SKH01):
|
||||
tp_frac = target TP01 (causale, >=0, long-flat) da TrendPortfolio.current_target
|
||||
skh_sign = +1 long / -1 short / 0 flat da _skyhook_positions (book 230m)
|
||||
E = equity reale del conto (fallback paper se offline)
|
||||
CAP = cap notional per-asset. Fisso = max_notional_per_asset_usd ($300), OPPURE
|
||||
DINAMICO = equity * max_notional_per_asset_frac (es. 0.5 -> equity/2) se la
|
||||
CAP = cap notional per-asset. Fisso = `max_notional_per_asset_usd` di config, OPPURE
|
||||
DINAMICO = equity * `max_notional_per_asset_frac` (es. 0.5 -> equity/2) se la
|
||||
frazione e' presente in config -> il cap cresce col capitale e non strozza il
|
||||
book quando depositi (frontiera 2026-07-03). Dinamico SOLO con equity reale
|
||||
fidata; su fallback/offline si torna al cap fisso (protezione downside).
|
||||
|
||||
+11
-8
@@ -1,13 +1,16 @@
|
||||
"""Accesso Deribit MAINNET in SOLA LETTURA (via Cerbero MCP) + costruttore ordini deterministico.
|
||||
"""Accesso Deribit MAINNET (via Cerbero MCP) + costruttore ordini deterministico. La classe
|
||||
`DeribitRead` qui e' in SOLA LETTURA (nessun metodo di trading, by design); l'esecuzione REALE la
|
||||
estende in un modulo separato (`src.live.execution.DeribitTrader`) — non e' piu' un progetto
|
||||
futuro (#35 A9): e' ARMATA e LIVE su Deribit mainnet dal 2026-06-23 (`execution_enabled` in
|
||||
config/live.json), usata da `scripts/live/book_execute.py`.
|
||||
|
||||
Serve lo SHADOW MODE di TP01 (`scripts/live/live_trend.py`): legge prezzi / conto / posizioni REALI
|
||||
dal mainnet (token `.env.mainnet`) e costruisce gli ordini di ribilancio **senza inviarli**. Qui NON
|
||||
esiste alcun metodo di trading — by design: l'unica via per piazzare ordini sara' un modulo separato,
|
||||
abilitato esplicitamente, dopo la validazione shadow + micro-test.
|
||||
Da qui vengono prezzi / conto / posizioni REALI (token `.env.mainnet`): li usa sia il libro live
|
||||
(TP01+SKH01, via src.live.book + execution) sia lo shadow mode e i monitor di sorveglianza.
|
||||
|
||||
Disciplina del progetto: **testnet FUORI** (feed farlocco, causa del reset v2.0.0). Solo mainnet reale,
|
||||
e in questa fase solo in lettura. I contratti sono ristretti a BTC/ETH-PERPETUAL (inverse: `amount`
|
||||
in USD notional, step verificato su Deribit: BTC $10, ETH $1).
|
||||
Disciplina del progetto: **testnet FUORI** (feed farlocco, causa del reset v2.0.0). Solo mainnet
|
||||
reale. I contratti ESEGUITI sono i perp LINEARI USDC (`_USDC-PERPETUAL`, non inverse): `amount` in
|
||||
base-coin (BTC/ETH), margine e P&L in USDC — vedi `_CONTRACT` sotto per step/tick/minimi per
|
||||
strumento.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -23,10 +23,11 @@ from dataclasses import dataclass
|
||||
from src.live.deribit import DISASTER_LABEL, DeribitRead, disaster_stop_price, notional_to_amount, quantize_price
|
||||
|
||||
# Conto USDC -> perp LINEARE USDC (amount in base-coin). MAX_AMOUNT = TETTO HARD anti-fat-finger di
|
||||
# DEFAULT (~$630/$430 su un conto ~$600), usato SOLO quando nessuno ha chiamato set_open_cap() (il
|
||||
# micro-test, che invia comunque size fissa minima). Sul libro il tetto per apertura lo deriva
|
||||
# set_open_cap() dal cap di config (config/live.json max_notional_per_asset_usd, #18): backstop
|
||||
# sopra il sizing di TP01/book, non il sizing operativo.
|
||||
# DEFAULT (in coin, sotto), usato SOLO quando nessuno ha chiamato set_open_cap() (il micro-test,
|
||||
# che invia comunque size fissa minima). Sul libro il tetto per apertura lo deriva set_open_cap()
|
||||
# dal cap di config (config/live.json max_notional_per_asset_usd, #18): backstop sopra il sizing
|
||||
# di TP01/book, non il sizing operativo. (#35 A9: via il "~$630/$430 su conto ~$600" cablato —
|
||||
# MAX_AMOUNT e' in coin, non in dollari, e non e' il tetto operativo da quando esiste set_open_cap.)
|
||||
ALLOWED = {"BTC_USDC-PERPETUAL", "ETH_USDC-PERPETUAL"}
|
||||
MAX_AMOUNT = {"BTC_USDC-PERPETUAL": 0.01, "ETH_USDC-PERPETUAL": 0.25} # DEFAULT — vedi set_open_cap()
|
||||
FLAT_USD = 1.0 # |notional| < $1 = posizione considerata flat
|
||||
|
||||
+33
-22
@@ -28,19 +28,21 @@ col codice 11051, e' manutenzione; se non risponde affatto, e' il venue ma per u
|
||||
se la sonda stessa non parte, il verdetto e' IGNOTO — **«non vedo» non e' «va tutto bene»** (P5).
|
||||
La sonda parte SOLO quando qualcosa e' gia' fallito: sul percorso sano non costa niente.
|
||||
|
||||
⚠️ **LA FINESTRA NON E' UNA PROVA.** `in_release_window()` dice soltanto che l'orologio e'
|
||||
compatibile con lo slot di release. Non declassa niente da sola: declassa solo cio' che la SONDA
|
||||
ha gia' riconosciuto come manutenzione, e solo dentro la durata ANNUNCIATA. Una manutenzione che
|
||||
sfora resta rumorosa (il 18/08 Deribit annuncio' 15-30 minuti e resto' giu' oltre l'ora), e un
|
||||
guasto vero di martedi' mattina resta rumoroso: altrimenti si sarebbe costruito il silenzio
|
||||
proprio nell'ora in cui e' piu' probabile che serva. E' la stessa logica di
|
||||
`venue_watch.MAINT_GRACE_HOURS` — *declassa dentro la tolleranza, RIALZA oltre* — applicata pero'
|
||||
a una domanda diversa (**questo giro** e' spiegato?) e quindi alla risoluzione del giro orario,
|
||||
non delle ore consecutive. Sono due politiche vicine e distinte: non accorparle senza misurare.
|
||||
|
||||
REGOLA P9 — *un allarme massimo speso per un evento atteso e' un allarme che non verra' letto il
|
||||
giorno che e' vero.* E' il motivo per cui `atteso` esiste: declassa la GRAVITA', **non** registra
|
||||
di meno. L'episodio finisce nel log e nella notifica comunque.
|
||||
⚠️ **IL RAMO CHE DECLASSAVA E' MORTO, RIMOSSO DA `diagnose()` (#35 B11, 27/09/2026).**
|
||||
`in_release_window()` diceva se l'orologio cadeva nello slot di release Deribit (martedi'
|
||||
09:00-09:30 UTC); `diagnose()` la usava per declassare la GRAVITA' di una manutenzione gia'
|
||||
riconosciuta dalla SONDA (REGOLA P9 — *un allarme massimo speso per un evento atteso e' un
|
||||
allarme che non verra' letto il giorno che e' vero*). Ma l'UNICO chiamante di `diagnose()` e'
|
||||
`book_execute.py`, in cron al minuto **:47** di ogni ora (dal 25/08): la finestra 09:00-09:30 non
|
||||
contiene mai :47, quindi quel ramo non poteva MAI diventare vero — si leggeva come una
|
||||
declassazione attiva e non lo e' mai stata. Il campo `atteso` RESTA sul verdetto
|
||||
(`book_execute.py` lo scrive nel dict del report: cambiare il formato per una pulizia e' un costo
|
||||
che non vale la pena) ma e' hardcoded a False in `diagnose()`. `in_release_window()` e le
|
||||
costanti `RELEASE_*` restano invece nel modulo: le usa `test_book_cadenza.py`/`test_revisione.py`
|
||||
per verificare che i cron di book/review evitino lo stesso slot — uno scopo indipendente da
|
||||
`diagnose()` e ancora valido. Per declassare di nuovo un evento annunciato in `diagnose()` la
|
||||
strada e' legarlo alla DURATA dell'annuncio (`venue_news`, se e quando esiste), non all'orologio
|
||||
di un chiamante che non e' mai sincrono con lo slot.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -53,7 +55,7 @@ import requests
|
||||
from src.live.venue_watch import is_maintenance # P1: la firma 11051 vive LI', non qui
|
||||
|
||||
# --- verdetti -----------------------------------------------------------------------------
|
||||
V_MANUTENZIONE = "VENUE_MANUTENZIONE" # il venue dichiara 11051: atteso se dentro lo slot
|
||||
V_MANUTENZIONE = "VENUE_MANUTENZIONE" # il venue dichiara 11051
|
||||
V_VENUE_GIU = "VENUE_GIU" # il venue non risponde, e non dice manutenzione
|
||||
V_GATEWAY = "GATEWAY" # il venue sta bene -> il guasto e' NOSTRO (riparabile)
|
||||
V_IGNOTO = "IGNOTO" # la sonda non e' partita: "non vedo" (P5)
|
||||
@@ -70,8 +72,12 @@ PROBE_TIMEOUT = 8.0
|
||||
# 30/06/2026 alle 09:00 UTC, "down for around 15-30 minutes").
|
||||
# ⚠️ M10: *due punti non fanno una tendenza*. Qui i punti sono quattro e c'e' una meccanica
|
||||
# dichiarata dal venue, ma resta una REGOLARITA' OSSERVATA, non un contratto: se Deribit sposta
|
||||
# lo slot, questa costante mente in silenzio. E' per questo che la finestra non decide da sola —
|
||||
# decide la sonda, e la finestra al massimo declassa.
|
||||
# lo slot, questa costante mente in silenzio.
|
||||
# ⚠️ #35 B11 (27/09/2026): questa finestra NON declassa piu' niente in `diagnose()` (vedi sotto):
|
||||
# l'unico chiamante gira in cron al minuto :47, che non ci cade mai dentro. La funzione e le
|
||||
# costanti restano perche' `test_book_cadenza.py`/`test_revisione.py` le usano per un controllo
|
||||
# diverso e ancora valido: che i cron di `book`/`review` cadano FUORI da questo slot (P1, la
|
||||
# stessa finestra, non una seconda dichiarata a mano).
|
||||
RELEASE_WEEKDAY = 1 # lunedi'=0 -> 1 = martedi'
|
||||
RELEASE_START_MIN = 9 * 60 # 09:00 UTC
|
||||
RELEASE_LEN_MIN = 30 # durata ANNUNCIATA. Oltre questa la manutenzione torna una notizia.
|
||||
@@ -79,7 +85,8 @@ RELEASE_LEN_MIN = 30 # durata ANNUNCIATA. Oltre questa la manutenzione to
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Diagnosi:
|
||||
"""Il verdetto (*cosa*), la spiegazione (*perche'*), e se l'evento era atteso (P9)."""
|
||||
"""Il verdetto (*cosa*) e la spiegazione (*perche'*). `atteso` resta nel formato ma e' sempre
|
||||
False (#35 B11): l'unico chiamante non cade mai nella finestra che l'avrebbe declassato."""
|
||||
verdetto: str
|
||||
perche: str
|
||||
atteso: bool
|
||||
@@ -87,7 +94,8 @@ class Diagnosi:
|
||||
|
||||
@property
|
||||
def gravita(self) -> str:
|
||||
"""Emoji del titolo di notifica. `atteso` declassa, non silenzia (P9)."""
|
||||
"""Emoji del titolo di notifica. `atteso` e' sempre False (#35 B11): ramo morto lasciato
|
||||
per compatibilita' di formato, non per una declassazione che avviene ancora."""
|
||||
if self.atteso:
|
||||
return "ℹ️"
|
||||
return {V_GATEWAY: "🛑", V_VENUE_GIU: "🛑", V_MANUTENZIONE: "⚠️"}.get(self.verdetto, "⚠️")
|
||||
@@ -106,8 +114,9 @@ class Diagnosi:
|
||||
def in_release_window(now: datetime | None = None) -> bool:
|
||||
"""L'orologio e' dentro lo slot di release Deribit (martedi' 09:00-09:30 UTC)?
|
||||
|
||||
PURA e testabile. Non e' una prova di niente da sola: vedi la nota in testa al modulo.
|
||||
"""
|
||||
PURA e testabile. Non declassa piu' niente in `diagnose()` (#35 B11): resta per chi verifica
|
||||
che un cron (book, review) sia schedulato fuori da questo slot (P1: la finestra si deriva da
|
||||
qui, non si riscrive una seconda volta nel test)."""
|
||||
now = now or datetime.now(timezone.utc)
|
||||
now = now.astimezone(timezone.utc)
|
||||
if now.weekday() != RELEASE_WEEKDAY:
|
||||
@@ -136,7 +145,6 @@ def probe_public(timeout: float = PROBE_TIMEOUT) -> tuple[bool | None, str]:
|
||||
|
||||
|
||||
def diagnose(errori_osservati: list[str] | None = None,
|
||||
now: datetime | None = None,
|
||||
sonda=None) -> Diagnosi:
|
||||
"""Classifica un guasto gia' avvenuto. `sonda` e' iniettabile: i test non toccano la rete.
|
||||
|
||||
@@ -169,7 +177,10 @@ def diagnose(errori_osservati: list[str] | None = None,
|
||||
verdetto = V_VENUE_GIU
|
||||
perche = "l'API pubblica Deribit non serve, e non dichiara manutenzione"
|
||||
|
||||
atteso = verdetto == V_MANUTENZIONE and in_release_window(now)
|
||||
# #35 B11: qui c'era `verdetto == V_MANUTENZIONE and in_release_window(now)`. L'unico
|
||||
# chiamante (book_execute.py, cron :47) non cade mai nella finestra 09:00-09:30 di martedi':
|
||||
# il ramo non poteva mai diventare vero. Il campo resta (formato del report), sempre False.
|
||||
atteso = False
|
||||
prova = "; ".join([*errori[:3], grezzo])[:500]
|
||||
return Diagnosi(verdetto=verdetto, perche=perche, atteso=atteso, prova=prova)
|
||||
|
||||
|
||||
@@ -400,8 +400,14 @@ def deribit_book_sleeves() -> list[Sleeve]:
|
||||
e VRP01 (opzioni modellate). FULL Sharpe ~1.78 / HOLD ~1.17 / DD ~9% (research; SKH01 daily-step).
|
||||
Pensato per il deploy reale a basso capitale: stesso conto, stesso feed, ribilanciamento
|
||||
periodico (vedi src.portfolio.portfolio.rebalance_sim + scripts/portfolio/run_deribit_book.py).
|
||||
TP01 e' gia' armato live; SKH01 e' il candidato 2a gamba (da validare codice d'esecuzione)."""
|
||||
TP01 e' gia' armato live; SKH01 e' il candidato 2a gamba (da validare codice d'esecuzione).
|
||||
|
||||
I pesi si DERIVANO da src.live.book (P1): due letterali qui erano una seconda dichiarazione,
|
||||
libera di divergere dal book vero (#35 B12). Import locale: book importa questo modulo
|
||||
(_skyhook_positions) a livello di modulo -> un import in testa qui creerebbe un ciclo (come
|
||||
fa usde.py con book)."""
|
||||
from src.live import book as B # lazy: vedi nota sul ciclo qui sopra
|
||||
return [
|
||||
tp01_sleeve(weight=0.75),
|
||||
skyhook_sleeve(weight=0.25),
|
||||
tp01_sleeve(weight=B.W_TP01),
|
||||
skyhook_sleeve(weight=B.W_SKH),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user