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:
@@ -15,7 +15,10 @@ attiva, sizing Quarter Kelly e disciplina di uscita rigida.
|
||||
- **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
|
||||
(|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.
|
||||
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/04-mcp-integration.md` | Mappa dei tool MCP usati e contratti |
|
||||
| `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/08-testing-validation.md` | TDD, paper trading, golden tests |
|
||||
| `docs/09-development-roadmap.md` | Fasi di sviluppo e milestone |
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ per imparare, ma **non sta nel loop di esecuzione**.
|
||||
### Cosa fa Cerbero Bite
|
||||
|
||||
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.
|
||||
4. Verifica liquidità, cap di rischio, calendar macro.
|
||||
5. Calcola sizing in contratti.
|
||||
|
||||
@@ -19,10 +19,12 @@ Sorgente teorica: `Cerbero_Office/NewStrategy/strategia-credit-spread-eth.md`
|
||||
|
||||
## 2. Trigger di apertura (entry)
|
||||
|
||||
Il rule engine valuta l'apertura di un nuovo trade **una sola volta al
|
||||
giorno**, alle **14:00 UTC** del lunedì (orario UE pomeridiano stabile,
|
||||
fuori dai picchi di funding statunitensi). Se il lunedì è festività
|
||||
italiana, l'engine ignora la regola e attende il lunedì successivo.
|
||||
Il rule engine valuta l'apertura di un nuovo trade **una volta al
|
||||
giorno**, alle **14:00 UTC** (orario UE pomeridiano stabile, fuori
|
||||
dai picchi di funding statunitensi). Crypto è 24/7: non c'è un "giorno
|
||||
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
|
||||
sono vere:
|
||||
@@ -70,7 +72,7 @@ Razionale: il selling vol nudo è strutturalmente neutro a win-rate
|
||||
per il razionale completo.
|
||||
|
||||
Se anche **una sola** condizione fallisce → **no entry**, log con motivo,
|
||||
ritento la settimana successiva.
|
||||
ritento il giorno successivo.
|
||||
|
||||
## 3. Selezione struttura
|
||||
|
||||
@@ -256,13 +258,13 @@ ogni entry-cycle:
|
||||
> Se `auto_pause.enabled` e P/L cumulato delle ultime
|
||||
> `lookback_trades` posizioni chiuse < `−max_drawdown_pct ×
|
||||
> 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
|
||||
si annulla automaticamente alla scadenza, oppure manualmente con
|
||||
`UPDATE system_state SET auto_pause_until = NULL`. Default
|
||||
disabilitato; profilo aggressivo: lookback 5 trade, soglia 15%, 2
|
||||
settimane di pausa.
|
||||
disabilitato; profilo aggressivo: lookback 5 trade, soglia 15%, 14
|
||||
giorni di pausa.
|
||||
|
||||
## 8. Esecuzione di apertura
|
||||
|
||||
@@ -296,7 +298,7 @@ settimane di pausa.
|
||||
durante un trade aperto).
|
||||
- Non aggiusta strike o size dopo l'apertura.
|
||||
- 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 si aggiorna automaticamente: nuovo set di regole = nuovo deploy
|
||||
con review esplicita.
|
||||
|
||||
@@ -122,7 +122,7 @@ Cerbero_Bite/
|
||||
│ │ ├── lockfile.py # fcntl.flock single-instance
|
||||
│ │ ├── alert_manager.py # severity routing
|
||||
│ │ ├── 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
|
||||
│ │ └── recovery.py # state reconcile al boot
|
||||
│ ├── state/ # persistenza
|
||||
|
||||
+10
-7
@@ -220,11 +220,12 @@ NULL = engine attivo.
|
||||
|
||||
### `option_chain_snapshots`
|
||||
|
||||
Snapshot della catena opzioni Deribit prelevata settimanalmente
|
||||
(cron `55 13 * * MON`, 5 minuti prima del trigger entry). Ogni
|
||||
tick contiene un quote per strumento entro la finestra
|
||||
`[dte_min, dte_max]` di config; tutti i quote prelevati nello stesso
|
||||
tick condividono ``timestamp``. Migration `0005`.
|
||||
Snapshot della catena opzioni Deribit prelevata in continuo
|
||||
(cron `*/15 * * * *`, allineato a `market_snapshots`). Crypto è
|
||||
24/7: l'accumulo dataset deve essere continuo, non gateato sulla
|
||||
settimana. Ogni tick contiene un quote per strumento entro la
|
||||
finestra `[dte_min, dte_max]` di config; tutti i quote prelevati
|
||||
nello stesso tick condividono ``timestamp``. Migration `0005`.
|
||||
|
||||
```sql
|
||||
CREATE TABLE option_chain_snapshots (
|
||||
@@ -259,8 +260,10 @@ sugli strike candidati al picker.
|
||||
con prezzi reali invece di Black-Scholes), la calibrazione empirica
|
||||
dello skew premium, la validazione ex-post dello strike picker.
|
||||
|
||||
Volume atteso: ~50 strike × 3 scadenze × 1 snapshot/settimana ×
|
||||
17 colonne ≈ 12 KB/settimana, ~600 KB/anno.
|
||||
Volume atteso (cron `*/15 * * * *`, ~96 snapshot/giorno):
|
||||
~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
|
||||
|
||||
|
||||
@@ -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
|
||||
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
|
||||
@@ -219,7 +221,7 @@ proposed
|
||||
|
||||
| Cron | Trigger | Frequenza |
|
||||
|---|---|---|
|
||||
| `0 14 * * MON` | Entry evaluation | Settimanale |
|
||||
| `0 14 * * *` | Entry evaluation | Giornaliera |
|
||||
| `0 2,14 * * *` | Position monitoring | 2× giorno |
|
||||
| `0 12 1 * *` | Kelly recalibration | Mensile |
|
||||
| `*/5 * * * *` | Health check | 5 min |
|
||||
@@ -237,7 +239,7 @@ Il bot riconosce due interruttori indipendenti, letti da
|
||||
| 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_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
|
||||
**sempre attivi**, indipendentemente dai flag, perché tengono in vita il
|
||||
|
||||
@@ -54,7 +54,7 @@ cerbero-bite kill-switch disarm --reason "<motivo>" \
|
||||
L'operazione è transazionale: SQLite `system_state.kill_switch = 0` +
|
||||
una linea `KILL_SWITCH_DISARMED` nella audit chain con il motivo. Il
|
||||
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.
|
||||
|
||||
## Cap di rischio (oltre alle regole di strategia)
|
||||
|
||||
@@ -157,9 +157,9 @@ chain:
|
||||
|
||||
| Test | Scenario |
|
||||
|---|---|
|
||||
| `test_weekly_open_happy_path` | Tutto OK → proposta inviata |
|
||||
| `test_weekly_open_no_strike_available` | Chain vuota nel range delta |
|
||||
| `test_weekly_open_macro_blocks` | FOMC entro 5 giorni |
|
||||
| `test_daily_open_happy_path` | Tutto OK → proposta inviata |
|
||||
| `test_daily_open_no_strike_available` | Chain vuota nel range delta |
|
||||
| `test_daily_open_macro_blocks` | FOMC entro 5 giorni |
|
||||
| `test_monitor_profit_take` | Mark = 50% credito → close_profit |
|
||||
| `test_monitor_vol_stop` | DVOL +12 → close_vol |
|
||||
| `test_recovery_after_crash_open_position` | Crash mid-fill, restart, riconcilia |
|
||||
@@ -175,8 +175,8 @@ checked-in.
|
||||
|
||||
```
|
||||
tests/golden/
|
||||
├── 2026-04-27_weekly_open_bull_put.yaml # input snapshot
|
||||
├── 2026-04-27_weekly_open_bull_put.golden # output atteso
|
||||
├── 2026-04-27_daily_open_bull_put.yaml # input snapshot
|
||||
├── 2026-04-27_daily_open_bull_put.golden # output atteso
|
||||
└── runner.py
|
||||
```
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ Tasks:
|
||||
4. `runtime/alert_manager.py` — escalation policy
|
||||
|
||||
Test integration su scenari completi (vedi `08-testing-validation.md`):
|
||||
- weekly open happy path
|
||||
- daily open happy path
|
||||
- monitor profit take
|
||||
- monitor vol stop
|
||||
- recovery dopo crash
|
||||
@@ -206,7 +206,7 @@ Setup:
|
||||
|
||||
Metriche da raccogliere:
|
||||
|
||||
- Numero proposte settimanali emesse
|
||||
- Numero proposte giornaliere emesse
|
||||
- Quante passano i filtri
|
||||
- Win rate, avg P&L paper
|
||||
- Discrepanze tra mid stimato e fill reale (slippage)
|
||||
|
||||
@@ -24,8 +24,8 @@ asset:
|
||||
# === ENTRY ===
|
||||
|
||||
entry:
|
||||
# finestra di valutazione settimanale
|
||||
cron: "0 14 * * MON" # lunedì 14:00 UTC
|
||||
# finestra di valutazione giornaliera (crypto 24/7)
|
||||
cron: "0 14 * * *" # ogni giorno 14:00 UTC
|
||||
skip_holidays_country: "IT"
|
||||
|
||||
# filtri di accesso (vedi 01-strategy-rules.md §2)
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
## 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
|
||||
mercato non è in **stress di liquidazione**, non ci sono **eventi macro
|
||||
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
|
||||
`market_snapshots`. Quei dati alimentano tre obiettivi distinti:
|
||||
|
||||
1. **Decisione live** — l'entry ciclo del lunedì 14:00 UTC legge i
|
||||
campi più freschi per dire "go/no-go".
|
||||
1. **Decisione live** — l'entry ciclo daily alle 14:00 UTC legge i
|
||||
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
|
||||
confronta la situazione con quella all'apertura.
|
||||
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
|
||||
`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
|
||||
@@ -211,7 +212,7 @@ SE NESSUNA posizione aperta
|
||||
ALLORA
|
||||
procedi alla Fase 2
|
||||
ALTRIMENTI
|
||||
no entry, log motivo, ritento la settimana successiva
|
||||
no entry, log motivo, ritento il giorno successivo
|
||||
```
|
||||
|
||||
### Fase 2 — Bias e struttura
|
||||
@@ -363,8 +364,8 @@ capitale **non aumenta** i contratti per trade.
|
||||
|
||||
### Frequenza realistica di entry
|
||||
|
||||
La regola si valuta una volta a settimana, ma la maggioranza dei
|
||||
lunedì viene saltata per:
|
||||
La regola si valuta **una volta al giorno** (crypto è 24/7), ma la
|
||||
maggioranza dei giorni viene saltata per:
|
||||
|
||||
| Motivo di skip | Frequenza tipica |
|
||||
|---|---|
|
||||
@@ -372,11 +373,12 @@ lunedì viene saltata per:
|
||||
| Bias non chiaro (trend × funding discordi o entrambi neutri senza IC) | 25–35% |
|
||||
| Macro entro DTE | 10–20% |
|
||||
| Funding o liquidation risk fuori soglia | 5–15% |
|
||||
| Capitale o sizing insufficiente | 0–5% |
|
||||
| Capitale, sizing insufficiente o concurrency cap raggiunto | 5–15% |
|
||||
|
||||
**Risultato netto: 30–50% delle settimane finisce in entry effettiva
|
||||
⇒ 15–25 trade / anno** (52 lunedì × 30–50%). Le altre settimane il
|
||||
bot sta fermo. È il design.
|
||||
**Risultato netto: ~30–40% dei giorni finisce in entry effettiva
|
||||
⇒ 110–145 trade / anno** (365 candidature × pass-rate, capped da
|
||||
`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%)
|
||||
|
||||
@@ -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
|
||||
sarebbero stati filtrati (vedi pagina `📐 Calibrazione`, colonna
|
||||
"% 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.
|
||||
|
||||
> Suggerimento: 4 settimane di dati = 4 lunedì × probabilità entry =
|
||||
> 1–2 candidate entry effettive. **Aspettare almeno 8 settimane**
|
||||
> Suggerimento: 30 giorni di dati = 30 candidature × probabilità entry
|
||||
> ≈ 9–12 candidate entry effettive. **Aspettare almeno 60 giorni**
|
||||
> prima di tarare le soglie dà uno storico con dispersione
|
||||
> 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
|
||||
setup passano questo gate, anche con IV-RV negativa. Motivo: nei
|
||||
primi 8 turni di lunedì non si hanno abbastanza tick per stabilire
|
||||
che soglia ha senso nel proprio regime. Lasciamo la pagina
|
||||
primi 60 giorni non si hanno abbastanza tick per stabilire che
|
||||
soglia ha senso nel proprio regime. Lasciamo la pagina
|
||||
`📐 Calibrazione` mostrare la distribuzione e poi alziamo
|
||||
manualmente.
|
||||
- **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
|
||||
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
|
||||
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
|
||||
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]` —
|
||||
legge l'ultimo snapshot, simula il selector di strike con la
|
||||
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
|
||||
N giorni → il regime sta pagando bene la vendita di vol. Sono i
|
||||
periodi in cui la strategia ha edge maggiore.
|
||||
2. **Premio "magro":** `dvol < 35` per più giorni → la finestra del
|
||||
lunedì viene saltata. Non è un fallimento: è la disciplina che
|
||||
funziona.
|
||||
2. **Premio "magro":** `dvol < 35` per più giorni → la finestra
|
||||
giornaliera viene saltata. Non è un fallimento: è la disciplina
|
||||
che funziona.
|
||||
3. **Stress imminente:** `liquidation_*_risk = high` o spike di
|
||||
`oi_delta_pct_4h` (> 5% in valore assoluto) + funding ai limiti
|
||||
→ atteso vol stop / time stop attivi nei prossimi cicli, anche
|
||||
|
||||
@@ -789,7 +789,7 @@ def backtest(
|
||||
table = Table(title=f"Backtest report — {strategy_path.name}")
|
||||
table.add_column("Metrica", style="cyan")
|
||||
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(
|
||||
"Accettati dai filtri",
|
||||
f"{report.n_accepted} ({report.n_accepted / max(1, report.n_picks):.0%})",
|
||||
@@ -815,7 +815,7 @@ def backtest(
|
||||
if report.skip_reasons:
|
||||
skip_table = Table(title="Motivi di skip aggregati")
|
||||
skip_table.add_column("Motivo")
|
||||
skip_table.add_column("Settimane", justify="right")
|
||||
skip_table.add_column("Giorni", justify="right")
|
||||
for reason, count in sorted(
|
||||
report.skip_reasons.items(), key=lambda kv: -kv[1]
|
||||
):
|
||||
@@ -1004,8 +1004,8 @@ def option_chain_trigger(
|
||||
) -> None:
|
||||
"""Esegue UNA volta il collector della catena opzioni e persiste in DB.
|
||||
|
||||
Utile per popolare i dati senza aspettare il cron settimanale del
|
||||
job ``option_chain_snapshot``. Riusa esattamente la stessa pipeline
|
||||
Utile per popolare i dati senza aspettare il cron del job
|
||||
``option_chain_snapshot``. Riusa esattamente la stessa pipeline
|
||||
schedulata.
|
||||
"""
|
||||
from cerbero_bite.runtime.dependencies import build_runtime # noqa: PLC0415
|
||||
|
||||
@@ -47,7 +47,7 @@ class EntryConfig(BaseModel):
|
||||
|
||||
model_config = ConfigDict(frozen=True, extra="forbid")
|
||||
|
||||
cron: str = "0 14 * * MON"
|
||||
cron: str = "0 14 * * *"
|
||||
skip_holidays_country: str = "IT"
|
||||
|
||||
# access filters (§2)
|
||||
@@ -183,7 +183,7 @@ class SizingConfig(BaseModel):
|
||||
|
||||
cap_per_trade_eur: Decimal = Field(default=Decimal("200"))
|
||||
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
|
||||
|
||||
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 —
|
||||
il P/L cumulato delle ultime `lookback_trades` posizioni chiuse
|
||||
in proporzione al capitale attuale. Se la perdita supera la
|
||||
soglia, l'engine si auto-mette in pausa per `pause_weeks`
|
||||
settimane (skip-week). La pausa si annulla automaticamente alla
|
||||
soglia, l'engine si auto-mette in pausa per `pause_days`
|
||||
giorni (skip-day). La pausa si annulla automaticamente alla
|
||||
scadenza, oppure manualmente via comando dalla GUI.
|
||||
|
||||
Difende da regime change non rilevati dai filtri quant: se i
|
||||
@@ -282,7 +282,7 @@ class AutoPauseConfig(BaseModel):
|
||||
enabled: bool = False
|
||||
lookback_trades: int = 5
|
||||
max_drawdown_pct: Decimal = Field(default=Decimal("0.10"))
|
||||
pause_weeks: int = 2
|
||||
pause_days: int = 14
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
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,
|
||||
reconstructing :class:`EntryContext` from the snapshot. This part
|
||||
is **rigorous**: it uses the same :func:`validate_entry` the live
|
||||
@@ -49,12 +49,12 @@ __all__ = [
|
||||
"BacktestEntry",
|
||||
"BacktestExit",
|
||||
"BacktestReport",
|
||||
"MondayPick",
|
||||
"DailyPick",
|
||||
"bs_put_delta",
|
||||
"bs_put_price",
|
||||
"daily_picks",
|
||||
"estimate_credit_eth",
|
||||
"find_strike_for_delta",
|
||||
"monday_picks",
|
||||
"normal_cdf",
|
||||
"run_backtest",
|
||||
"simulate_entry_filters",
|
||||
@@ -184,41 +184,40 @@ def estimate_credit_eth(
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MondayPick:
|
||||
"""Indice di un tick "Monday 14:00 UTC" nella time-series."""
|
||||
class DailyPick:
|
||||
"""Indice di un tick "daily h:00 UTC" nella time-series."""
|
||||
|
||||
timestamp: datetime
|
||||
snapshot: MarketSnapshotRecord
|
||||
|
||||
|
||||
def monday_picks(
|
||||
def daily_picks(
|
||||
snapshots: list[MarketSnapshotRecord],
|
||||
*,
|
||||
weekday: int = 0, # Monday
|
||||
hour_utc: int = 14,
|
||||
asset: str = "ETH",
|
||||
) -> list[MondayPick]:
|
||||
"""Estrae i tick più vicini a "Monday h:00 UTC" per ogni settimana.
|
||||
) -> list[DailyPick]:
|
||||
"""Estrae un tick per giorno all'ora ``hour_utc``.
|
||||
|
||||
``snapshots`` deve essere ordinato per timestamp ascending. Per ogni
|
||||
occorrenza di ``weekday + hour_utc`` (es. lun 14:00) presa l'unica
|
||||
riga ETH che la copre. Settimane senza tick a quell'ora vengono
|
||||
saltate.
|
||||
Crypto è 24/7, quindi non gateiamo sul giorno della settimana: per
|
||||
ogni giorno di calendario presente in ``snapshots``, prendiamo la
|
||||
riga ETH che cade a ``hour_utc:00``. Giorni senza tick a quell'ora
|
||||
vengono saltati. ``snapshots`` deve essere ordinato per timestamp
|
||||
ascending.
|
||||
"""
|
||||
picks: list[MondayPick] = []
|
||||
seen_dates: set[tuple[int, int]] = set() # (iso_year, iso_week)
|
||||
picks: list[DailyPick] = []
|
||||
seen_dates: set[tuple[int, int, int]] = set() # (year, month, day)
|
||||
for snap in snapshots:
|
||||
if snap.asset.upper() != asset.upper():
|
||||
continue
|
||||
ts = snap.timestamp.astimezone(UTC)
|
||||
if ts.weekday() != weekday or ts.hour != hour_utc:
|
||||
if ts.hour != hour_utc:
|
||||
continue
|
||||
iso_y, iso_w, _ = ts.isocalendar()
|
||||
key = (iso_y, iso_w)
|
||||
key = (ts.year, ts.month, ts.day)
|
||||
if key in seen_dates:
|
||||
continue
|
||||
seen_dates.add(key)
|
||||
picks.append(MondayPick(timestamp=ts, snapshot=snap))
|
||||
picks.append(DailyPick(timestamp=ts, snapshot=snap))
|
||||
return picks
|
||||
|
||||
|
||||
@@ -231,9 +230,8 @@ def _entry_context_from_snapshot(
|
||||
"""Costruisce :class:`EntryContext` dal tick storico.
|
||||
|
||||
``None`` quando la riga non ha i campi minimi (spot, dvol, funding).
|
||||
Nel filtro questo si traduce in "skip della settimana" — è la
|
||||
stessa logica del live: un tick incompleto è meglio di un'entry
|
||||
al buio.
|
||||
Nel filtro questo si traduce in "skip del giorno" — è la stessa
|
||||
logica del live: un tick incompleto è meglio di un'entry al buio.
|
||||
"""
|
||||
if snap.dvol is None or snap.funding_perp_annualized is None:
|
||||
return None
|
||||
@@ -254,21 +252,21 @@ def _entry_context_from_snapshot(
|
||||
|
||||
@dataclass(frozen=True)
|
||||
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
|
||||
reasons: list[str]
|
||||
skipped_for_data: bool # True se il tick non aveva i campi minimi
|
||||
|
||||
|
||||
def simulate_entry_filters(
|
||||
picks: list[MondayPick],
|
||||
picks: list[DailyPick],
|
||||
cfg: StrategyConfig,
|
||||
*,
|
||||
capital_usd: Decimal,
|
||||
) -> 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`.
|
||||
Restituisce la lista degli esiti, una entry per pick.
|
||||
@@ -500,7 +498,7 @@ class BacktestReport(BaseModel):
|
||||
|
||||
|
||||
def _build_entry_from_pick(
|
||||
pick: MondayPick,
|
||||
pick: DailyPick,
|
||||
cfg: StrategyConfig,
|
||||
*,
|
||||
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:
|
||||
"""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.
|
||||
"""
|
||||
@@ -578,7 +577,7 @@ def _sharpe_annualized(pnls_usd: list[Decimal], capital_usd: Decimal) -> Decimal
|
||||
std = math.sqrt(var)
|
||||
if std == 0:
|
||||
return None
|
||||
sharpe = mean / std * math.sqrt(52)
|
||||
sharpe = mean / std * math.sqrt(120)
|
||||
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."""
|
||||
snapshots = sorted(snapshots, key=lambda s: s.timestamp)
|
||||
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)
|
||||
|
||||
# Tally skip reasons
|
||||
|
||||
@@ -435,7 +435,7 @@ def _compute_pl(
|
||||
- ``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
|
||||
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 è
|
||||
cappato a lookback_trades=5.
|
||||
|
||||
@@ -691,8 +691,13 @@ def _render_pl_panel(
|
||||
),
|
||||
)
|
||||
trades_per_year = col_d.slider(
|
||||
"Trade / anno (post-filtri)", 8, 30, value=18, step=1,
|
||||
help="52 lunedì × probabilità di superare i filtri (30–50%).",
|
||||
"Trade / anno (post-filtri)", 20, 200, value=110, step=5,
|
||||
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)
|
||||
@@ -746,13 +751,17 @@ def _render_pl_panel(
|
||||
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)
|
||||
with col_cons:
|
||||
_render_profile_card(
|
||||
"🛡️ Conservativa",
|
||||
cons_caps,
|
||||
cons,
|
||||
"_(golden config v1.2.0)_",
|
||||
f"_(golden config v{cons_version})_",
|
||||
features=feats_cons,
|
||||
metrics_base=cons_base if apply_features and any(feats_cons.values()) else None,
|
||||
)
|
||||
@@ -761,7 +770,7 @@ def _render_pl_panel(
|
||||
"🔥 Aggressiva",
|
||||
aggr_caps,
|
||||
aggr,
|
||||
"_(deroga §11, richiede paper trading)_",
|
||||
f"_(v{aggr_version} · deroga §11, richiede paper trading)_",
|
||||
features=feats_aggr,
|
||||
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."
|
||||
)
|
||||
|
||||
if win_rate < 0.72:
|
||||
if cons["annual_pl"] < 0 and aggr["annual_pl"] < 0:
|
||||
st.error(
|
||||
"**Win rate sotto 0.72: entrambi i profili perdono soldi.** "
|
||||
"Selling vol nudo è strutturalmente neutro qui. L'edge della "
|
||||
"strategia sono i FILTRI (dealer gamma>0, no macro, "
|
||||
"liquidation≠high, bias chiaro) che alzano il win rate sopra "
|
||||
"il 0.75. Senza filtri attivi nessuno dei due profili è "
|
||||
"viable."
|
||||
f"**Entrambi i profili in perdita** (cons {cons['apr']:+.1%}, "
|
||||
f"aggr {aggr['apr']:+.1%} APR). Selling vol nudo a win rate "
|
||||
f"{win_rate:.0%} è strutturalmente non profittevole. L'edge "
|
||||
"sono i FILTRI (dealer gamma>0, no macro, liquidation≠high, "
|
||||
"bias chiaro) e i miglioramenti F+D+A+IV-RV gate, che alzano "
|
||||
"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 =====
|
||||
|
||||
@@ -84,15 +84,15 @@ def is_paused(
|
||||
)
|
||||
|
||||
|
||||
def pause_until(now: datetime, weeks: int) -> datetime:
|
||||
"""Calcola la scadenza della pausa (``now + weeks``).
|
||||
def pause_until(now: datetime, days: int) -> datetime:
|
||||
"""Calcola la scadenza della pausa (``now + days``).
|
||||
|
||||
Estratto in funzione separata per facilitare i test e per ricordare
|
||||
che la pausa è espressa in **settimane** (la strategia ha cron
|
||||
settimanale; pause più corte non avrebbero modo di evitare una
|
||||
settimana di entry).
|
||||
che la pausa è espressa in **giorni** (la strategia ha cron
|
||||
giornaliero su crypto 24/7; pause sub-giornaliere non avrebbero
|
||||
modo di evitare un'entry).
|
||||
"""
|
||||
return now + timedelta(weeks=max(1, weeks))
|
||||
return now + timedelta(days=max(1, days))
|
||||
|
||||
|
||||
def evaluate_drawdown_breach(
|
||||
|
||||
@@ -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.
|
||||
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.
|
||||
Crypto è 24/7: la cadenza di candidatura non è gateata sulla
|
||||
settimana, sono i gate quantitativi a decidere se entrare o saltare
|
||||
il giorno. Pure orchestration over the existing core/clients/state
|
||||
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
|
||||
@@ -328,7 +330,7 @@ async def run_entry_cycle(
|
||||
eur_to_usd_rate: Decimal,
|
||||
now: datetime | None = None,
|
||||
) -> EntryCycleResult:
|
||||
"""Run one weekly entry evaluation cycle.
|
||||
"""Run one daily entry evaluation cycle.
|
||||
|
||||
The function is idempotent and side-effect aware: it persists the
|
||||
decision in the ``decisions`` table regardless of outcome and only
|
||||
@@ -406,7 +408,7 @@ async def run_entry_cycle(
|
||||
capital_usd=capital_usd,
|
||||
)
|
||||
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)
|
||||
try:
|
||||
with transaction(conn):
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"""Periodic option-chain snapshot collector (§13).
|
||||
|
||||
Fetches the Deribit option chain for every strike entro la finestra
|
||||
DTE configurata, prima del trigger entry settimanale (cron
|
||||
``55 13 * * MON`` di default). Persiste un quote per ogni strumento
|
||||
in ``option_chain_snapshots`` con un timestamp condiviso, che diventa
|
||||
il dato di base per:
|
||||
DTE configurata. Cadenza di default ``*/15 * * * *`` (allineata a
|
||||
``market_snapshot``: crypto è 24/7 e l'accumulo dataset deve essere
|
||||
continuo, non gateato sui rollover TradFi-style). Persiste un quote
|
||||
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``),
|
||||
* la calibrazione empirica dello skew premium e del credit/width
|
||||
|
||||
@@ -50,13 +50,13 @@ _log = logging.getLogger("cerbero_bite.runtime.orchestrator")
|
||||
Environment = Literal["testnet", "mainnet"]
|
||||
|
||||
# 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_HEALTH = "*/5 * * * *"
|
||||
_CRON_BACKUP = "0 * * * *"
|
||||
_CRON_MANUAL_ACTIONS = "*/1 * * * *"
|
||||
_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
|
||||
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
# 2× via "ETH + BTC" indicato in `📚 Strategia` è una **stima ex-ante**
|
||||
# di cosa otterresti DOPO quel lavoro di codice.
|
||||
|
||||
config_version: "1.3.0-aggressiva"
|
||||
config_hash: "e983e156bf0c270941765e7b9639a35fdc6de7b091076bf5a9b360e294e81e4c"
|
||||
config_version: "1.4.0-aggressiva"
|
||||
config_hash: "7a39214a7efd2861d22d465f6caf758fec84598775c3f01d922782d7f6f337b0"
|
||||
last_review: "2026-04-26"
|
||||
last_reviewer: "Adriano"
|
||||
|
||||
@@ -38,7 +38,7 @@ asset:
|
||||
exchange: "deribit"
|
||||
|
||||
entry:
|
||||
cron: "0 14 * * MON"
|
||||
cron: "0 14 * * *"
|
||||
skip_holidays_country: "IT"
|
||||
|
||||
capital_min_usd: "2880" # 4× del minimo conservativo (720)
|
||||
@@ -108,8 +108,8 @@ sizing:
|
||||
|
||||
# Le tre leve dominanti:
|
||||
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)
|
||||
max_concurrent_positions: 2 # era 1
|
||||
cap_aggregate_open_eur: "6400" # 8 posizioni concorrenti × 800 cap_per_trade (entry daily)
|
||||
max_concurrent_positions: 8 # era 2 (entry daily × DTE 14-21 × pass-rate)
|
||||
max_contracts_per_trade: 16 # era 4 → 4×
|
||||
|
||||
dvol_adjustment:
|
||||
@@ -150,7 +150,7 @@ auto_pause:
|
||||
enabled: true
|
||||
lookback_trades: 5
|
||||
max_drawdown_pct: "0.15"
|
||||
pause_weeks: 2
|
||||
pause_days: 14
|
||||
|
||||
execution:
|
||||
environment: "testnet"
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
# cerbero-bite config hash --file strategy.conservativa.yaml
|
||||
# e bumpare config_version.
|
||||
|
||||
config_version: "1.3.0-conservativa"
|
||||
config_hash: "900646beb1dd0a7bfaf553f76adb4b55004eff1f094585f779302131625919e8"
|
||||
config_version: "1.4.0-conservativa"
|
||||
config_hash: "b6af7b041508a67846eba5985e27e655526fe89105653f86bc88b8a4a437ac3a"
|
||||
last_review: "2026-04-26"
|
||||
last_reviewer: "Adriano"
|
||||
|
||||
@@ -25,7 +25,7 @@ asset:
|
||||
exchange: "deribit"
|
||||
|
||||
entry:
|
||||
cron: "0 14 * * MON"
|
||||
cron: "0 14 * * *"
|
||||
skip_holidays_country: "IT"
|
||||
|
||||
capital_min_usd: "720"
|
||||
@@ -84,8 +84,8 @@ sizing:
|
||||
kelly_fraction: "0.13"
|
||||
|
||||
cap_per_trade_eur: "200"
|
||||
cap_aggregate_open_eur: "1000"
|
||||
max_concurrent_positions: 1
|
||||
cap_aggregate_open_eur: "1000" # 3 posizioni × ~200 + headroom (entry daily, profilo conservativo)
|
||||
max_concurrent_positions: 3
|
||||
|
||||
max_contracts_per_trade: 4
|
||||
|
||||
@@ -119,7 +119,7 @@ auto_pause:
|
||||
enabled: false
|
||||
lookback_trades: 5
|
||||
max_drawdown_pct: "0.10"
|
||||
pause_weeks: 2
|
||||
pause_days: 14
|
||||
|
||||
execution:
|
||||
environment: "testnet"
|
||||
|
||||
+5
-5
@@ -6,8 +6,8 @@
|
||||
# config hash), and lands as a separate commit with the motivation in
|
||||
# the commit message.
|
||||
|
||||
config_version: "1.3.0"
|
||||
config_hash: "178a87467707d54d1ffef2d585a3a01be54de5ccc7e23493356eac47fd1c24d8"
|
||||
config_version: "1.4.0"
|
||||
config_hash: "22182814216190331e0b69b3bc99493e6d69cc813f7ed937394986eecc1f5d11"
|
||||
last_review: "2026-04-26"
|
||||
last_reviewer: "Adriano"
|
||||
|
||||
@@ -16,7 +16,7 @@ asset:
|
||||
exchange: "deribit"
|
||||
|
||||
entry:
|
||||
cron: "0 14 * * MON"
|
||||
cron: "0 14 * * *"
|
||||
skip_holidays_country: "IT"
|
||||
|
||||
capital_min_usd: "720"
|
||||
@@ -81,7 +81,7 @@ sizing:
|
||||
|
||||
cap_per_trade_eur: "200"
|
||||
cap_aggregate_open_eur: "1000"
|
||||
max_concurrent_positions: 1
|
||||
max_concurrent_positions: 5
|
||||
|
||||
max_contracts_per_trade: 4
|
||||
|
||||
@@ -121,7 +121,7 @@ auto_pause:
|
||||
enabled: false
|
||||
lookback_trades: 5
|
||||
max_drawdown_pct: "0.10"
|
||||
pause_weeks: 2
|
||||
pause_days: 14
|
||||
|
||||
execution:
|
||||
environment: "testnet" # testnet|mainnet — kill switch on broker mismatch
|
||||
|
||||
@@ -68,16 +68,16 @@ def test_is_paused_returns_false_when_until_in_past() -> None:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_pause_until_adds_weeks() -> None:
|
||||
until = pause_until(_NOW, weeks=2)
|
||||
assert until == _NOW + timedelta(weeks=2)
|
||||
def test_pause_until_adds_days() -> None:
|
||||
until = pause_until(_NOW, days=14)
|
||||
assert until == _NOW + timedelta(days=14)
|
||||
|
||||
|
||||
def test_pause_until_clamps_to_one_week_minimum() -> None:
|
||||
# weeks <= 0 deve cmq dare almeno 1 settimana di pausa, altrimenti
|
||||
# la cron settimanale potrebbe scattare comunque.
|
||||
assert pause_until(_NOW, weeks=0) == _NOW + timedelta(weeks=1)
|
||||
assert pause_until(_NOW, weeks=-3) == _NOW + timedelta(weeks=1)
|
||||
def test_pause_until_clamps_to_one_day_minimum() -> None:
|
||||
# days <= 0 deve cmq dare almeno 1 giorno di pausa, altrimenti
|
||||
# la cron giornaliera potrebbe scattare comunque.
|
||||
assert pause_until(_NOW, days=0) == _NOW + timedelta(days=1)
|
||||
assert pause_until(_NOW, days=-3) == _NOW + timedelta(days=1)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -90,7 +90,7 @@ def _cfg(**overrides: object) -> AutoPauseConfig:
|
||||
"enabled": True,
|
||||
"lookback_trades": 5,
|
||||
"max_drawdown_pct": Decimal("0.10"),
|
||||
"pause_weeks": 2,
|
||||
"pause_days": 14,
|
||||
}
|
||||
base.update(overrides)
|
||||
return AutoPauseConfig(**base) # type: ignore[arg-type]
|
||||
|
||||
+28
-28
@@ -11,9 +11,9 @@ from cerbero_bite.config import StrategyConfig, golden_config
|
||||
from cerbero_bite.core.backtest import (
|
||||
bs_put_delta,
|
||||
bs_put_price,
|
||||
daily_picks,
|
||||
estimate_credit_eth,
|
||||
find_strike_for_delta,
|
||||
monday_picks,
|
||||
normal_cdf,
|
||||
run_backtest,
|
||||
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:
|
||||
monday_2026_05_04 = datetime(2026, 5, 4, 14, 0, tzinfo=UTC)
|
||||
monday_2026_05_11 = datetime(2026, 5, 11, 14, 0, tzinfo=UTC)
|
||||
def test_daily_picks_extracts_one_per_calendar_day() -> None:
|
||||
day1 = datetime(2026, 5, 4, 14, 0, tzinfo=UTC)
|
||||
day2 = datetime(2026, 5, 5, 14, 0, tzinfo=UTC) # Tuesday: PICKED ora (crypto 24/7)
|
||||
snapshots = [
|
||||
_snap(ts=monday_2026_05_04),
|
||||
_snap(ts=monday_2026_05_04 + timedelta(minutes=15)), # not picked
|
||||
_snap(ts=monday_2026_05_11),
|
||||
_snap(ts=day1),
|
||||
_snap(ts=day1 + timedelta(minutes=15)), # stesso giorno, deduplicato
|
||||
_snap(ts=day2),
|
||||
]
|
||||
picks = monday_picks(snapshots)
|
||||
picks = daily_picks(snapshots)
|
||||
assert len(picks) == 2
|
||||
assert picks[0].timestamp == monday_2026_05_04
|
||||
assert picks[1].timestamp == monday_2026_05_11
|
||||
assert picks[0].timestamp == day1
|
||||
assert picks[1].timestamp == day2
|
||||
|
||||
|
||||
def test_monday_picks_skips_other_days_and_hours() -> None:
|
||||
def test_daily_picks_skips_other_hours() -> None:
|
||||
snapshots = [
|
||||
_snap(ts=datetime(2026, 5, 4, 13, 0, tzinfo=UTC)), # Monday 13:00
|
||||
_snap(ts=datetime(2026, 5, 5, 14, 0, tzinfo=UTC)), # Tuesday 14:00
|
||||
_snap(ts=datetime(2026, 5, 4, 13, 0, tzinfo=UTC)), # 13:00 → skipped
|
||||
_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:
|
||||
monday = datetime(2026, 5, 4, 14, 0, tzinfo=UTC)
|
||||
def test_daily_picks_filters_by_asset() -> None:
|
||||
day = datetime(2026, 5, 4, 14, 0, tzinfo=UTC)
|
||||
snapshots = [
|
||||
_snap(ts=monday, asset="BTC"),
|
||||
_snap(ts=monday, asset="ETH"),
|
||||
_snap(ts=day, asset="BTC"),
|
||||
_snap(ts=day, asset="ETH"),
|
||||
]
|
||||
picks = monday_picks(snapshots, asset="ETH")
|
||||
picks = daily_picks(snapshots, asset="ETH")
|
||||
assert len(picks) == 1
|
||||
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]
|
||||
]
|
||||
# Hack: build via real dataclass
|
||||
from cerbero_bite.core.backtest import MondayPick
|
||||
picks = [MondayPick(timestamp=monday, snapshot=snap)]
|
||||
from cerbero_bite.core.backtest import DailyPick
|
||||
picks = [DailyPick(timestamp=monday, snapshot=snap)]
|
||||
results = simulate_entry_filters(picks, cfg, capital_usd=Decimal("1500"))
|
||||
assert len(results) == 1
|
||||
assert results[0].accepted is True
|
||||
@@ -167,8 +167,8 @@ def test_simulate_entry_filters_rejects_dvol_out_of_band() -> None:
|
||||
cfg = golden_config()
|
||||
monday = datetime(2026, 5, 4, 14, 0, tzinfo=UTC)
|
||||
snap = _snap(ts=monday, dvol=20, funding=0.10) # below 35
|
||||
from cerbero_bite.core.backtest import MondayPick
|
||||
picks = [MondayPick(timestamp=monday, snapshot=snap)]
|
||||
from cerbero_bite.core.backtest import DailyPick
|
||||
picks = [DailyPick(timestamp=monday, snapshot=snap)]
|
||||
results = simulate_entry_filters(picks, cfg, capital_usd=Decimal("1500"))
|
||||
assert results[0].accepted is False
|
||||
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
|
||||
fetch_ok=False,
|
||||
)
|
||||
from cerbero_bite.core.backtest import MondayPick
|
||||
picks = [MondayPick(timestamp=monday, snapshot=incomplete)]
|
||||
from cerbero_bite.core.backtest import DailyPick
|
||||
picks = [DailyPick(timestamp=monday, snapshot=incomplete)]
|
||||
results = simulate_entry_filters(picks, cfg, capital_usd=Decimal("1500"))
|
||||
assert results[0].accepted is False
|
||||
assert results[0].skipped_for_data is True
|
||||
@@ -208,8 +208,8 @@ def _synthetic_year_of_snapshots(
|
||||
base = monday + timedelta(weeks=week)
|
||||
# Lunedì 14:00 è il pick
|
||||
rows.append(_snap(ts=base, spot=spot, dvol=dvol, funding=funding))
|
||||
# Tick intermedi che NON cadono di lunedì alle 14:00:
|
||||
# offset +1h così vengono ignorati da `monday_picks`.
|
||||
# Tick intermedi che NON cadono alle 14:00:
|
||||
# offset +1h (=15:00) così vengono ignorati da `daily_picks`.
|
||||
for d in (2, 8, 14, 19):
|
||||
rows.append(
|
||||
_snap(
|
||||
|
||||
@@ -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:
|
||||
"""The committed strategy.yaml validates with the recorded hash."""
|
||||
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.computed_hash == result.config.config_hash
|
||||
|
||||
|
||||
@@ -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:
|
||||
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.reason_if_zero is not None
|
||||
assert "position" in res.reason_if_zero.lower()
|
||||
|
||||
Reference in New Issue
Block a user