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,
|
- **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
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) | 25–35% |
|
| Bias non chiaro (trend × funding discordi o entrambi neutri senza IC) | 25–35% |
|
||||||
| Macro entro DTE | 10–20% |
|
| Macro entro DTE | 10–20% |
|
||||||
| Funding o liquidation risk fuori soglia | 5–15% |
|
| 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
|
**Risultato netto: ~30–40% dei giorni finisce in entry effettiva
|
||||||
⇒ 15–25 trade / anno** (52 lunedì × 30–50%). Le altre settimane il
|
⇒ 110–145 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
|
||||||
> 1–2 candidate entry effettive. **Aspettare almeno 8 settimane**
|
> ≈ 9–12 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 (30–50%).",
|
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 =====
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user