feat(strategy): abbandono gating settimanale — entry daily 24/7

Crypto opera 24/7: la cadenza settimanale lunedì-only era un retaggio
TradFi senza giustificazione. La nuova cadenza è giornaliera (cron
0 14 * * *), con i gate quantitativi a decidere se entrare o saltare.

Cambiamenti principali:

* runtime/orchestrator.py — _CRON_ENTRY 0 14 * * * (era MON)
* runtime/auto_pause.py — pause_until(days=) (era weeks=); minimo
  clamp 1 giorno (era 1 settimana)
* core/backtest.py — MondayPick→DailyPick, monday_picks→daily_picks
  (1 pick per calendar-day all'ora target); Sharpe annualization su
  ~120 trade/anno (era 52)
* config/schema.py — default cron daily; max_concurrent_positions 1→5;
  AutoPauseConfig.pause_weeks→pause_days, default 14
* runtime/option_chain_snapshot_cycle.py + orchestrator — cron */15
  per accumulo continuo dataset di backtest empirico

Strategy yamls (config_version 1.3.0 → 1.4.0, hash rigenerati):

* strategy.yaml — max_concurrent 1→5, cap_aggregate coerente
* strategy.aggressiva.yaml — max_concurrent 2→8, cap_aggregate
  3200→6400, max_contracts_per_trade invariato a 16
* strategy.conservativa.yaml — max_concurrent 1→3
* tutti — pause_weeks→pause_days: 14

GUI (pages/7_📚_Strategia.py):

* slider Trade/anno: range 20-200 (era 8-30), default 110, help
  riallineato sulla math 365 candidature × pass-rate 30-40%
* card profili: versione letta dinamicamente da config_version invece
  che hard-coded "v1.2.0"
* warning "entrambi perdono soldi" ora valuta i P/L effettivi
  (cons['annual_pl'], aggr['annual_pl']) invece del win_rate grezzo;
  aggiunto stato intermedio quando solo conservativo è in perdita

Tests (450/450 passati):

* test_auto_pause: pause_days, clamp ≥1 giorno
* test_backtest: rinomina + ridisegno daily picks (assert su
  calendar-day dedupe e hour filter)
* test_sizing_engine: other_open_positions=5 per cap default
* test_config_loader: version 1.4.0

Docs (README + 9 file in docs/) — tutti i riferimenti weekly/lunedì
allineati a daily/24-7, volume option_chain ricalcolato per cron
*/15 (~1.1 MB/giorno, ~400 MB/anno).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-05-03 16:21:16 +00:00
parent dabcc8d15b
commit 6ff021fbf4
26 changed files with 216 additions and 181 deletions
+5 -2
View File
@@ -15,7 +15,10 @@ attiva, sizing Quarter Kelly e disciplina di uscita rigida.
- **Gestione attiva:** profit take 50% credito, stop loss 1.5× credito, - **Gestione attiva:** profit take 50% credito, stop loss 1.5× credito,
vol stop +10 punti DVOL, time stop 7 DTE, exit su short strike testato vol stop +10 punti DVOL, time stop 7 DTE, exit su short strike testato
(|delta| ≥ 0.30). Su ETH **non si difende rollando**: si esce. (|delta| ≥ 0.30). Su ETH **non si difende rollando**: si esce.
- **Frequenza:** apertura ogni 7 giorni, una posizione alla volta. - **Frequenza:** candidatura giornaliera (cron `0 14 * * *`, crypto è 24/7),
fino a **5 posizioni concorrenti** sul profilo principale (3 sul
conservativo, 8 sull'aggressivo). I gate quantitativi decidono se entrare;
nei giorni in cui falliscono, niente trade.
Il sistema è **deterministico**: nessun LLM partecipa al decision loop. Il sistema è **deterministico**: nessun LLM partecipa al decision loop.
Le regole sono codificate, le soglie sono parametri di configurazione, Le regole sono codificate, le soglie sono parametri di configurazione,
@@ -48,7 +51,7 @@ leggere in ordine per chi implementa.
| `docs/03-algorithms.md` | Specifiche dettagliate dei sette algoritmi core | | `docs/03-algorithms.md` | Specifiche dettagliate dei sette algoritmi core |
| `docs/04-mcp-integration.md` | Mappa dei tool MCP usati e contratti | | `docs/04-mcp-integration.md` | Mappa dei tool MCP usati e contratti |
| `docs/05-data-model.md` | Schema persistenza posizioni, log, KB | | `docs/05-data-model.md` | Schema persistenza posizioni, log, KB |
| `docs/06-operational-flow.md` | Flussi operativi: avvio, settimanale, monitoring | | `docs/06-operational-flow.md` | Flussi operativi: avvio, entry daily, monitoring |
| `docs/07-risk-controls.md` | Kill switch, cap, dead-man, audit | | `docs/07-risk-controls.md` | Kill switch, cap, dead-man, audit |
| `docs/08-testing-validation.md` | TDD, paper trading, golden tests | | `docs/08-testing-validation.md` | TDD, paper trading, golden tests |
| `docs/09-development-roadmap.md` | Fasi di sviluppo e milestone | | `docs/09-development-roadmap.md` | Fasi di sviluppo e milestone |
+1 -1
View File
@@ -36,7 +36,7 @@ per imparare, ma **non sta nel loop di esecuzione**.
### Cosa fa Cerbero Bite ### Cosa fa Cerbero Bite
1. Legge dati di mercato dagli MCP (Deribit, Hyperliquid, sentiment, macro). 1. Legge dati di mercato dagli MCP (Deribit, Hyperliquid, sentiment, macro).
2. Valuta condizioni di entrata su finestra temporale fissa (settimanale). 2. Valuta condizioni di entrata su finestra temporale fissa (giornaliera, 14:00 UTC; crypto è 24/7).
3. Calcola la struttura ottimale dello spread secondo le regole. 3. Calcola la struttura ottimale dello spread secondo le regole.
4. Verifica liquidità, cap di rischio, calendar macro. 4. Verifica liquidità, cap di rischio, calendar macro.
5. Calcola sizing in contratti. 5. Calcola sizing in contratti.
+11 -9
View File
@@ -19,10 +19,12 @@ Sorgente teorica: `Cerbero_Office/NewStrategy/strategia-credit-spread-eth.md`
## 2. Trigger di apertura (entry) ## 2. Trigger di apertura (entry)
Il rule engine valuta l'apertura di un nuovo trade **una sola volta al Il rule engine valuta l'apertura di un nuovo trade **una volta al
giorno**, alle **14:00 UTC** del lunedì (orario UE pomeridiano stabile, giorno**, alle **14:00 UTC** (orario UE pomeridiano stabile, fuori
fuori dai picchi di funding statunitensi). Se il lunedì è festività dai picchi di funding statunitensi). Crypto è 24/7: non c'è un "giorno
italiana, l'engine ignora la regola e attende il lunedì successivo. buono" intrinseco, sono i gate quantitativi a decidere se entrare o
saltare. Se il giorno è festività italiana e `skip_holidays_country`
è attivo, l'engine attende il giorno successivo.
Una nuova posizione viene aperta **solo se tutte** le seguenti condizioni Una nuova posizione viene aperta **solo se tutte** le seguenti condizioni
sono vere: sono vere:
@@ -70,7 +72,7 @@ Razionale: il selling vol nudo è strutturalmente neutro a win-rate
per il razionale completo. per il razionale completo.
Se anche **una sola** condizione fallisce → **no entry**, log con motivo, Se anche **una sola** condizione fallisce → **no entry**, log con motivo,
ritento la settimana successiva. ritento il giorno successivo.
## 3. Selezione struttura ## 3. Selezione struttura
@@ -256,13 +258,13 @@ ogni entry-cycle:
> Se `auto_pause.enabled` e P/L cumulato delle ultime > Se `auto_pause.enabled` e P/L cumulato delle ultime
> `lookback_trades` posizioni chiuse < `max_drawdown_pct × > `lookback_trades` posizioni chiuse < `max_drawdown_pct ×
> capitale_corrente`, l'engine si auto-mette in pausa per > capitale_corrente`, l'engine si auto-mette in pausa per
> `pause_weeks` settimane (skip-week mode). > `pause_days` giorni (skip-day mode).
Difende dai regime change non rilevati dai filtri quant. La pausa Difende dai regime change non rilevati dai filtri quant. La pausa
si annulla automaticamente alla scadenza, oppure manualmente con si annulla automaticamente alla scadenza, oppure manualmente con
`UPDATE system_state SET auto_pause_until = NULL`. Default `UPDATE system_state SET auto_pause_until = NULL`. Default
disabilitato; profilo aggressivo: lookback 5 trade, soglia 15%, 2 disabilitato; profilo aggressivo: lookback 5 trade, soglia 15%, 14
settimane di pausa. giorni di pausa.
## 8. Esecuzione di apertura ## 8. Esecuzione di apertura
@@ -296,7 +298,7 @@ settimane di pausa.
durante un trade aperto). durante un trade aperto).
- Non aggiusta strike o size dopo l'apertura. - Non aggiusta strike o size dopo l'apertura.
- Non apre nuovi trade per "compensare" perdite recenti. - Non apre nuovi trade per "compensare" perdite recenti.
- Non opera fuori dalla finestra del lunedì 14:00 UTC, eccetto chiusure. - Non opera fuori dalla finestra delle 14:00 UTC, eccetto chiusure.
- Non deroga ai cap nemmeno per "opportunità eccezionali". - Non deroga ai cap nemmeno per "opportunità eccezionali".
- Non si aggiorna automaticamente: nuovo set di regole = nuovo deploy - Non si aggiorna automaticamente: nuovo set di regole = nuovo deploy
con review esplicita. con review esplicita.
+1 -1
View File
@@ -122,7 +122,7 @@ Cerbero_Bite/
│ │ ├── lockfile.py # fcntl.flock single-instance │ │ ├── lockfile.py # fcntl.flock single-instance
│ │ ├── alert_manager.py # severity routing │ │ ├── alert_manager.py # severity routing
│ │ ├── health_check.py # ping + 3-strikes kill switch │ │ ├── health_check.py # ping + 3-strikes kill switch
│ │ ├── entry_cycle.py # weekly entry auto-execute │ │ ├── entry_cycle.py # daily entry auto-execute (crypto 24/7)
│ │ ├── monitor_cycle.py # 12h exit auto-execute │ │ ├── monitor_cycle.py # 12h exit auto-execute
│ │ └── recovery.py # state reconcile al boot │ │ └── recovery.py # state reconcile al boot
│ ├── state/ # persistenza │ ├── state/ # persistenza
+10 -7
View File
@@ -220,11 +220,12 @@ NULL = engine attivo.
### `option_chain_snapshots` ### `option_chain_snapshots`
Snapshot della catena opzioni Deribit prelevata settimanalmente Snapshot della catena opzioni Deribit prelevata in continuo
(cron `55 13 * * MON`, 5 minuti prima del trigger entry). Ogni (cron `*/15 * * * *`, allineato a `market_snapshots`). Crypto è
tick contiene un quote per strumento entro la finestra 24/7: l'accumulo dataset deve essere continuo, non gateato sulla
`[dte_min, dte_max]` di config; tutti i quote prelevati nello stesso settimana. Ogni tick contiene un quote per strumento entro la
tick condividono ``timestamp``. Migration `0005`. finestra `[dte_min, dte_max]` di config; tutti i quote prelevati
nello stesso tick condividono ``timestamp``. Migration `0005`.
```sql ```sql
CREATE TABLE option_chain_snapshots ( CREATE TABLE option_chain_snapshots (
@@ -259,8 +260,10 @@ sugli strike candidati al picker.
con prezzi reali invece di Black-Scholes), la calibrazione empirica con prezzi reali invece di Black-Scholes), la calibrazione empirica
dello skew premium, la validazione ex-post dello strike picker. dello skew premium, la validazione ex-post dello strike picker.
Volume atteso: ~50 strike × 3 scadenze × 1 snapshot/settimana × Volume atteso (cron `*/15 * * * *`, ~96 snapshot/giorno):
17 colonne ≈ 12 KB/settimana, ~600 KB/anno. ~50 strike × 3 scadenze × 96 snap/giorno × 17 colonne ≈ ~1.1 MB/giorno,
~400 MB/anno. Considerare politiche di retention (archive trimestrale
in parquet) se il bot gira a lungo.
## Log file ## Log file
+6 -4
View File
@@ -27,9 +27,11 @@ L'avvio è progettato per essere **safe**: se qualcosa non torna, il
sistema si rifiuta di operare. Mai partire con uno stato dubbio o un sistema si rifiuta di operare. Mai partire con uno stato dubbio o un
ambiente diverso da quello atteso. ambiente diverso da quello atteso.
## Flusso 2 — Settimanale (entry) ## Flusso 2 — Daily (entry)
Trigger: cron `0 14 * * MON` (lunedì 14:00 UTC). Trigger: cron `0 14 * * *` (ogni giorno 14:00 UTC). Crypto è 24/7:
la cadenza di candidatura non è gateata sulla settimana — sono i gate
quantitativi a decidere se entrare o saltare il giorno.
``` ```
START START
@@ -219,7 +221,7 @@ proposed
| Cron | Trigger | Frequenza | | Cron | Trigger | Frequenza |
|---|---|---| |---|---|---|
| `0 14 * * MON` | Entry evaluation | Settimanale | | `0 14 * * *` | Entry evaluation | Giornaliera |
| `0 2,14 * * *` | Position monitoring | 2× giorno | | `0 2,14 * * *` | Position monitoring | 2× giorno |
| `0 12 1 * *` | Kelly recalibration | Mensile | | `0 12 1 * *` | Kelly recalibration | Mensile |
| `*/5 * * * *` | Health check | 5 min | | `*/5 * * * *` | Health check | 5 min |
@@ -237,7 +239,7 @@ Il bot riconosce due interruttori indipendenti, letti da
| Variabile d'ambiente | Default | Cosa abilita | | Variabile d'ambiente | Default | Cosa abilita |
|---|---|---| |---|---|---|
| `CERBERO_BITE_ENABLE_DATA_ANALYSIS` | `true` | Job `market_snapshot` ogni 15 min: raccolta dati MCP, scrittura tabella `market_snapshots`, calibrazione soglie. | | `CERBERO_BITE_ENABLE_DATA_ANALYSIS` | `true` | Job `market_snapshot` ogni 15 min: raccolta dati MCP, scrittura tabella `market_snapshots`, calibrazione soglie. |
| `CERBERO_BITE_ENABLE_STRATEGY` | `false` | Job `entry` (lunedì 14:00 UTC) e `monitor` (2× giorno): valutazione regole §2-§9 di `01-strategy-rules.md` e proposta/esecuzione ordini. | | `CERBERO_BITE_ENABLE_STRATEGY` | `false` | Job `entry` (daily 14:00 UTC) e `monitor` (2× giorno): valutazione regole §2-§9 di `01-strategy-rules.md` e proposta/esecuzione ordini. |
I job di infrastruttura (`health`, `backup`, `manual_actions`) sono I job di infrastruttura (`health`, `backup`, `manual_actions`) sono
**sempre attivi**, indipendentemente dai flag, perché tengono in vita il **sempre attivi**, indipendentemente dai flag, perché tengono in vita il
+1 -1
View File
@@ -54,7 +54,7 @@ cerbero-bite kill-switch disarm --reason "<motivo>" \
L'operazione è transazionale: SQLite `system_state.kill_switch = 0` + L'operazione è transazionale: SQLite `system_state.kill_switch = 0` +
una linea `KILL_SWITCH_DISARMED` nella audit chain con il motivo. Il una linea `KILL_SWITCH_DISARMED` nella audit chain con il motivo. Il
disarm non riavvia automaticamente lo scheduler; è il prossimo tick disarm non riavvia automaticamente lo scheduler; è il prossimo tick
naturale (entry settimanale o monitor 12h) a far ripartire la naturale (entry giornaliero o monitor 12h) a far ripartire la
decisione. decisione.
## Cap di rischio (oltre alle regole di strategia) ## Cap di rischio (oltre alle regole di strategia)
+5 -5
View File
@@ -157,9 +157,9 @@ chain:
| Test | Scenario | | Test | Scenario |
|---|---| |---|---|
| `test_weekly_open_happy_path` | Tutto OK → proposta inviata | | `test_daily_open_happy_path` | Tutto OK → proposta inviata |
| `test_weekly_open_no_strike_available` | Chain vuota nel range delta | | `test_daily_open_no_strike_available` | Chain vuota nel range delta |
| `test_weekly_open_macro_blocks` | FOMC entro 5 giorni | | `test_daily_open_macro_blocks` | FOMC entro 5 giorni |
| `test_monitor_profit_take` | Mark = 50% credito → close_profit | | `test_monitor_profit_take` | Mark = 50% credito → close_profit |
| `test_monitor_vol_stop` | DVOL +12 → close_vol | | `test_monitor_vol_stop` | DVOL +12 → close_vol |
| `test_recovery_after_crash_open_position` | Crash mid-fill, restart, riconcilia | | `test_recovery_after_crash_open_position` | Crash mid-fill, restart, riconcilia |
@@ -175,8 +175,8 @@ checked-in.
``` ```
tests/golden/ tests/golden/
├── 2026-04-27_weekly_open_bull_put.yaml # input snapshot ├── 2026-04-27_daily_open_bull_put.yaml # input snapshot
├── 2026-04-27_weekly_open_bull_put.golden # output atteso ├── 2026-04-27_daily_open_bull_put.golden # output atteso
└── runner.py └── runner.py
``` ```
+2 -2
View File
@@ -114,7 +114,7 @@ Tasks:
4. `runtime/alert_manager.py` — escalation policy 4. `runtime/alert_manager.py` — escalation policy
Test integration su scenari completi (vedi `08-testing-validation.md`): Test integration su scenari completi (vedi `08-testing-validation.md`):
- weekly open happy path - daily open happy path
- monitor profit take - monitor profit take
- monitor vol stop - monitor vol stop
- recovery dopo crash - recovery dopo crash
@@ -206,7 +206,7 @@ Setup:
Metriche da raccogliere: Metriche da raccogliere:
- Numero proposte settimanali emesse - Numero proposte giornaliere emesse
- Quante passano i filtri - Quante passano i filtri
- Win rate, avg P&L paper - Win rate, avg P&L paper
- Discrepanze tra mid stimato e fill reale (slippage) - Discrepanze tra mid stimato e fill reale (slippage)
+2 -2
View File
@@ -24,8 +24,8 @@ asset:
# === ENTRY === # === ENTRY ===
entry: entry:
# finestra di valutazione settimanale # finestra di valutazione giornaliera (crypto 24/7)
cron: "0 14 * * MON" # lunedì 14:00 UTC cron: "0 14 * * *" # ogni giorno 14:00 UTC
skip_holidays_country: "IT" skip_holidays_country: "IT"
# filtri di accesso (vedi 01-strategy-rules.md §2) # filtri di accesso (vedi 01-strategy-rules.md §2)
+24 -21
View File
@@ -12,7 +12,7 @@
## TL;DR ## TL;DR
Cerbero Bite vende **credit spread settimanali su ETH/Deribit** quando Cerbero Bite vende **credit spread su ETH/Deribit (DTE 14-21, valutati ogni giorno)** quando
la volatilità implicita è **abbastanza alta da pagare bene**, il la volatilità implicita è **abbastanza alta da pagare bene**, il
mercato non è in **stress di liquidazione**, non ci sono **eventi macro mercato non è in **stress di liquidazione**, non ci sono **eventi macro
forti** in finestra, e il bias direzionale è **chiaro** (bull o bear). forti** in finestra, e il bias direzionale è **chiaro** (bull o bear).
@@ -22,8 +22,9 @@ strategia.
Ogni 15 minuti raccoglie 1 riga per asset (ETH e BTC) nella tabella Ogni 15 minuti raccoglie 1 riga per asset (ETH e BTC) nella tabella
`market_snapshots`. Quei dati alimentano tre obiettivi distinti: `market_snapshots`. Quei dati alimentano tre obiettivi distinti:
1. **Decisione live** — l'entry ciclo del lunedì 14:00 UTC legge i 1. **Decisione live** — l'entry ciclo daily alle 14:00 UTC legge i
campi più freschi per dire "go/no-go". campi più freschi per dire "go/no-go" (crypto è 24/7: la cadenza
non è gateata sulla settimana, decidono i gate quantitativi).
2. **Monitoring continuo** — il decision loop di gestione attiva 2. **Monitoring continuo** — il decision loop di gestione attiva
confronta la situazione con quella all'apertura. confronta la situazione con quella all'apertura.
3. **Calibrazione** — la pagina `📐 Calibrazione` usa la distribuzione 3. **Calibrazione** — la pagina `📐 Calibrazione` usa la distribuzione
@@ -197,7 +198,7 @@ Quanto segue è la versione "leggibile" delle regole §2-§9 di
`01-strategy-rules.md`. Ogni passo cita i campi di `01-strategy-rules.md`. Ogni passo cita i campi di
`market_snapshots` che lo alimentano. `market_snapshots` che lo alimentano.
### Fase 1 — Trigger (lunedì 14:00 UTC, festività italiane escluse) ### Fase 1 — Trigger (daily 14:00 UTC, festività italiane escluse se `skip_holidays_country` è on)
``` ```
SE NESSUNA posizione aperta SE NESSUNA posizione aperta
@@ -211,7 +212,7 @@ SE NESSUNA posizione aperta
ALLORA ALLORA
procedi alla Fase 2 procedi alla Fase 2
ALTRIMENTI ALTRIMENTI
no entry, log motivo, ritento la settimana successiva no entry, log motivo, ritento il giorno successivo
``` ```
### Fase 2 — Bias e struttura ### Fase 2 — Bias e struttura
@@ -363,8 +364,8 @@ capitale **non aumenta** i contratti per trade.
### Frequenza realistica di entry ### Frequenza realistica di entry
La regola si valuta una volta a settimana, ma la maggioranza dei La regola si valuta **una volta al giorno** (crypto è 24/7), ma la
lunedì viene saltata per: maggioranza dei giorni viene saltata per:
| Motivo di skip | Frequenza tipica | | Motivo di skip | Frequenza tipica |
|---|---| |---|---|
@@ -372,11 +373,12 @@ lunedì viene saltata per:
| Bias non chiaro (trend × funding discordi o entrambi neutri senza IC) | 2535% | | Bias non chiaro (trend × funding discordi o entrambi neutri senza IC) | 2535% |
| Macro entro DTE | 1020% | | Macro entro DTE | 1020% |
| Funding o liquidation risk fuori soglia | 515% | | Funding o liquidation risk fuori soglia | 515% |
| Capitale o sizing insufficiente | 05% | | Capitale, sizing insufficiente o concurrency cap raggiunto | 515% |
**Risultato netto: 3050% delle settimane finisce in entry effettiva **Risultato netto: ~3040% dei giorni finisce in entry effettiva
⇒ 1525 trade / anno** (52 lunedì × 3050%). Le altre settimane il ⇒ 110145 trade / anno** (365 candidature × pass-rate, capped da
bot sta fermo. È il design. `max_concurrent_positions`). I restanti giorni il bot sta fermo:
è il design — la disciplina è la strategia.
### Win-rate atteso (short delta 0.12 + profit-take 50%) ### Win-rate atteso (short delta 0.12 + profit-take 50%)
@@ -450,11 +452,11 @@ In modalità data-only (oggi) il P/L atteso è **0** — l'engine
2. **Validare** i filtri quant osservando ex-post quanti tick 2. **Validare** i filtri quant osservando ex-post quanti tick
sarebbero stati filtrati (vedi pagina `📐 Calibrazione`, colonna sarebbero stati filtrati (vedi pagina `📐 Calibrazione`, colonna
"% bloccato dalla soglia"). "% bloccato dalla soglia").
3. **Misurare** la quota effettiva di lunedì che superano i filtri 3. **Misurare** la quota effettiva di giorni che superano i filtri
nel proprio regime, prima di committare capitale. nel proprio regime, prima di committare capitale.
> Suggerimento: 4 settimane di dati = 4 lunedì × probabilità entry = > Suggerimento: 30 giorni di dati = 30 candidature × probabilità entry
> 12 candidate entry effettive. **Aspettare almeno 8 settimane** > ≈ 912 candidate entry effettive. **Aspettare almeno 60 giorni**
> prima di tarare le soglie dà uno storico con dispersione > prima di tarare le soglie dà uno storico con dispersione
> sufficiente per decisioni non-rumorose. > sufficiente per decisioni non-rumorose.
@@ -560,8 +562,8 @@ step di calibrazione, vedi §4-quinquies in roadmap).
- **Conservativa / golden config**: `enabled=false, min=0`. Tutti i - **Conservativa / golden config**: `enabled=false, min=0`. Tutti i
setup passano questo gate, anche con IV-RV negativa. Motivo: nei setup passano questo gate, anche con IV-RV negativa. Motivo: nei
primi 8 turni di lunedì non si hanno abbastanza tick per stabilire primi 60 giorni non si hanno abbastanza tick per stabilire che
che soglia ha senso nel proprio regime. Lasciamo la pagina soglia ha senso nel proprio regime. Lasciamo la pagina
`📐 Calibrazione` mostrare la distribuzione e poi alziamo `📐 Calibrazione` mostrare la distribuzione e poi alziamo
manualmente. manualmente.
- **Aggressiva**: `enabled=true, min=3`. Il profilo aggressivo già di - **Aggressiva**: `enabled=true, min=3`. Il profilo aggressivo già di
@@ -599,7 +601,8 @@ questa logica: muovi da 0.72 a 0.78 e vedi l'APR scattare.
In aggiunta a `market_snapshots` (cron `*/15`), il bot raccoglie In aggiunta a `market_snapshots` (cron `*/15`), il bot raccoglie
ora una seconda fonte di dati: la **catena opzioni Deribit completa** ora una seconda fonte di dati: la **catena opzioni Deribit completa**
ogni lunedì alle 13:55 UTC (5 minuti prima del trigger entry). ogni 15 minuti (cron `*/15`, allineato a `market_snapshots` — crypto
è 24/7, l'accumulo dataset deve essere continuo).
Tabella `option_chain_snapshots` — vedi `05-data-model.md` per lo Tabella `option_chain_snapshots` — vedi `05-data-model.md` per lo
schema. Cosa registra per ogni strumento entro la finestra schema. Cosa registra per ogni strumento entro la finestra
@@ -628,7 +631,7 @@ schema. Cosa registra per ogni strumento entro la finestra
- `cerbero-bite option-chain trigger` — esegue UNA volta il - `cerbero-bite option-chain trigger` — esegue UNA volta il
collector senza aspettare il cron. Utile per test e per popolare collector senza aspettare il cron. Utile per test e per popolare
prima del primo lunedì utile. prima del primo tick utile.
- `cerbero-bite option-chain analyze [--bias bull_put|bear_call]` - `cerbero-bite option-chain analyze [--bias bull_put|bear_call]`
legge l'ultimo snapshot, simula il selector di strike con la legge l'ultimo snapshot, simula il selector di strike con la
strategy passata e stampa: short/long strike, delta, width, strategy passata e stampa: short/long strike, delta, width,
@@ -643,9 +646,9 @@ Tre euristiche operative sui campi raccolti:
1. **Premio "ricco":** `iv_minus_rv` consistentemente > 5 punti per 1. **Premio "ricco":** `iv_minus_rv` consistentemente > 5 punti per
N giorni → il regime sta pagando bene la vendita di vol. Sono i N giorni → il regime sta pagando bene la vendita di vol. Sono i
periodi in cui la strategia ha edge maggiore. periodi in cui la strategia ha edge maggiore.
2. **Premio "magro":** `dvol < 35` per più giorni → la finestra del 2. **Premio "magro":** `dvol < 35` per più giorni → la finestra
lunedì viene saltata. Non è un fallimento: è la disciplina che giornaliera viene saltata. Non è un fallimento: è la disciplina
funziona. che funziona.
3. **Stress imminente:** `liquidation_*_risk = high` o spike di 3. **Stress imminente:** `liquidation_*_risk = high` o spike di
`oi_delta_pct_4h` (> 5% in valore assoluto) + funding ai limiti `oi_delta_pct_4h` (> 5% in valore assoluto) + funding ai limiti
→ atteso vol stop / time stop attivi nei prossimi cicli, anche → atteso vol stop / time stop attivi nei prossimi cicli, anche
+4 -4
View File
@@ -789,7 +789,7 @@ def backtest(
table = Table(title=f"Backtest report — {strategy_path.name}") table = Table(title=f"Backtest report — {strategy_path.name}")
table.add_column("Metrica", style="cyan") table.add_column("Metrica", style="cyan")
table.add_column("Valore", style="bold") table.add_column("Valore", style="bold")
table.add_row("Picks (lunedì 14:00)", str(report.n_picks)) table.add_row("Picks (daily 14:00)", str(report.n_picks))
table.add_row( table.add_row(
"Accettati dai filtri", "Accettati dai filtri",
f"{report.n_accepted} ({report.n_accepted / max(1, report.n_picks):.0%})", f"{report.n_accepted} ({report.n_accepted / max(1, report.n_picks):.0%})",
@@ -815,7 +815,7 @@ def backtest(
if report.skip_reasons: if report.skip_reasons:
skip_table = Table(title="Motivi di skip aggregati") skip_table = Table(title="Motivi di skip aggregati")
skip_table.add_column("Motivo") skip_table.add_column("Motivo")
skip_table.add_column("Settimane", justify="right") skip_table.add_column("Giorni", justify="right")
for reason, count in sorted( for reason, count in sorted(
report.skip_reasons.items(), key=lambda kv: -kv[1] report.skip_reasons.items(), key=lambda kv: -kv[1]
): ):
@@ -1004,8 +1004,8 @@ def option_chain_trigger(
) -> None: ) -> None:
"""Esegue UNA volta il collector della catena opzioni e persiste in DB. """Esegue UNA volta il collector della catena opzioni e persiste in DB.
Utile per popolare i dati senza aspettare il cron settimanale del Utile per popolare i dati senza aspettare il cron del job
job ``option_chain_snapshot``. Riusa esattamente la stessa pipeline ``option_chain_snapshot``. Riusa esattamente la stessa pipeline
schedulata. schedulata.
""" """
from cerbero_bite.runtime.dependencies import build_runtime # noqa: PLC0415 from cerbero_bite.runtime.dependencies import build_runtime # noqa: PLC0415
+5 -5
View File
@@ -47,7 +47,7 @@ class EntryConfig(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid") model_config = ConfigDict(frozen=True, extra="forbid")
cron: str = "0 14 * * MON" cron: str = "0 14 * * *"
skip_holidays_country: str = "IT" skip_holidays_country: str = "IT"
# access filters (§2) # access filters (§2)
@@ -183,7 +183,7 @@ class SizingConfig(BaseModel):
cap_per_trade_eur: Decimal = Field(default=Decimal("200")) cap_per_trade_eur: Decimal = Field(default=Decimal("200"))
cap_aggregate_open_eur: Decimal = Field(default=Decimal("1000")) cap_aggregate_open_eur: Decimal = Field(default=Decimal("1000"))
max_concurrent_positions: int = 1 max_concurrent_positions: int = 5
max_contracts_per_trade: int = 4 max_contracts_per_trade: int = 4
dvol_adjustment: list[DvolAdjustmentBand] = Field(default_factory=_default_dvol_bands) dvol_adjustment: list[DvolAdjustmentBand] = Field(default_factory=_default_dvol_bands)
@@ -266,8 +266,8 @@ class AutoPauseConfig(BaseModel):
Quando abilitato, il rule engine valuta prima di ogni entry Quando abilitato, il rule engine valuta prima di ogni entry
il P/L cumulato delle ultime `lookback_trades` posizioni chiuse il P/L cumulato delle ultime `lookback_trades` posizioni chiuse
in proporzione al capitale attuale. Se la perdita supera la in proporzione al capitale attuale. Se la perdita supera la
soglia, l'engine si auto-mette in pausa per `pause_weeks` soglia, l'engine si auto-mette in pausa per `pause_days`
settimane (skip-week). La pausa si annulla automaticamente alla giorni (skip-day). La pausa si annulla automaticamente alla
scadenza, oppure manualmente via comando dalla GUI. scadenza, oppure manualmente via comando dalla GUI.
Difende da regime change non rilevati dai filtri quant: se i Difende da regime change non rilevati dai filtri quant: se i
@@ -282,7 +282,7 @@ class AutoPauseConfig(BaseModel):
enabled: bool = False enabled: bool = False
lookback_trades: int = 5 lookback_trades: int = 5
max_drawdown_pct: Decimal = Field(default=Decimal("0.10")) max_drawdown_pct: Decimal = Field(default=Decimal("0.10"))
pause_weeks: int = 2 pause_days: int = 14
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
+29 -30
View File
@@ -2,7 +2,7 @@
Two layers, both pure functions: Two layers, both pure functions:
1. **Entry-filter simulation** for each Monday 14:00 UTC tick in the 1. **Entry-filter simulation** for each daily 14:00 UTC tick in the
recorded snapshots, evaluate which §2 gates would have passed, recorded snapshots, evaluate which §2 gates would have passed,
reconstructing :class:`EntryContext` from the snapshot. This part reconstructing :class:`EntryContext` from the snapshot. This part
is **rigorous**: it uses the same :func:`validate_entry` the live is **rigorous**: it uses the same :func:`validate_entry` the live
@@ -49,12 +49,12 @@ __all__ = [
"BacktestEntry", "BacktestEntry",
"BacktestExit", "BacktestExit",
"BacktestReport", "BacktestReport",
"MondayPick", "DailyPick",
"bs_put_delta", "bs_put_delta",
"bs_put_price", "bs_put_price",
"daily_picks",
"estimate_credit_eth", "estimate_credit_eth",
"find_strike_for_delta", "find_strike_for_delta",
"monday_picks",
"normal_cdf", "normal_cdf",
"run_backtest", "run_backtest",
"simulate_entry_filters", "simulate_entry_filters",
@@ -184,41 +184,40 @@ def estimate_credit_eth(
@dataclass(frozen=True) @dataclass(frozen=True)
class MondayPick: class DailyPick:
"""Indice di un tick "Monday 14:00 UTC" nella time-series.""" """Indice di un tick "daily h:00 UTC" nella time-series."""
timestamp: datetime timestamp: datetime
snapshot: MarketSnapshotRecord snapshot: MarketSnapshotRecord
def monday_picks( def daily_picks(
snapshots: list[MarketSnapshotRecord], snapshots: list[MarketSnapshotRecord],
*, *,
weekday: int = 0, # Monday
hour_utc: int = 14, hour_utc: int = 14,
asset: str = "ETH", asset: str = "ETH",
) -> list[MondayPick]: ) -> list[DailyPick]:
"""Estrae i tick più vicini a "Monday h:00 UTC" per ogni settimana. """Estrae un tick per giorno all'ora ``hour_utc``.
``snapshots`` deve essere ordinato per timestamp ascending. Per ogni Crypto è 24/7, quindi non gateiamo sul giorno della settimana: per
occorrenza di ``weekday + hour_utc`` (es. lun 14:00) presa l'unica ogni giorno di calendario presente in ``snapshots``, prendiamo la
riga ETH che la copre. Settimane senza tick a quell'ora vengono riga ETH che cade a ``hour_utc:00``. Giorni senza tick a quell'ora
saltate. vengono saltati. ``snapshots`` deve essere ordinato per timestamp
ascending.
""" """
picks: list[MondayPick] = [] picks: list[DailyPick] = []
seen_dates: set[tuple[int, int]] = set() # (iso_year, iso_week) seen_dates: set[tuple[int, int, int]] = set() # (year, month, day)
for snap in snapshots: for snap in snapshots:
if snap.asset.upper() != asset.upper(): if snap.asset.upper() != asset.upper():
continue continue
ts = snap.timestamp.astimezone(UTC) ts = snap.timestamp.astimezone(UTC)
if ts.weekday() != weekday or ts.hour != hour_utc: if ts.hour != hour_utc:
continue continue
iso_y, iso_w, _ = ts.isocalendar() key = (ts.year, ts.month, ts.day)
key = (iso_y, iso_w)
if key in seen_dates: if key in seen_dates:
continue continue
seen_dates.add(key) seen_dates.add(key)
picks.append(MondayPick(timestamp=ts, snapshot=snap)) picks.append(DailyPick(timestamp=ts, snapshot=snap))
return picks return picks
@@ -231,9 +230,8 @@ def _entry_context_from_snapshot(
"""Costruisce :class:`EntryContext` dal tick storico. """Costruisce :class:`EntryContext` dal tick storico.
``None`` quando la riga non ha i campi minimi (spot, dvol, funding). ``None`` quando la riga non ha i campi minimi (spot, dvol, funding).
Nel filtro questo si traduce in "skip della settimana" è la Nel filtro questo si traduce in "skip del giorno" è la stessa
stessa logica del live: un tick incompleto è meglio di un'entry logica del live: un tick incompleto è meglio di un'entry al buio.
al buio.
""" """
if snap.dvol is None or snap.funding_perp_annualized is None: if snap.dvol is None or snap.funding_perp_annualized is None:
return None return None
@@ -254,21 +252,21 @@ def _entry_context_from_snapshot(
@dataclass(frozen=True) @dataclass(frozen=True)
class EntryFilterResult: class EntryFilterResult:
"""Esito del check filtri per una singola Monday pick.""" """Esito del check filtri per una singola daily pick."""
pick: MondayPick pick: DailyPick
accepted: bool accepted: bool
reasons: list[str] reasons: list[str]
skipped_for_data: bool # True se il tick non aveva i campi minimi skipped_for_data: bool # True se il tick non aveva i campi minimi
def simulate_entry_filters( def simulate_entry_filters(
picks: list[MondayPick], picks: list[DailyPick],
cfg: StrategyConfig, cfg: StrategyConfig,
*, *,
capital_usd: Decimal, capital_usd: Decimal,
) -> list[EntryFilterResult]: ) -> list[EntryFilterResult]:
"""Per ogni Monday pick, valuta validate_entry come farebbe il live. """Per ogni daily pick, valuta validate_entry come farebbe il live.
Rigoroso: usa esattamente :func:`validate_entry` e :class:`EntryContext`. Rigoroso: usa esattamente :func:`validate_entry` e :class:`EntryContext`.
Restituisce la lista degli esiti, una entry per pick. Restituisce la lista degli esiti, una entry per pick.
@@ -500,7 +498,7 @@ class BacktestReport(BaseModel):
def _build_entry_from_pick( def _build_entry_from_pick(
pick: MondayPick, pick: DailyPick,
cfg: StrategyConfig, cfg: StrategyConfig,
*, *,
capital_usd: Decimal, capital_usd: Decimal,
@@ -566,7 +564,8 @@ def _max_drawdown_usd(equity: list[Decimal]) -> tuple[Decimal, Decimal]:
def _sharpe_annualized(pnls_usd: list[Decimal], capital_usd: Decimal) -> Decimal | None: def _sharpe_annualized(pnls_usd: list[Decimal], capital_usd: Decimal) -> Decimal | None:
"""Annualized Sharpe approximation: 52 trade/anno (settimanali). """Annualized Sharpe approximation: ~120 trade/anno (entry daily,
crypto 24/7, post-filtri + concurrency cap).
Restituisce ``None`` se ci sono <5 trade o stdev = 0. Restituisce ``None`` se ci sono <5 trade o stdev = 0.
""" """
@@ -578,7 +577,7 @@ def _sharpe_annualized(pnls_usd: list[Decimal], capital_usd: Decimal) -> Decimal
std = math.sqrt(var) std = math.sqrt(var)
if std == 0: if std == 0:
return None return None
sharpe = mean / std * math.sqrt(52) sharpe = mean / std * math.sqrt(120)
return Decimal(str(round(sharpe, 3))) return Decimal(str(round(sharpe, 3)))
@@ -593,7 +592,7 @@ def run_backtest(
"""Esegue il backtest end-to-end sui ``snapshots`` ETH ordinati per ts.""" """Esegue il backtest end-to-end sui ``snapshots`` ETH ordinati per ts."""
snapshots = sorted(snapshots, key=lambda s: s.timestamp) snapshots = sorted(snapshots, key=lambda s: s.timestamp)
eth_snapshots = [s for s in snapshots if s.asset.upper() == asset.upper()] eth_snapshots = [s for s in snapshots if s.asset.upper() == asset.upper()]
picks = monday_picks(eth_snapshots, asset=asset) picks = daily_picks(eth_snapshots, asset=asset)
filter_results = simulate_entry_filters(picks, cfg, capital_usd=capital_usd) filter_results = simulate_entry_filters(picks, cfg, capital_usd=capital_usd)
# Tally skip reasons # Tally skip reasons
+31 -12
View File
@@ -435,7 +435,7 @@ def _compute_pl(
- ``A`` (delta dinamico, §3.2): +1.5 pp win-rate, sl_loss × 0.95. - ``A`` (delta dinamico, §3.2): +1.5 pp win-rate, sl_loss × 0.95.
- ``D`` (vol-harvest, §7-bis): 5% delle would-be-loss diventano - ``D`` (vol-harvest, §7-bis): 5% delle would-be-loss diventano
harvest exit a +0.20 × credito. harvest exit a +0.20 × credito.
- ``F`` (auto-pause, §7-bis): 8% trade/anno (skip-week dopo - ``F`` (auto-pause, §7-bis): 8% trade/anno (skip-day dopo
streak), e nei calcoli di drawdown atteso il streak_99 è streak), e nei calcoli di drawdown atteso il streak_99 è
cappato a lookback_trades=5. cappato a lookback_trades=5.
@@ -691,8 +691,13 @@ def _render_pl_panel(
), ),
) )
trades_per_year = col_d.slider( trades_per_year = col_d.slider(
"Trade / anno (post-filtri)", 8, 30, value=18, step=1, "Trade / anno (post-filtri)", 20, 200, value=110, step=5,
help="52 lunedì × probabilità di superare i filtri (3050%).", help=(
"Crypto è 24/7: l'entry cycle gira ogni giorno alle 14:00 UTC "
"(`0 14 * * *`). 365 candidature × ~30-50% pass-rate effettivo "
"(post-filtri + cap concorrenza) ≈ 110-180/anno. Auto-pause F "
"riduce ulteriormente di ~8% in regime drawdown."
),
) )
cons_caps = _profile_caps(strategy_conservativa or strategy_main) cons_caps = _profile_caps(strategy_conservativa or strategy_main)
@@ -746,13 +751,17 @@ def _render_pl_panel(
features=feats_aggr, features=feats_aggr,
) )
cons_version = getattr(
strategy_conservativa or strategy_main, "config_version", "?"
)
aggr_version = getattr(strategy_aggressiva, "config_version", "?")
col_cons, col_aggr = st.columns(2) col_cons, col_aggr = st.columns(2)
with col_cons: with col_cons:
_render_profile_card( _render_profile_card(
"🛡️ Conservativa", "🛡️ Conservativa",
cons_caps, cons_caps,
cons, cons,
"_(golden config v1.2.0)_", f"_(golden config v{cons_version})_",
features=feats_cons, features=feats_cons,
metrics_base=cons_base if apply_features and any(feats_cons.values()) else None, metrics_base=cons_base if apply_features and any(feats_cons.values()) else None,
) )
@@ -761,7 +770,7 @@ def _render_pl_panel(
"🔥 Aggressiva", "🔥 Aggressiva",
aggr_caps, aggr_caps,
aggr, aggr,
"_(deroga §11, richiede paper trading)_", f"_(v{aggr_version} · deroga §11, richiede paper trading)_",
features=feats_aggr, features=feats_aggr,
metrics_base=aggr_base if apply_features and any(feats_aggr.values()) else None, metrics_base=aggr_base if apply_features and any(feats_aggr.values()) else None,
) )
@@ -774,14 +783,24 @@ def _render_pl_panel(
"APR). Drawdown atteso scala con lo stesso fattore." "APR). Drawdown atteso scala con lo stesso fattore."
) )
if win_rate < 0.72: if cons["annual_pl"] < 0 and aggr["annual_pl"] < 0:
st.error( st.error(
"**Win rate sotto 0.72: entrambi i profili perdono soldi.** " f"**Entrambi i profili in perdita** (cons {cons['apr']:+.1%}, "
"Selling vol nudo è strutturalmente neutro qui. L'edge della " f"aggr {aggr['apr']:+.1%} APR). Selling vol nudo a win rate "
"strategia sono i FILTRI (dealer gamma>0, no macro, " f"{win_rate:.0%} è strutturalmente non profittevole. L'edge "
"liquidation≠high, bias chiaro) che alzano il win rate sopra " "sono i FILTRI (dealer gamma>0, no macro, liquidation≠high, "
"il 0.75. Senza filtri attivi nessuno dei due profili è " "bias chiaro) e i miglioramenti F+D+A+IV-RV gate, che alzano "
"viable." "il win rate effettivo sopra ~0.75 e/o riducono i tail loss. "
"Spunta l'opzione 'Applica gli effetti dei miglioramenti' qui "
"sopra per vedere i numeri con i filtri attivi."
)
elif cons["annual_pl"] < 0:
st.warning(
f"**Conservativo in perdita** ({cons['apr']:+.1%} APR), "
f"aggressivo positivo ({aggr['apr']:+.1%} APR). I miglioramenti "
"F+D+A+IV-RV gate stanno facendo il loro lavoro sull'aggressivo. "
"Sul conservativo i cap stretti riducono troppo il P/L atteso "
"a questo win rate."
) )
# === Mini-tabella: contributo marginale di ogni feature ===== # === Mini-tabella: contributo marginale di ogni feature =====
+6 -6
View File
@@ -84,15 +84,15 @@ def is_paused(
) )
def pause_until(now: datetime, weeks: int) -> datetime: def pause_until(now: datetime, days: int) -> datetime:
"""Calcola la scadenza della pausa (``now + weeks``). """Calcola la scadenza della pausa (``now + days``).
Estratto in funzione separata per facilitare i test e per ricordare Estratto in funzione separata per facilitare i test e per ricordare
che la pausa è espressa in **settimane** (la strategia ha cron che la pausa è espressa in **giorni** (la strategia ha cron
settimanale; pause più corte non avrebbero modo di evitare una giornaliero su crypto 24/7; pause sub-giornaliere non avrebbero
settimana di entry). modo di evitare un'entry).
""" """
return now + timedelta(weeks=max(1, weeks)) return now + timedelta(days=max(1, days))
def evaluate_drawdown_breach( def evaluate_drawdown_breach(
+9 -7
View File
@@ -1,9 +1,11 @@
"""Weekly entry decision loop (``docs/06-operational-flow.md`` §2). """Daily entry decision loop (``docs/06-operational-flow.md`` §2).
Pure orchestration over the existing core/clients/state primitives. Crypto è 24/7: la cadenza di candidatura non è gateata sulla
The cycle is auto-execute: when every gate passes, the engine sends settimana, sono i gate quantitativi a decidere se entrare o saltare
the combo order without asking Adriano. Telegram is used only to il giorno. Pure orchestration over the existing core/clients/state
notify the outcome. primitives. The cycle is auto-execute: when every gate passes, the
engine sends the combo order without asking Adriano. Telegram is
used only to notify the outcome.
""" """
from __future__ import annotations from __future__ import annotations
@@ -328,7 +330,7 @@ async def run_entry_cycle(
eur_to_usd_rate: Decimal, eur_to_usd_rate: Decimal,
now: datetime | None = None, now: datetime | None = None,
) -> EntryCycleResult: ) -> EntryCycleResult:
"""Run one weekly entry evaluation cycle. """Run one daily entry evaluation cycle.
The function is idempotent and side-effect aware: it persists the The function is idempotent and side-effect aware: it persists the
decision in the ``decisions`` table regardless of outcome and only decision in the ``decisions`` table regardless of outcome and only
@@ -406,7 +408,7 @@ async def run_entry_cycle(
capital_usd=capital_usd, capital_usd=capital_usd,
) )
if breach.should_pause: if breach.should_pause:
until = auto_pause_module.pause_until(when, auto_cfg.pause_weeks) until = auto_pause_module.pause_until(when, auto_cfg.pause_days)
conn = connect_state(ctx.db_path) conn = connect_state(ctx.db_path)
try: try:
with transaction(conn): with transaction(conn):
@@ -1,10 +1,11 @@
"""Periodic option-chain snapshot collector (§13). """Periodic option-chain snapshot collector (§13).
Fetches the Deribit option chain for every strike entro la finestra Fetches the Deribit option chain for every strike entro la finestra
DTE configurata, prima del trigger entry settimanale (cron DTE configurata. Cadenza di default ``*/15 * * * *`` (allineata a
``55 13 * * MON`` di default). Persiste un quote per ogni strumento ``market_snapshot``: crypto è 24/7 e l'accumulo dataset deve essere
in ``option_chain_snapshots`` con un timestamp condiviso, che diventa continuo, non gateato sui rollover TradFi-style). Persiste un quote
il dato di base per: per ogni strumento in ``option_chain_snapshots`` con un timestamp
condiviso, che diventa il dato di base per:
* il backtest non-stilizzato (vedi ``core/backtest.py``), * il backtest non-stilizzato (vedi ``core/backtest.py``),
* la calibrazione empirica dello skew premium e del credit/width * la calibrazione empirica dello skew premium e del credit/width
+2 -2
View File
@@ -50,13 +50,13 @@ _log = logging.getLogger("cerbero_bite.runtime.orchestrator")
Environment = Literal["testnet", "mainnet"] Environment = Literal["testnet", "mainnet"]
# Default cron schedule (matches docs/06-operational-flow.md table). # Default cron schedule (matches docs/06-operational-flow.md table).
_CRON_ENTRY = "0 14 * * MON" _CRON_ENTRY = "0 14 * * *" # crypto 24/7: candidatura giornaliera; i gate decidono se entrare
_CRON_MONITOR = "0 2,14 * * *" _CRON_MONITOR = "0 2,14 * * *"
_CRON_HEALTH = "*/5 * * * *" _CRON_HEALTH = "*/5 * * * *"
_CRON_BACKUP = "0 * * * *" _CRON_BACKUP = "0 * * * *"
_CRON_MANUAL_ACTIONS = "*/1 * * * *" _CRON_MANUAL_ACTIONS = "*/1 * * * *"
_CRON_MARKET_SNAPSHOT = "*/15 * * * *" _CRON_MARKET_SNAPSHOT = "*/15 * * * *"
_CRON_OPTION_CHAIN_SNAPSHOT = "55 13 * * MON" # 5 min prima del trigger entry _CRON_OPTION_CHAIN_SNAPSHOT = "*/15 * * * *" # crypto è 24/7: cadenza continua allineata a market_snapshot
_BACKUP_RETENTION_DAYS = 30 _BACKUP_RETENTION_DAYS = 30
+6 -6
View File
@@ -28,8 +28,8 @@
# 2× via "ETH + BTC" indicato in `📚 Strategia` è una **stima ex-ante** # 2× via "ETH + BTC" indicato in `📚 Strategia` è una **stima ex-ante**
# di cosa otterresti DOPO quel lavoro di codice. # di cosa otterresti DOPO quel lavoro di codice.
config_version: "1.3.0-aggressiva" config_version: "1.4.0-aggressiva"
config_hash: "e983e156bf0c270941765e7b9639a35fdc6de7b091076bf5a9b360e294e81e4c" config_hash: "7a39214a7efd2861d22d465f6caf758fec84598775c3f01d922782d7f6f337b0"
last_review: "2026-04-26" last_review: "2026-04-26"
last_reviewer: "Adriano" last_reviewer: "Adriano"
@@ -38,7 +38,7 @@ asset:
exchange: "deribit" exchange: "deribit"
entry: entry:
cron: "0 14 * * MON" cron: "0 14 * * *"
skip_holidays_country: "IT" skip_holidays_country: "IT"
capital_min_usd: "2880" # 4× del minimo conservativo (720) capital_min_usd: "2880" # 4× del minimo conservativo (720)
@@ -108,8 +108,8 @@ sizing:
# Le tre leve dominanti: # Le tre leve dominanti:
cap_per_trade_eur: "800" # era 200 → 4× cap_per_trade_eur: "800" # era 200 → 4×
cap_aggregate_open_eur: "3200" # era 1000 → 4× (proporzionato a 2 posizioni × cap_per_trade × 2 ruote) cap_aggregate_open_eur: "6400" # 8 posizioni concorrenti × 800 cap_per_trade (entry daily)
max_concurrent_positions: 2 # era 1 max_concurrent_positions: 8 # era 2 (entry daily × DTE 14-21 × pass-rate)
max_contracts_per_trade: 16 # era 4 → 4× max_contracts_per_trade: 16 # era 4 → 4×
dvol_adjustment: dvol_adjustment:
@@ -150,7 +150,7 @@ auto_pause:
enabled: true enabled: true
lookback_trades: 5 lookback_trades: 5
max_drawdown_pct: "0.15" max_drawdown_pct: "0.15"
pause_weeks: 2 pause_days: 14
execution: execution:
environment: "testnet" environment: "testnet"
+6 -6
View File
@@ -15,8 +15,8 @@
# cerbero-bite config hash --file strategy.conservativa.yaml # cerbero-bite config hash --file strategy.conservativa.yaml
# e bumpare config_version. # e bumpare config_version.
config_version: "1.3.0-conservativa" config_version: "1.4.0-conservativa"
config_hash: "900646beb1dd0a7bfaf553f76adb4b55004eff1f094585f779302131625919e8" config_hash: "b6af7b041508a67846eba5985e27e655526fe89105653f86bc88b8a4a437ac3a"
last_review: "2026-04-26" last_review: "2026-04-26"
last_reviewer: "Adriano" last_reviewer: "Adriano"
@@ -25,7 +25,7 @@ asset:
exchange: "deribit" exchange: "deribit"
entry: entry:
cron: "0 14 * * MON" cron: "0 14 * * *"
skip_holidays_country: "IT" skip_holidays_country: "IT"
capital_min_usd: "720" capital_min_usd: "720"
@@ -84,8 +84,8 @@ sizing:
kelly_fraction: "0.13" kelly_fraction: "0.13"
cap_per_trade_eur: "200" cap_per_trade_eur: "200"
cap_aggregate_open_eur: "1000" cap_aggregate_open_eur: "1000" # 3 posizioni × ~200 + headroom (entry daily, profilo conservativo)
max_concurrent_positions: 1 max_concurrent_positions: 3
max_contracts_per_trade: 4 max_contracts_per_trade: 4
@@ -119,7 +119,7 @@ auto_pause:
enabled: false enabled: false
lookback_trades: 5 lookback_trades: 5
max_drawdown_pct: "0.10" max_drawdown_pct: "0.10"
pause_weeks: 2 pause_days: 14
execution: execution:
environment: "testnet" environment: "testnet"
+5 -5
View File
@@ -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.3.0" config_version: "1.4.0"
config_hash: "178a87467707d54d1ffef2d585a3a01be54de5ccc7e23493356eac47fd1c24d8" config_hash: "22182814216190331e0b69b3bc99493e6d69cc813f7ed937394986eecc1f5d11"
last_review: "2026-04-26" last_review: "2026-04-26"
last_reviewer: "Adriano" last_reviewer: "Adriano"
@@ -16,7 +16,7 @@ asset:
exchange: "deribit" exchange: "deribit"
entry: entry:
cron: "0 14 * * MON" cron: "0 14 * * *"
skip_holidays_country: "IT" skip_holidays_country: "IT"
capital_min_usd: "720" capital_min_usd: "720"
@@ -81,7 +81,7 @@ sizing:
cap_per_trade_eur: "200" cap_per_trade_eur: "200"
cap_aggregate_open_eur: "1000" cap_aggregate_open_eur: "1000"
max_concurrent_positions: 1 max_concurrent_positions: 5
max_contracts_per_trade: 4 max_contracts_per_trade: 4
@@ -121,7 +121,7 @@ auto_pause:
enabled: false enabled: false
lookback_trades: 5 lookback_trades: 5
max_drawdown_pct: "0.10" max_drawdown_pct: "0.10"
pause_weeks: 2 pause_days: 14
execution: execution:
environment: "testnet" # testnet|mainnet — kill switch on broker mismatch environment: "testnet" # testnet|mainnet — kill switch on broker mismatch
+9 -9
View File
@@ -68,16 +68,16 @@ def test_is_paused_returns_false_when_until_in_past() -> None:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def test_pause_until_adds_weeks() -> None: def test_pause_until_adds_days() -> None:
until = pause_until(_NOW, weeks=2) until = pause_until(_NOW, days=14)
assert until == _NOW + timedelta(weeks=2) assert until == _NOW + timedelta(days=14)
def test_pause_until_clamps_to_one_week_minimum() -> None: def test_pause_until_clamps_to_one_day_minimum() -> None:
# weeks <= 0 deve cmq dare almeno 1 settimana di pausa, altrimenti # days <= 0 deve cmq dare almeno 1 giorno di pausa, altrimenti
# la cron settimanale potrebbe scattare comunque. # la cron giornaliera potrebbe scattare comunque.
assert pause_until(_NOW, weeks=0) == _NOW + timedelta(weeks=1) assert pause_until(_NOW, days=0) == _NOW + timedelta(days=1)
assert pause_until(_NOW, weeks=-3) == _NOW + timedelta(weeks=1) assert pause_until(_NOW, days=-3) == _NOW + timedelta(days=1)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -90,7 +90,7 @@ def _cfg(**overrides: object) -> AutoPauseConfig:
"enabled": True, "enabled": True,
"lookback_trades": 5, "lookback_trades": 5,
"max_drawdown_pct": Decimal("0.10"), "max_drawdown_pct": Decimal("0.10"),
"pause_weeks": 2, "pause_days": 14,
} }
base.update(overrides) base.update(overrides)
return AutoPauseConfig(**base) # type: ignore[arg-type] return AutoPauseConfig(**base) # type: ignore[arg-type]
+28 -28
View File
@@ -11,9 +11,9 @@ from cerbero_bite.config import StrategyConfig, golden_config
from cerbero_bite.core.backtest import ( from cerbero_bite.core.backtest import (
bs_put_delta, bs_put_delta,
bs_put_price, bs_put_price,
daily_picks,
estimate_credit_eth, estimate_credit_eth,
find_strike_for_delta, find_strike_for_delta,
monday_picks,
normal_cdf, normal_cdf,
run_backtest, run_backtest,
simulate_entry_filters, simulate_entry_filters,
@@ -87,7 +87,7 @@ def test_estimate_credit_returns_positive_credit_in_normal_regime() -> None:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Monday picks + entry filter simulation # Daily picks + entry filter simulation
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -114,35 +114,35 @@ def _snap(
) )
def test_monday_picks_extracts_one_per_iso_week() -> None: def test_daily_picks_extracts_one_per_calendar_day() -> None:
monday_2026_05_04 = datetime(2026, 5, 4, 14, 0, tzinfo=UTC) day1 = datetime(2026, 5, 4, 14, 0, tzinfo=UTC)
monday_2026_05_11 = datetime(2026, 5, 11, 14, 0, tzinfo=UTC) day2 = datetime(2026, 5, 5, 14, 0, tzinfo=UTC) # Tuesday: PICKED ora (crypto 24/7)
snapshots = [ snapshots = [
_snap(ts=monday_2026_05_04), _snap(ts=day1),
_snap(ts=monday_2026_05_04 + timedelta(minutes=15)), # not picked _snap(ts=day1 + timedelta(minutes=15)), # stesso giorno, deduplicato
_snap(ts=monday_2026_05_11), _snap(ts=day2),
] ]
picks = monday_picks(snapshots) picks = daily_picks(snapshots)
assert len(picks) == 2 assert len(picks) == 2
assert picks[0].timestamp == monday_2026_05_04 assert picks[0].timestamp == day1
assert picks[1].timestamp == monday_2026_05_11 assert picks[1].timestamp == day2
def test_monday_picks_skips_other_days_and_hours() -> None: def test_daily_picks_skips_other_hours() -> None:
snapshots = [ snapshots = [
_snap(ts=datetime(2026, 5, 4, 13, 0, tzinfo=UTC)), # Monday 13:00 _snap(ts=datetime(2026, 5, 4, 13, 0, tzinfo=UTC)), # 13:00 → skipped
_snap(ts=datetime(2026, 5, 5, 14, 0, tzinfo=UTC)), # Tuesday 14:00 _snap(ts=datetime(2026, 5, 5, 15, 30, tzinfo=UTC)), # 15:30 → skipped
] ]
assert monday_picks(snapshots) == [] assert daily_picks(snapshots) == []
def test_monday_picks_filters_by_asset() -> None: def test_daily_picks_filters_by_asset() -> None:
monday = datetime(2026, 5, 4, 14, 0, tzinfo=UTC) day = datetime(2026, 5, 4, 14, 0, tzinfo=UTC)
snapshots = [ snapshots = [
_snap(ts=monday, asset="BTC"), _snap(ts=day, asset="BTC"),
_snap(ts=monday, asset="ETH"), _snap(ts=day, asset="ETH"),
] ]
picks = monday_picks(snapshots, asset="ETH") picks = daily_picks(snapshots, asset="ETH")
assert len(picks) == 1 assert len(picks) == 1
assert picks[0].snapshot.asset == "ETH" assert picks[0].snapshot.asset == "ETH"
@@ -156,8 +156,8 @@ def test_simulate_entry_filters_accepts_clean_snapshot(
type("MP", (), {"timestamp": monday, "snapshot": snap})() # type: ignore[arg-type] type("MP", (), {"timestamp": monday, "snapshot": snap})() # type: ignore[arg-type]
] ]
# Hack: build via real dataclass # Hack: build via real dataclass
from cerbero_bite.core.backtest import MondayPick from cerbero_bite.core.backtest import DailyPick
picks = [MondayPick(timestamp=monday, snapshot=snap)] picks = [DailyPick(timestamp=monday, snapshot=snap)]
results = simulate_entry_filters(picks, cfg, capital_usd=Decimal("1500")) results = simulate_entry_filters(picks, cfg, capital_usd=Decimal("1500"))
assert len(results) == 1 assert len(results) == 1
assert results[0].accepted is True assert results[0].accepted is True
@@ -167,8 +167,8 @@ def test_simulate_entry_filters_rejects_dvol_out_of_band() -> None:
cfg = golden_config() cfg = golden_config()
monday = datetime(2026, 5, 4, 14, 0, tzinfo=UTC) monday = datetime(2026, 5, 4, 14, 0, tzinfo=UTC)
snap = _snap(ts=monday, dvol=20, funding=0.10) # below 35 snap = _snap(ts=monday, dvol=20, funding=0.10) # below 35
from cerbero_bite.core.backtest import MondayPick from cerbero_bite.core.backtest import DailyPick
picks = [MondayPick(timestamp=monday, snapshot=snap)] picks = [DailyPick(timestamp=monday, snapshot=snap)]
results = simulate_entry_filters(picks, cfg, capital_usd=Decimal("1500")) results = simulate_entry_filters(picks, cfg, capital_usd=Decimal("1500"))
assert results[0].accepted is False assert results[0].accepted is False
assert any("dvol" in r.lower() for r in results[0].reasons) assert any("dvol" in r.lower() for r in results[0].reasons)
@@ -182,8 +182,8 @@ def test_simulate_entry_filters_skips_incomplete_snapshot() -> None:
# dvol=None ⇒ skipped # dvol=None ⇒ skipped
fetch_ok=False, fetch_ok=False,
) )
from cerbero_bite.core.backtest import MondayPick from cerbero_bite.core.backtest import DailyPick
picks = [MondayPick(timestamp=monday, snapshot=incomplete)] picks = [DailyPick(timestamp=monday, snapshot=incomplete)]
results = simulate_entry_filters(picks, cfg, capital_usd=Decimal("1500")) results = simulate_entry_filters(picks, cfg, capital_usd=Decimal("1500"))
assert results[0].accepted is False assert results[0].accepted is False
assert results[0].skipped_for_data is True assert results[0].skipped_for_data is True
@@ -208,8 +208,8 @@ def _synthetic_year_of_snapshots(
base = monday + timedelta(weeks=week) base = monday + timedelta(weeks=week)
# Lunedì 14:00 è il pick # Lunedì 14:00 è il pick
rows.append(_snap(ts=base, spot=spot, dvol=dvol, funding=funding)) rows.append(_snap(ts=base, spot=spot, dvol=dvol, funding=funding))
# Tick intermedi che NON cadono di lunedì alle 14:00: # Tick intermedi che NON cadono alle 14:00:
# offset +1h così vengono ignorati da `monday_picks`. # offset +1h (=15:00) così vengono ignorati da `daily_picks`.
for d in (2, 8, 14, 19): for d in (2, 8, 14, 19):
rows.append( rows.append(
_snap( _snap(
+1 -1
View File
@@ -68,7 +68,7 @@ def test_compute_hash_is_independent_of_recorded_hash_value(tmp_path: Path) -> N
def test_load_repo_strategy_yaml(tmp_path: Path) -> None: def test_load_repo_strategy_yaml(tmp_path: Path) -> None:
"""The committed strategy.yaml validates with the recorded hash.""" """The committed strategy.yaml validates with the recorded hash."""
result = load_strategy(REPO_ROOT / "strategy.yaml") result = load_strategy(REPO_ROOT / "strategy.yaml")
assert result.config.config_version == "1.3.0" assert result.config.config_version == "1.4.0"
assert result.config.sizing.kelly_fraction == Decimal("0.13") assert result.config.sizing.kelly_fraction == Decimal("0.13")
assert result.computed_hash == result.config.config_hash assert result.computed_hash == result.config.config_hash
+2 -1
View File
@@ -144,7 +144,8 @@ def test_aggregate_cap_at_975_reduces_to_one(cfg: StrategyConfig) -> None:
def test_concurrent_positions_at_cap_returns_zero(cfg: StrategyConfig) -> None: def test_concurrent_positions_at_cap_returns_zero(cfg: StrategyConfig) -> None:
res = compute_contracts(_ctx(capital_usd="1500", other_open_positions=1), cfg) # Default cap = 5 (entry daily). other_open_positions=5 ⇒ cap raggiunto.
res = compute_contracts(_ctx(capital_usd="1500", other_open_positions=5), cfg)
assert res.n_contracts == 0 assert res.n_contracts == 0
assert res.reason_if_zero is not None assert res.reason_if_zero is not None
assert "position" in res.reason_if_zero.lower() assert "position" in res.reason_if_zero.lower()