From 6ff021fbf462c9356821ac89e0db12bc171b1db5 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 3 May 2026 16:21:16 +0000 Subject: [PATCH] =?UTF-8?q?feat(strategy):=20abbandono=20gating=20settiman?= =?UTF-8?q?ale=20=E2=80=94=20entry=20daily=2024/7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- README.md | 7 ++- docs/00-overview.md | 2 +- docs/01-strategy-rules.md | 20 ++++--- docs/02-architecture.md | 2 +- docs/05-data-model.md | 17 +++--- docs/06-operational-flow.md | 10 ++-- docs/07-risk-controls.md | 2 +- docs/08-testing-validation.md | 10 ++-- docs/09-development-roadmap.md | 4 +- docs/10-config-spec.md | 4 +- docs/13-strategia-spiegata.md | 45 +++++++------- src/cerbero_bite/cli.py | 8 +-- src/cerbero_bite/config/schema.py | 10 ++-- src/cerbero_bite/core/backtest.py | 59 +++++++++---------- src/cerbero_bite/gui/pages/7_📚_Strategia.py | 43 ++++++++++---- src/cerbero_bite/runtime/auto_pause.py | 12 ++-- src/cerbero_bite/runtime/entry_cycle.py | 16 ++--- .../runtime/option_chain_snapshot_cycle.py | 9 +-- src/cerbero_bite/runtime/orchestrator.py | 4 +- strategy.aggressiva.yaml | 12 ++-- strategy.conservativa.yaml | 12 ++-- strategy.yaml | 10 ++-- tests/unit/test_auto_pause.py | 18 +++--- tests/unit/test_backtest.py | 56 +++++++++--------- tests/unit/test_config_loader.py | 2 +- tests/unit/test_sizing_engine.py | 3 +- 26 files changed, 216 insertions(+), 181 deletions(-) diff --git a/README.md b/README.md index c08d2f3..c73ec8b 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/docs/00-overview.md b/docs/00-overview.md index 6b746ae..37bc6c8 100644 --- a/docs/00-overview.md +++ b/docs/00-overview.md @@ -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. diff --git a/docs/01-strategy-rules.md b/docs/01-strategy-rules.md index 2f0ed3b..ecbf390 100644 --- a/docs/01-strategy-rules.md +++ b/docs/01-strategy-rules.md @@ -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. diff --git a/docs/02-architecture.md b/docs/02-architecture.md index 610089d..878da16 100644 --- a/docs/02-architecture.md +++ b/docs/02-architecture.md @@ -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 diff --git a/docs/05-data-model.md b/docs/05-data-model.md index 43de405..b21e293 100644 --- a/docs/05-data-model.md +++ b/docs/05-data-model.md @@ -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 diff --git a/docs/06-operational-flow.md b/docs/06-operational-flow.md index 216f9e1..d4da5e1 100644 --- a/docs/06-operational-flow.md +++ b/docs/06-operational-flow.md @@ -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 diff --git a/docs/07-risk-controls.md b/docs/07-risk-controls.md index 071add0..583f241 100644 --- a/docs/07-risk-controls.md +++ b/docs/07-risk-controls.md @@ -54,7 +54,7 @@ cerbero-bite kill-switch disarm --reason "" \ 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) diff --git a/docs/08-testing-validation.md b/docs/08-testing-validation.md index cb678cb..cbfe28a 100644 --- a/docs/08-testing-validation.md +++ b/docs/08-testing-validation.md @@ -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 ``` diff --git a/docs/09-development-roadmap.md b/docs/09-development-roadmap.md index a27a540..2cda67e 100644 --- a/docs/09-development-roadmap.md +++ b/docs/09-development-roadmap.md @@ -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) diff --git a/docs/10-config-spec.md b/docs/10-config-spec.md index 25505b6..3c841a1 100644 --- a/docs/10-config-spec.md +++ b/docs/10-config-spec.md @@ -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) diff --git a/docs/13-strategia-spiegata.md b/docs/13-strategia-spiegata.md index 28a435e..6d15813 100644 --- a/docs/13-strategia-spiegata.md +++ b/docs/13-strategia-spiegata.md @@ -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 diff --git a/src/cerbero_bite/cli.py b/src/cerbero_bite/cli.py index e52010f..a5af0b4 100644 --- a/src/cerbero_bite/cli.py +++ b/src/cerbero_bite/cli.py @@ -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 diff --git a/src/cerbero_bite/config/schema.py b/src/cerbero_bite/config/schema.py index 6fd674a..1f34172 100644 --- a/src/cerbero_bite/config/schema.py +++ b/src/cerbero_bite/config/schema.py @@ -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 # --------------------------------------------------------------------------- diff --git a/src/cerbero_bite/core/backtest.py b/src/cerbero_bite/core/backtest.py index bc4a91f..9405d4c 100644 --- a/src/cerbero_bite/core/backtest.py +++ b/src/cerbero_bite/core/backtest.py @@ -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 diff --git a/src/cerbero_bite/gui/pages/7_📚_Strategia.py b/src/cerbero_bite/gui/pages/7_📚_Strategia.py index 1253f0c..713fbcd 100644 --- a/src/cerbero_bite/gui/pages/7_📚_Strategia.py +++ b/src/cerbero_bite/gui/pages/7_📚_Strategia.py @@ -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 ===== diff --git a/src/cerbero_bite/runtime/auto_pause.py b/src/cerbero_bite/runtime/auto_pause.py index fedd90c..c142cb3 100644 --- a/src/cerbero_bite/runtime/auto_pause.py +++ b/src/cerbero_bite/runtime/auto_pause.py @@ -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( diff --git a/src/cerbero_bite/runtime/entry_cycle.py b/src/cerbero_bite/runtime/entry_cycle.py index 9282dc9..a0ddfb8 100644 --- a/src/cerbero_bite/runtime/entry_cycle.py +++ b/src/cerbero_bite/runtime/entry_cycle.py @@ -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): diff --git a/src/cerbero_bite/runtime/option_chain_snapshot_cycle.py b/src/cerbero_bite/runtime/option_chain_snapshot_cycle.py index d2b6d0c..3ed2a89 100644 --- a/src/cerbero_bite/runtime/option_chain_snapshot_cycle.py +++ b/src/cerbero_bite/runtime/option_chain_snapshot_cycle.py @@ -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 diff --git a/src/cerbero_bite/runtime/orchestrator.py b/src/cerbero_bite/runtime/orchestrator.py index 3b064f5..2e21e8e 100644 --- a/src/cerbero_bite/runtime/orchestrator.py +++ b/src/cerbero_bite/runtime/orchestrator.py @@ -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 diff --git a/strategy.aggressiva.yaml b/strategy.aggressiva.yaml index c9127e6..4279c20 100644 --- a/strategy.aggressiva.yaml +++ b/strategy.aggressiva.yaml @@ -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" diff --git a/strategy.conservativa.yaml b/strategy.conservativa.yaml index 8d40f39..3fe2de4 100644 --- a/strategy.conservativa.yaml +++ b/strategy.conservativa.yaml @@ -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" diff --git a/strategy.yaml b/strategy.yaml index 2b123c2..0de9513 100644 --- a/strategy.yaml +++ b/strategy.yaml @@ -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 diff --git a/tests/unit/test_auto_pause.py b/tests/unit/test_auto_pause.py index 8fd8cc7..3b51520 100644 --- a/tests/unit/test_auto_pause.py +++ b/tests/unit/test_auto_pause.py @@ -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] diff --git a/tests/unit/test_backtest.py b/tests/unit/test_backtest.py index 7c3643d..b122671 100644 --- a/tests/unit/test_backtest.py +++ b/tests/unit/test_backtest.py @@ -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( diff --git a/tests/unit/test_config_loader.py b/tests/unit/test_config_loader.py index f780368..04e9a8e 100644 --- a/tests/unit/test_config_loader.py +++ b/tests/unit/test_config_loader.py @@ -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 diff --git a/tests/unit/test_sizing_engine.py b/tests/unit/test_sizing_engine.py index f5825c4..a06bf26 100644 --- a/tests/unit/test_sizing_engine.py +++ b/tests/unit/test_sizing_engine.py @@ -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()