Compare commits
4 Commits
1a46abd5cf
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a80c17f2bc | |||
| 097e764f6a | |||
| 8a1fb823dc | |||
| 4d7b02b6cb |
+26
-12
@@ -100,13 +100,20 @@ Risultato:
|
|||||||
Si seleziona lo strike short più vicino a delta target, in valore
|
Si seleziona lo strike short più vicino a delta target, in valore
|
||||||
assoluto.
|
assoluto.
|
||||||
|
|
||||||
| Parametro | Valore |
|
| Parametro | Valore (profilo B, v1.7.0) |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Delta short target | 0.12 |
|
| Delta short target | 0.18 |
|
||||||
| Tolleranza delta | [0.10, 0.15] |
|
| Tolleranza delta | [0.12, 0.22] |
|
||||||
| Distanza minima OTM | 15% (anche se delta è dentro tolleranza) |
|
| Distanza minima OTM | 10% (anche se delta è dentro tolleranza) |
|
||||||
| Distanza massima OTM | 25% |
|
| Distanza massima OTM | 25% |
|
||||||
|
|
||||||
|
> **Perché delta 0.18 e non 0.12.** A delta 0.10-0.15 il miglior
|
||||||
|
> `credit/width` fisicamente ottenibile su ETH (analisi 3.689 snapshot,
|
||||||
|
> 1mag-8giu '26) è ~6%, sotto il minimo richiesto: 0 spread fattibili.
|
||||||
|
> Per uno spread verticale `credit/width ≲ delta_short`, quindi l'unica
|
||||||
|
> leva che produce credito vendibile è vendere più vicino. Trade-off:
|
||||||
|
> la prob. di assegnazione sale da ~12% a ~18-22%.
|
||||||
|
|
||||||
**Variante dinamica per regime DVOL (Phase 5, opt-in).** Il campo
|
**Variante dinamica per regime DVOL (Phase 5, opt-in).** Il campo
|
||||||
`short_strike.delta_by_dvol` (lista step-function) sostituisce il
|
`short_strike.delta_by_dvol` (lista step-function) sostituisce il
|
||||||
delta target singolo con bande ordinate per `dvol_under`: a DVOL
|
delta target singolo con bande ordinate per `dvol_under`: a DVOL
|
||||||
@@ -117,9 +124,9 @@ classico col delta target sopra. Esempio bande nel profilo
|
|||||||
|
|
||||||
| dvol_under | delta_target | delta_min | delta_max |
|
| dvol_under | delta_target | delta_min | delta_max |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 50 | 0.15 | 0.13 | 0.17 |
|
| 50 | 0.22 | 0.18 | 0.25 |
|
||||||
| 70 | 0.12 | 0.10 | 0.15 |
|
| 70 | 0.18 | 0.12 | 0.22 |
|
||||||
| 90 | 0.10 | 0.08 | 0.12 |
|
| 90 | 0.15 | 0.10 | 0.18 |
|
||||||
|
|
||||||
Se nessuno strike disponibile rientra in entrambe le tolleranze → no entry.
|
Se nessuno strike disponibile rientra in entrambe le tolleranze → no entry.
|
||||||
|
|
||||||
@@ -131,16 +138,23 @@ Si seleziona lo strike a distanza fissa dallo short:
|
|||||||
|---|---|
|
|---|---|
|
||||||
| Larghezza spread (in % di spot) | 4% |
|
| Larghezza spread (in % di spot) | 4% |
|
||||||
| Larghezza minima accettabile | 3% |
|
| Larghezza minima accettabile | 3% |
|
||||||
| Larghezza massima accettabile | 5% |
|
| Larghezza massima accettabile | 6% |
|
||||||
|
|
||||||
Se non esiste lo strike esatto a 4%, si prende il più vicino entro
|
Se non esiste lo strike esatto a 4%, si prende il più vicino entro
|
||||||
[3%, 5%]; altrimenti no entry.
|
[3%, 6%]; altrimenti no entry. La banda arriva a 6% perché la griglia
|
||||||
|
strike ETH sotto i $1500 è a 100pt (~6% a spot <$2k): con cap 5%
|
||||||
|
nessuna gamba long rientrava in banda.
|
||||||
|
|
||||||
### 3.4 Rapporto credito/larghezza
|
### 3.4 Rapporto credito/larghezza
|
||||||
|
|
||||||
Il credito netto incassato (mid-price del combo) deve essere ≥ **30%**
|
Il credito netto incassato (mid-price del combo) deve essere ≥ **8%**
|
||||||
della larghezza dello spread. Sotto questa soglia il rapporto
|
della larghezza dello spread (profilo B). → altrimenti no entry.
|
||||||
rischio/rendimento è inferiore al minimo accettabile. → no entry.
|
|
||||||
|
> Era **30%**, ma è fisicamente irraggiungibile con delta short ≤0.22
|
||||||
|
> in questo mercato: il miglior `credit/width` misurato su ETH è ~6%
|
||||||
|
> (anche vendendo a delta 0.30 non si superava il 20%). La soglia 8% è
|
||||||
|
> allineata al realizzabile a delta ~0.18 e mantiene un floor sul
|
||||||
|
> rapporto rischio/rendimento.
|
||||||
|
|
||||||
## 4. Liquidità (filtro hard pre-entry)
|
## 4. Liquidità (filtro hard pre-entry)
|
||||||
|
|
||||||
|
|||||||
+35
-12
@@ -229,12 +229,23 @@ NULL = engine attivo.
|
|||||||
|
|
||||||
### `option_chain_snapshots`
|
### `option_chain_snapshots`
|
||||||
|
|
||||||
Snapshot della catena opzioni Deribit prelevata in continuo
|
Snapshot della catena opzioni Deribit. La tabella è alimentata da
|
||||||
(cron `*/15 * * * *`, allineato a `market_snapshots`). Crypto è
|
**due collettori distinti**, separati dalla colonna `source`:
|
||||||
24/7: l'accumulo dataset deve essere continuo, non gateato sulla
|
|
||||||
settimana. Ogni tick contiene un quote per strumento entro la
|
* `source='live'` — collettore operativo (cron `*/15 * * * *`,
|
||||||
finestra `[dte_min, dte_max]` di config; tutti i quote prelevati
|
allineato a `market_snapshots`). Ogni tick contiene un quote per
|
||||||
nello stesso tick condividono ``timestamp``. Migration `0005`.
|
strumento entro la finestra `[dte_min, dte_max]` di config (1
|
||||||
|
scadenza, ~13-15 strike); `book_depth_top3` NULL.
|
||||||
|
* `source='research'` — collettore full-chain (§13-bis, cron orario,
|
||||||
|
opt-in via `research_collector.enabled`). Cattura tutte le scadenze
|
||||||
|
≤`expiry_max_days` ed entrambe le ali, con `book_depth_top3`
|
||||||
|
popolato. Trasforma il dataset da "skew/premi medi" a backtest
|
||||||
|
opzioni vero (per-trade e standing put).
|
||||||
|
|
||||||
|
Crypto è 24/7: l'accumulo dataset deve essere continuo, non gateato
|
||||||
|
sulla settimana. Tutti i quote prelevati nello stesso tick
|
||||||
|
condividono ``timestamp``. Migration `0005` (tabella), `0007`
|
||||||
|
(colonna `source`).
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
CREATE TABLE option_chain_snapshots (
|
CREATE TABLE option_chain_snapshots (
|
||||||
@@ -255,15 +266,19 @@ CREATE TABLE option_chain_snapshots (
|
|||||||
open_interest INTEGER,
|
open_interest INTEGER,
|
||||||
volume_24h INTEGER,
|
volume_24h INTEGER,
|
||||||
book_depth_top3 INTEGER,
|
book_depth_top3 INTEGER,
|
||||||
|
source TEXT NOT NULL DEFAULT 'live',
|
||||||
PRIMARY KEY (timestamp, instrument_name)
|
PRIMARY KEY (timestamp, instrument_name)
|
||||||
) WITHOUT ROWID;
|
) WITHOUT ROWID;
|
||||||
```
|
```
|
||||||
|
|
||||||
Indici: `(asset, timestamp DESC)` per listing recenti, `(asset,
|
Indici: `(asset, timestamp DESC)` per listing recenti, `(asset,
|
||||||
expiry)` per query per scadenza specifica. ``book_depth_top3`` è
|
expiry)` per query per scadenza specifica, `(asset, source,
|
||||||
NULL by design — il collector non chiama l'order book per ogni
|
timestamp DESC)` per separare live/research nelle query di backtest.
|
||||||
strike per non saturare l'API; lo legge il liquidity gate live solo
|
``book_depth_top3`` è NULL sulle righe `live` (il collector operativo
|
||||||
sugli strike candidati al picker.
|
non chiama l'order book per ogni strike, per non saturare l'API; lo
|
||||||
|
legge il liquidity gate live solo sugli strike candidati al picker)
|
||||||
|
ed è **popolato sulle righe `research`** (1 call orderbook/strumento,
|
||||||
|
concorrenza limitata) per modellare lo slippage reale.
|
||||||
|
|
||||||
**Sblocca**: il backtest non-stilizzato (modulo `core/backtest.py`
|
**Sblocca**: il backtest non-stilizzato (modulo `core/backtest.py`
|
||||||
con prezzi reali invece di Black-Scholes), la calibrazione empirica
|
con prezzi reali invece di Black-Scholes), la calibrazione empirica
|
||||||
@@ -363,12 +378,20 @@ idempotente, forward-only:
|
|||||||
| 3 | `0003_market_snapshots.sql` | tabella `market_snapshots` |
|
| 3 | `0003_market_snapshots.sql` | tabella `market_snapshots` |
|
||||||
| 4 | `0004_auto_pause.sql` | `system_state.auto_pause_until / _reason` |
|
| 4 | `0004_auto_pause.sql` | `system_state.auto_pause_until / _reason` |
|
||||||
| 5 | `0005_option_chain_snapshots.sql` | tabella `option_chain_snapshots` |
|
| 5 | `0005_option_chain_snapshots.sql` | tabella `option_chain_snapshots` |
|
||||||
|
| 6 | `0006_dvol_history_multi_asset.sql` | `dvol_history` multi-asset |
|
||||||
|
| 7 | `0007_option_chain_source.sql` | colonna `source` (live/research) + indice |
|
||||||
|
|
||||||
## Backup
|
## Backup
|
||||||
|
|
||||||
Backup orari di `state.sqlite` in
|
Backup **giornalieri** di `state.sqlite` in
|
||||||
`data/backups/state-YYYYMMDD-HH.sqlite`, ritenuti per 30 giorni.
|
`data/backups/state-YYYYMMDD-HH.sqlite`, ritenuti per 30 giorni.
|
||||||
Operazione idempotente con SQLite `VACUUM INTO`. Lo job è registrato
|
Operazione idempotente con SQLite `VACUUM INTO`. Lo job è registrato
|
||||||
nello scheduler dell'orchestrator (`backup_cron = "0 * * * *"`); è
|
nello scheduler dell'orchestrator (`backup_cron = "0 0 * * *"`); è
|
||||||
disponibile anche come CLI `python scripts/backup.py` per backup
|
disponibile anche come CLI `python scripts/backup.py` per backup
|
||||||
ad-hoc.
|
ad-hoc.
|
||||||
|
|
||||||
|
> Nota storica: il cron era orario (`0 * * * *`) e accumulava ~16 GB
|
||||||
|
> di snapshot full per ~98 MB di dato unico (la stessa catena opzioni
|
||||||
|
> ricopiata ~24×/giorno). Portato a giornaliero per ridurre il
|
||||||
|
> footprint di ~24× a parità di retention — rilevante prima di
|
||||||
|
> accendere il collettore research, che 2-4× la crescita del DB.
|
||||||
|
|||||||
@@ -234,6 +234,8 @@ proposed
|
|||||||
| `0 12 1 * *` | Kelly recalibration | Mensile |
|
| `0 12 1 * *` | Kelly recalibration | Mensile |
|
||||||
| `*/5 * * * *` | Health check | 5 min |
|
| `*/5 * * * *` | Health check | 5 min |
|
||||||
| `*/15 * * * *` | Market snapshot (calibrazione soglie) | 15 min |
|
| `*/15 * * * *` | Market snapshot (calibrazione soglie) | 15 min |
|
||||||
|
| `*/15 * * * *` | Option chain snapshot (live, finestra DTE strategia) | 15 min |
|
||||||
|
| `0 * * * *` | Option chain research (full-chain + book_depth, opt-in) | Orario |
|
||||||
| `0 0 * * *` | Backup SQLite + rotation log | Giornaliero |
|
| `0 0 * * *` | Backup SQLite + rotation log | Giornaliero |
|
||||||
| `0 8 * * *` | Daily digest Telegram | Giornaliero |
|
| `0 8 * * *` | Daily digest Telegram | Giornaliero |
|
||||||
|
|
||||||
|
|||||||
+30
-6
@@ -55,19 +55,43 @@ structure:
|
|||||||
dte_min: 14
|
dte_min: 14
|
||||||
dte_max: 21
|
dte_max: 21
|
||||||
|
|
||||||
|
# Profilo B (v1.7.0). L'analisi su 3.689 snapshot (1mag-8giu '26) ha
|
||||||
|
# mostrato che a delta 0.10-0.15 il miglior credit/width fisicamente
|
||||||
|
# ottenibile è ~6%, quindi il vecchio credit_to_width_ratio_min=0.30
|
||||||
|
# era irraggiungibile (0 spread fattibili). La sola leva efficace è
|
||||||
|
# vendere più vicino → delta alzato a ~0.18.
|
||||||
short_strike:
|
short_strike:
|
||||||
delta_target: 0.12
|
delta_target: 0.18
|
||||||
delta_min: 0.10
|
delta_min: 0.12
|
||||||
delta_max: 0.15
|
delta_max: 0.22
|
||||||
distance_otm_pct_min: 0.15
|
distance_otm_pct_min: 0.10
|
||||||
distance_otm_pct_max: 0.25
|
distance_otm_pct_max: 0.25
|
||||||
|
|
||||||
spread_width:
|
spread_width:
|
||||||
target_pct_of_spot: 0.04
|
target_pct_of_spot: 0.04
|
||||||
min_pct_of_spot: 0.03
|
min_pct_of_spot: 0.03
|
||||||
max_pct_of_spot: 0.05
|
# 0.06: la griglia strike ETH sotto i $1500 è a 100pt (~6% a spot
|
||||||
|
# <$2k); con cap 0.05 nessuna gamba long rientrava in banda.
|
||||||
|
max_pct_of_spot: 0.06
|
||||||
|
|
||||||
credit_to_width_ratio_min: 0.30
|
credit_to_width_ratio_min: 0.08 # era 0.30 (irraggiungibile a delta ≤0.22)
|
||||||
|
|
||||||
|
# === RESEARCH COLLECTOR (§13-bis) ===
|
||||||
|
|
||||||
|
# Collettore opzioni full-chain, indipendente dal live. Cattura tutte
|
||||||
|
# le scadenze ≤ expiry_max_days ed entrambe le ali, popolando
|
||||||
|
# book_depth_top3 (1 call orderbook/strumento) → backtest opzioni
|
||||||
|
# per-trade e standing. Disabilitato di default: costo API non
|
||||||
|
# trascurabile. Scrive righe con source='research'.
|
||||||
|
research_collector:
|
||||||
|
enabled: false
|
||||||
|
cron: "0 * * * *" # orario, indipendente dal */15 del live
|
||||||
|
expiry_max_days: 95 # 1g..3mesi
|
||||||
|
moneyness_band_pct: null # null = catena completa (entrambe le ali)
|
||||||
|
open_interest_min: 100
|
||||||
|
fetch_book_depth: true
|
||||||
|
book_depth_concurrency: 8 # concorrenza max delle call orderbook
|
||||||
|
assets: ["ETH", "BTC"]
|
||||||
|
|
||||||
# === LIQUIDITY GATE ===
|
# === LIQUIDITY GATE ===
|
||||||
|
|
||||||
|
|||||||
@@ -1204,6 +1204,70 @@ def option_chain_trigger(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@option_chain.command(name="research-trigger")
|
||||||
|
@click.option(
|
||||||
|
"--strategy",
|
||||||
|
"strategy_path",
|
||||||
|
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
||||||
|
default=_DEFAULT_STRATEGY_PATH,
|
||||||
|
show_default=True,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--db",
|
||||||
|
"db_path",
|
||||||
|
type=click.Path(dir_okay=False, path_type=Path),
|
||||||
|
default=_DEFAULT_DB_PATH,
|
||||||
|
show_default=True,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--audit",
|
||||||
|
"audit_path",
|
||||||
|
type=click.Path(dir_okay=False, path_type=Path),
|
||||||
|
default=_DEFAULT_AUDIT_PATH,
|
||||||
|
show_default=True,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--token",
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
help="MCP bearer token (override su CERBERO_BITE_MCP_TOKEN).",
|
||||||
|
)
|
||||||
|
@click.option("--asset", default="ETH", show_default=True)
|
||||||
|
def option_chain_research_trigger(
|
||||||
|
strategy_path: Path,
|
||||||
|
db_path: Path,
|
||||||
|
audit_path: Path,
|
||||||
|
token: str | None,
|
||||||
|
asset: str,
|
||||||
|
) -> None:
|
||||||
|
"""Esegue UNA volta il collector research full-chain (source='research').
|
||||||
|
|
||||||
|
Cattura tutte le scadenze ed entrambe le ali con book_depth_top3
|
||||||
|
popolato. ``force=True``: gira anche se ``research_collector.enabled``
|
||||||
|
è false, utile per testare prima di accendere il cron orario.
|
||||||
|
"""
|
||||||
|
from cerbero_bite.runtime.dependencies import build_runtime # noqa: PLC0415
|
||||||
|
from cerbero_bite.runtime.option_chain_research_cycle import ( # noqa: PLC0415
|
||||||
|
collect_option_chain_research,
|
||||||
|
)
|
||||||
|
|
||||||
|
cfg = load_strategy(strategy_path).config
|
||||||
|
ctx = build_runtime(
|
||||||
|
cfg=cfg,
|
||||||
|
endpoints=load_endpoints(),
|
||||||
|
token=load_token(value=token),
|
||||||
|
db_path=db_path,
|
||||||
|
audit_path=audit_path,
|
||||||
|
bot_tag=load_bot_tag(),
|
||||||
|
)
|
||||||
|
n = asyncio.run(collect_option_chain_research(ctx, asset=asset, force=True))
|
||||||
|
console.print(
|
||||||
|
f"[green]Persisted {n} research quote(s) for {asset}[/green]"
|
||||||
|
if n > 0
|
||||||
|
else "[yellow]No quotes persisted (chain empty or fetch failed)[/yellow]"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@option_chain.command(name="analyze")
|
@option_chain.command(name="analyze")
|
||||||
@click.option(
|
@click.option(
|
||||||
"--strategy",
|
"--strategy",
|
||||||
|
|||||||
@@ -70,12 +70,16 @@ async def collect_option_chain_research(
|
|||||||
asset: str = "ETH",
|
asset: str = "ETH",
|
||||||
now: datetime | None = None,
|
now: datetime | None = None,
|
||||||
batch_size: int = DEFAULT_BATCH_SIZE,
|
batch_size: int = DEFAULT_BATCH_SIZE,
|
||||||
|
force: bool = False,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Collect + persist un singolo snapshot full-chain ``research`` per
|
"""Collect + persist un singolo snapshot full-chain ``research`` per
|
||||||
``asset``. Ritorna il numero di quote persistiti (0 su fallimento
|
``asset``. Ritorna il numero di quote persistiti (0 su fallimento
|
||||||
best-effort o se il collettore è disabilitato)."""
|
best-effort o se il collettore è disabilitato).
|
||||||
|
|
||||||
|
``force=True`` bypassa il gate ``enabled`` (usato dal trigger CLI per
|
||||||
|
testare la raccolta prima di accendere il cron schedulato)."""
|
||||||
rc = getattr(ctx.cfg, "research_collector", None)
|
rc = getattr(ctx.cfg, "research_collector", None)
|
||||||
if rc is None or not rc.enabled:
|
if rc is None or (not rc.enabled and not force):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
when = (now or datetime.now(UTC)).astimezone(UTC)
|
when = (now or datetime.now(UTC)).astimezone(UTC)
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ Environment = Literal["testnet", "mainnet"]
|
|||||||
_CRON_ENTRY = "0 */2 * * *" # crypto 24/7: valutazione ogni 2h; i gate decidono se entrare
|
_CRON_ENTRY = "0 */2 * * *" # crypto 24/7: valutazione ogni 2h; i gate decidono se entrare
|
||||||
_CRON_MONITOR = "0 * * * *" # stop/take-profit check ogni ora
|
_CRON_MONITOR = "0 * * * *" # stop/take-profit check ogni ora
|
||||||
_CRON_HEALTH = "*/5 * * * *"
|
_CRON_HEALTH = "*/5 * * * *"
|
||||||
_CRON_BACKUP = "0 * * * *"
|
_CRON_BACKUP = "0 0 * * *" # giornaliero (00:00 UTC): lo snapshot full orario gonfiava i backup (16GB per ~98MB di dato unico)
|
||||||
_CRON_MANUAL_ACTIONS = "*/1 * * * *"
|
_CRON_MANUAL_ACTIONS = "*/1 * * * *"
|
||||||
_CRON_MARKET_SNAPSHOT = "*/15 * * * *"
|
_CRON_MARKET_SNAPSHOT = "*/15 * * * *"
|
||||||
_CRON_OPTION_CHAIN_SNAPSHOT = "*/15 * * * *" # crypto è 24/7: cadenza continua allineata a market_snapshot
|
_CRON_OPTION_CHAIN_SNAPSHOT = "*/15 * * * *" # crypto è 24/7: cadenza continua allineata a market_snapshot
|
||||||
|
|||||||
+3
-3
@@ -6,8 +6,8 @@
|
|||||||
# config hash), and lands as a separate commit with the motivation in
|
# config hash), and lands as a separate commit with the motivation in
|
||||||
# the commit message.
|
# the commit message.
|
||||||
|
|
||||||
config_version: "1.7.0"
|
config_version: "1.7.1"
|
||||||
config_hash: "1171380de6d3334be1f6eed04797cebe15e5b8ec2124e130b582c2e2097bde37"
|
config_hash: "720a202e9dde4ccff7f79a8bc9521de62dc30115f11f486c55ad6f3986858dcb"
|
||||||
last_review: "2026-06-09"
|
last_review: "2026-06-09"
|
||||||
last_reviewer: "Adriano"
|
last_reviewer: "Adriano"
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ entry:
|
|||||||
# ali, popolando book_depth_top3 → backtest opzioni per-trade e
|
# ali, popolando book_depth_top3 → backtest opzioni per-trade e
|
||||||
# standing put + slippage reale. Disabilitato di default (costo API).
|
# standing put + slippage reale. Disabilitato di default (costo API).
|
||||||
research_collector:
|
research_collector:
|
||||||
enabled: false
|
enabled: true
|
||||||
cron: "0 * * * *" # orario
|
cron: "0 * * * *" # orario
|
||||||
expiry_max_days: 95 # 1g..3mesi
|
expiry_max_days: 95 # 1g..3mesi
|
||||||
moneyness_band_pct: null # null = catena completa (entrambe le ali)
|
moneyness_band_pct: null # null = catena completa (entrambe le ali)
|
||||||
|
|||||||
Reference in New Issue
Block a user