docs: align 05/06/09/11 with implemented GUI Phases A–D
* docs/11-gui-streamlit.md — replaces the original spec with what was actually built: implementation status table, real page filenames (1_Status, 2_Audit, 3_Equity, 4_History, 5_Position), per-page inventory of implemented vs deferred sections, GUI ↔ engine table showing arm_kill/disarm_kill via manual_actions and the not_supported markers for force_close + approve/reject_proposal, consumer signature with cron */1, lock model clarified (no GUI lockfile), DoD updated with current state. * docs/05-data-model.md — manual_actions is no longer "pianificata": populated by gui/data_layer.py, drained by the manual_actions job; per-kind status table (arm/disarm OK, others not_supported). * docs/09-development-roadmap.md — Phase 4.5 marked implemented with per-task ✅/⏳ markers for the deferred items (auto-refresh, AppTest, force-close hook). * docs/06-operational-flow.md — adds Flusso 5b describing the manual_actions consumer pattern (enqueue → KillSwitch transition → audit log linkage). 360/360 tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+23
-10
@@ -152,27 +152,40 @@ CREATE TABLE dvol_history (
|
|||||||
### `manual_actions`
|
### `manual_actions`
|
||||||
|
|
||||||
Coda di azioni manuali generate dalla GUI Streamlit (vedi
|
Coda di azioni manuali generate dalla GUI Streamlit (vedi
|
||||||
`11-gui-streamlit.md`). Schema previsto in vista della Fase 4.5; al
|
`11-gui-streamlit.md`). La tabella è popolata dal layer
|
||||||
momento la GUI non è implementata e la tabella resta vuota.
|
`gui/data_layer.py` (`enqueue_arm_kill`, `enqueue_disarm_kill`) ed è
|
||||||
|
drenata dal job APScheduler `manual_actions`
|
||||||
|
(`runtime/manual_actions_consumer.consume_manual_actions`, cron
|
||||||
|
`*/1 * * * *`).
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
CREATE TABLE manual_actions (
|
CREATE TABLE manual_actions (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
kind TEXT NOT NULL, -- approve_proposal, reject_proposal,
|
kind TEXT NOT NULL, -- arm_kill, disarm_kill,
|
||||||
-- force_close, arm_kill, disarm_kill
|
-- force_close, approve_proposal, reject_proposal
|
||||||
proposal_id TEXT, -- NULL se l'azione non è legata a una proposta
|
proposal_id TEXT, -- NULL se l'azione non è legata a una proposta
|
||||||
payload_json TEXT, -- JSON con motivo, conferma typed, ecc.
|
payload_json TEXT, -- JSON con reason, conferma typed, ecc.
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
consumed_at TEXT, -- NULL = ancora da processare
|
consumed_at TEXT, -- NULL = ancora da processare
|
||||||
consumed_by TEXT,
|
consumed_by TEXT, -- "engine" quando applicata dal consumer
|
||||||
result TEXT
|
result TEXT -- "ok" / "not_supported" / "error: ..."
|
||||||
);
|
);
|
||||||
CREATE INDEX idx_manual_actions_unconsumed ON manual_actions(consumed_at);
|
CREATE INDEX idx_manual_actions_unconsumed ON manual_actions(consumed_at);
|
||||||
```
|
```
|
||||||
|
|
||||||
Le `manual_actions` non bypassano i risk control: il consumer
|
Stato implementativo per `kind`:
|
||||||
(quando esisterà) applicherà gli stessi check di
|
|
||||||
`safety.system_healthy()` prima di eseguire.
|
| `kind` | Implementato | Effetto |
|
||||||
|
|---|---|---|
|
||||||
|
| `arm_kill` | ✅ | `KillSwitch.arm(reason, source="manual_gui")` |
|
||||||
|
| `disarm_kill` | ✅ | `KillSwitch.disarm(reason, source="manual_gui")` |
|
||||||
|
| `force_close` | ⏳ | Marcato `result="not_supported"` finché l'orchestrator non espone `handle_force_close` |
|
||||||
|
| `approve_proposal` / `reject_proposal` | ⏳ | Idem |
|
||||||
|
|
||||||
|
Le `manual_actions` **non** bypassano i risk control: ogni azione di
|
||||||
|
kill switch passa dalla classe `KillSwitch`, che valida lo stato e
|
||||||
|
appende l'evento corrispondente alla audit chain. La typed
|
||||||
|
confirmation lato GUI è gating prima dell'enqueue.
|
||||||
|
|
||||||
### `system_state`
|
### `system_state`
|
||||||
|
|
||||||
|
|||||||
@@ -154,6 +154,26 @@ Trigger: ogni 5 minuti.
|
|||||||
Il dead-man (`scripts/dead_man.sh`) sorveglia che `HEALTH_OK` venga
|
Il dead-man (`scripts/dead_man.sh`) sorveglia che `HEALTH_OK` venga
|
||||||
scritto: silenzio > 15 min → kill switch via SQLite e alert.
|
scritto: silenzio > 15 min → kill switch via SQLite e alert.
|
||||||
|
|
||||||
|
## Flusso 5b — Manual actions consumer
|
||||||
|
|
||||||
|
Trigger: cron `*/1 * * * *` (job APScheduler `manual_actions`).
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Mentre la coda ha righe non consumate:
|
||||||
|
- leggi `next_unconsumed_action` (oldest-first)
|
||||||
|
- dispatch per kind:
|
||||||
|
arm_kill → KillSwitch.arm(reason, source="manual_gui")
|
||||||
|
disarm_kill → KillSwitch.disarm(reason, source="manual_gui")
|
||||||
|
force_close / approve_proposal / reject_proposal → result="not_supported"
|
||||||
|
- mark_action_consumed con consumed_by="engine" e result
|
||||||
|
2. Latenza tipica end-to-end (enqueue da GUI → effetto): ≤ 60 sec.
|
||||||
|
```
|
||||||
|
|
||||||
|
Il consumer è il **canale unico** di scrittura dalla GUI verso il
|
||||||
|
runtime: ogni transizione del kill switch passa dalla classe
|
||||||
|
`KillSwitch` per mantenere SQLite e audit chain in lock-step. Vedi
|
||||||
|
`runtime/manual_actions_consumer.py` e `docs/11-gui-streamlit.md`.
|
||||||
|
|
||||||
## Flusso 6 — Recovery dopo crash
|
## Flusso 6 — Recovery dopo crash
|
||||||
|
|
||||||
All'avvio o dopo un riavvio del container:
|
All'avvio o dopo un riavvio del container:
|
||||||
|
|||||||
@@ -126,29 +126,38 @@ Definition of Done:
|
|||||||
- Engine può girare in `--dry-run` per 24h senza errori
|
- Engine può girare in `--dry-run` per 24h senza errori
|
||||||
- I log sono leggibili e completi
|
- I log sono leggibili e completi
|
||||||
|
|
||||||
## Fase 4.5 — GUI Streamlit (4 giorni)
|
## Fase 4.5 — GUI Streamlit (4 giorni) ✅ implementata
|
||||||
|
|
||||||
**Obiettivo:** dashboard locale per osservazione e azioni manuali. Spec
|
**Obiettivo:** dashboard locale per osservazione e azioni manuali. Spec
|
||||||
dettagliata in `11-gui-streamlit.md`.
|
dettagliata in `11-gui-streamlit.md`.
|
||||||
|
|
||||||
Tasks:
|
Implementata in quattro round (A–D):
|
||||||
|
|
||||||
1. Setup `gui/main.py` + sidebar nav + auto-refresh
|
1. ✅ `gui/main.py` + sidebar nav (auto-refresh attivo non cablato; il
|
||||||
2. Pagina Status (engine, capitale, MCP health, kill switch panel)
|
re-render Streamlit è sufficiente per la frequenza tipica)
|
||||||
3. Pagina Equity (curve, drawdown, monthly stats)
|
2. ✅ Pagina Status (engine state, kill switch panel con typed
|
||||||
4. Pagina Position (legs, payoff plotly, decision history, force-close)
|
confirmation, audit anchor, open positions)
|
||||||
5. Pagina History (filtri, KPI, export CSV)
|
3. ✅ Pagina Equity (cumulative P&L, drawdown, P&L distribution per
|
||||||
6. Pagina Audit (log live, verify chain, search)
|
close reason, per-month stats)
|
||||||
7. Tabella `manual_actions` + consumer job APScheduler nell'engine
|
4. ✅ Pagina Position (legs from entry snapshot, payoff plotly per
|
||||||
8. Test integration con `streamlit.testing.v1.AppTest`
|
bull_put/bear_call con annotazioni, decision history) — greche
|
||||||
|
live e force-close differiti
|
||||||
|
5. ✅ Pagina History (filtri window/reason/winners-losers, KPI strip,
|
||||||
|
CSV export)
|
||||||
|
6. ✅ Pagina Audit (live log stream, chain verify, event filter)
|
||||||
|
7. ✅ Consumer `runtime/manual_actions_consumer.py` con job APScheduler
|
||||||
|
`*/1` per arm/disarm (force_close = `not_supported` per ora)
|
||||||
|
8. ⏳ Test integration con `streamlit.testing.v1.AppTest`
|
||||||
|
|
||||||
Definition of Done:
|
Definition of Done — stato:
|
||||||
|
|
||||||
- `cerbero-bite gui` lancia la dashboard su `127.0.0.1:8765`
|
- ✅ `cerbero-bite gui` lancia la dashboard su `127.0.0.1:8765`
|
||||||
- Tutte le 5 pagine raggiungibili e popolate
|
- ✅ Tutte le 5 pagine raggiungibili e popolate
|
||||||
- Disarm da GUI loggato in audit chain ed effettivo entro 30 sec
|
- ✅ Disarm da GUI loggato in audit chain (`source="manual_gui"`) ed
|
||||||
- Force-close da GUI consumato dall'engine entro 30 sec
|
effettivo entro ~1 minuto
|
||||||
- Test integration su ogni pagina passing
|
- ⏳ Force-close da GUI: l'enqueue funziona ma l'orchestrator deve
|
||||||
|
ancora esporre `handle_force_close`
|
||||||
|
- ⏳ Test integration AppTest: non scritti
|
||||||
|
|
||||||
## Fase 5 — Reporting e UX (3-5 giorni)
|
## Fase 5 — Reporting e UX (3-5 giorni)
|
||||||
|
|
||||||
|
|||||||
+183
-134
@@ -46,129 +46,152 @@ uv run streamlit run src/cerbero_bite/gui/main.py \
|
|||||||
--browser.gatherUsageStats false
|
--browser.gatherUsageStats false
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Stato implementativo
|
||||||
|
|
||||||
|
La dashboard è stata costruita in quattro fasi incrementali:
|
||||||
|
|
||||||
|
| Fase | Contenuto | Stato |
|
||||||
|
|---|---|---|
|
||||||
|
| A | Status + Audit (osservazione di base) | ✅ |
|
||||||
|
| B | Equity + History (analitica + export CSV) | ✅ |
|
||||||
|
| C | Position drilldown con payoff plotly + decision history | ✅ |
|
||||||
|
| D | Kill-switch arm/disarm dalla dashboard via coda `manual_actions` | ✅ |
|
||||||
|
|
||||||
|
Per scelta di scope, restano fuori dalla prima iterazione: force-close
|
||||||
|
dalla GUI (richiede un hook `handle_force_close` nell'orchestrator),
|
||||||
|
approve/reject di una proposta (il bot decide autonomamente, non c'è un
|
||||||
|
flusso di proposta in attesa) e auto-refresh attivo via
|
||||||
|
`st_autorefresh`. Il consumer di `manual_actions` riconosce già i
|
||||||
|
`kind` corrispondenti e li archivia con `result="not_supported"`
|
||||||
|
finché i flussi non saranno cablati.
|
||||||
|
|
||||||
## Layout cartelle
|
## Layout cartelle
|
||||||
|
|
||||||
```
|
```
|
||||||
src/cerbero_bite/gui/
|
src/cerbero_bite/gui/
|
||||||
├── __init__.py
|
├── __init__.py
|
||||||
├── main.py # entry point streamlit, sidebar nav
|
├── main.py # entry Streamlit, sidebar, home
|
||||||
├── pages/
|
├── data_layer.py # wrapper read-only + write helpers
|
||||||
│ ├── 1_📊_status.py
|
└── pages/
|
||||||
│ ├── 2_📈_equity.py
|
├── 1_📊_Status.py # health, kill switch, audit anchor
|
||||||
│ ├── 3_💼_position.py
|
├── 2_🔍_Audit.py # log stream + chain integrity
|
||||||
│ ├── 4_📜_history.py
|
├── 3_📈_Equity.py # cumulative P&L + drawdown
|
||||||
│ └── 5_🔍_audit.py
|
├── 4_📜_History.py # closed trades + KPI + CSV
|
||||||
├── components/
|
└── 5_💼_Position.py # drilldown + payoff plotly
|
||||||
│ ├── kill_switch_panel.py
|
|
||||||
│ ├── mcp_health_grid.py
|
|
||||||
│ ├── pending_proposal_card.py
|
|
||||||
│ ├── payoff_chart.py
|
|
||||||
│ └── greeks_panel.py
|
|
||||||
└── data_layer.py # wrapper read-only verso state.repository
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
I componenti riutilizzabili descritti nello spec originale
|
||||||
|
(`kill_switch_panel`, `payoff_chart`, ecc.) non sono stati estratti in
|
||||||
|
file separati: ogni pagina è autonoma e tiene la propria UI inline,
|
||||||
|
così l'evoluzione resta locale al singolo file. La promozione a
|
||||||
|
componenti separati è giustificata solo se più pagine condividono lo
|
||||||
|
stesso widget — al momento non è il caso.
|
||||||
|
|
||||||
## Pagine
|
## Pagine
|
||||||
|
|
||||||
### 1. 📊 Status (home)
|
### 1. 📊 Status (home)
|
||||||
|
|
||||||
Vista a colpo d'occhio dello stato corrente.
|
Stato corrente e controlli sul kill switch.
|
||||||
|
|
||||||
Sezioni:
|
Sezioni implementate:
|
||||||
|
|
||||||
- **Engine status**: badge verde/giallo/rosso (running/degraded/killed),
|
- **Engine status banner** colorato in base alla health derivata dalla
|
||||||
uptime, ultimo health check, kill_switch state, kill_reason se armato.
|
combinazione `system_state.kill_switch` + età di `last_health_check`
|
||||||
- **Capitale**: equity corrente da `cerbero-portfolio` (cache ultimo
|
(`running`/`degraded`/`stopped`/`killed`/`unknown`).
|
||||||
valore noto + timestamp), variazione % vs giorno prima, vs settimana,
|
- **Top metric tiles**: posizioni aperte, età ultimo health check,
|
||||||
vs mese.
|
`started_at`, `config_version`.
|
||||||
- **Posizione attiva**: card con riepilogo (proposal_id, expiry, credit,
|
- **Kill switch controls**: form arm/disarm con typed confirmation
|
||||||
P&L unrealized stimato, days_to_expiry) o "nessuna posizione aperta".
|
(`"yes I am sure"`) + reason obbligatoria. La submission scrive
|
||||||
- **MCP health grid**: 8 box, uno per server, con latenza ms e semaforo.
|
un'azione in `manual_actions`; il consumer la applica entro un minuto.
|
||||||
- **Pending action**: se l'engine ha una proposta in attesa di conferma
|
- **Pending manual actions**: tabella delle azioni in coda non ancora
|
||||||
e il timeout Telegram è scaduto, qui appare una card con `Approve`/`Reject`.
|
consumate (visibile solo se la coda è non vuota).
|
||||||
Effetto: la decisione viene scritta in coda e il decision orchestrator
|
- **Audit anchor**: hash chain head persistito in `system_state`.
|
||||||
la legge al prossimo health-check.
|
- **Open positions table**: spread type, contracts, credit, max loss,
|
||||||
- **Big buttons**: `🟢 Disarm` / `🔴 Arm Kill Switch` (con conferma
|
strikes, status, opened/expiry.
|
||||||
typed `"yes I am sure"`).
|
|
||||||
|
|
||||||
Auto-refresh: 5 secondi.
|
Sezioni non ancora implementate rispetto allo spec originale: capitale
|
||||||
|
con variazioni %, MCP health grid (i probe sono fatti dall'engine e
|
||||||
|
visibili in audit), pending-proposal card. Il refresh automatico è
|
||||||
|
manuale (la pagina si aggiorna alla navigazione o al re-render
|
||||||
|
spontaneo di Streamlit).
|
||||||
|
|
||||||
### 2. 📈 Equity
|
### 2. 🔍 Audit
|
||||||
|
|
||||||
Grafico storia capitale e analitica.
|
Live log stream + verifica integrità della hash chain.
|
||||||
|
|
||||||
Sezioni:
|
Sezioni implementate:
|
||||||
|
|
||||||
- **Equity curve** (line chart): capitale nel tempo dall'inizio del
|
- **Chain integrity verify**: bottone che richiama `verify_chain` e
|
||||||
tracking. Risoluzione giornaliera. Sovrapposizione opzionale:
|
riporta numero di entries verificate o l'errore di mismatch.
|
||||||
- banda Monte Carlo P5/P50/P95 (statica, dal documento)
|
- **Filtri**: limit (10–500) + event filter (auto-popolato dagli event
|
||||||
- DVOL nel tempo (asse Y secondario)
|
effettivamente presenti nella tail).
|
||||||
- eventi macro (vertical lines sui giorni FOMC/CPI)
|
- **Event-count strip**: `Counter` dei tipi di evento nella finestra.
|
||||||
- **Drawdown rolling** (sotto curve): area chart del DD% corrente.
|
- **Tail table**: timestamp, event, payload JSON canonico, hash
|
||||||
- **P&L distribution** (histogram): trade chiusi raggruppati per outcome
|
abbreviato — newest-first.
|
||||||
(profit_take, stop_loss, vol_stop, time_stop, ecc.).
|
|
||||||
- **Tabella mensile**: per ogni mese — n trade, win rate, P&L, max DD.
|
|
||||||
|
|
||||||
Filtri: range temporale, asset (solo ETH per ora).
|
### 3. 📈 Equity
|
||||||
|
|
||||||
Auto-refresh: 30 secondi (cambia raramente).
|
Curva P&L cumulato e analitica trade chiusi.
|
||||||
|
|
||||||
### 3. 💼 Position
|
Sezioni implementate:
|
||||||
|
|
||||||
Drill-down sulla posizione attualmente aperta (se esiste).
|
- **KPI strip**: closed trades, win rate, total P&L, edge per trade,
|
||||||
|
max drawdown (USD + %).
|
||||||
|
- **Cumulative P&L** (Plotly): riempito a zero, con linea zero di
|
||||||
|
riferimento.
|
||||||
|
- **Drawdown** (Plotly area chart, asse invertito).
|
||||||
|
- **P&L distribution by close reason**: istogrammi Plotly sovrapposti
|
||||||
|
con conteggio trades per reason in metric tiles.
|
||||||
|
- **Per-month stats**: tabella aggregata UTC (mese, n trade, vincitori,
|
||||||
|
win rate, P&L totale, P&L medio).
|
||||||
|
|
||||||
Sezioni:
|
Window picker: All time, last 30/90 giorni, year-to-date. Banda Monte
|
||||||
|
Carlo, overlay DVOL e linee eventi macro non sono ancora implementati.
|
||||||
- **Header**: proposal_id, opened_at, expiry, days_left, status.
|
|
||||||
- **Legs table**: instrument, side, size, mid corrente, delta,
|
|
||||||
theta, vega — refresh periodico via `clients.deribit`.
|
|
||||||
- **Greche aggregate**: delta/theta/vega netti.
|
|
||||||
- **Payoff diagram** (plotly): P&L vs spot ETH a scadenza, con
|
|
||||||
breakeven, max profit, max loss, spot corrente come marker.
|
|
||||||
- **Decision history**: tabella con tutte le `decisions` di tipo
|
|
||||||
`exit_check` per questa posizione, in ordine cronologico, con
|
|
||||||
outcome HOLD / CLOSE_*.
|
|
||||||
- **Distance metrics**: short strike a `X% OTM`, delta corrente,
|
|
||||||
distanza in sigma.
|
|
||||||
- **Force close** (collapsibile): typed confirmation + reason field.
|
|
||||||
Su submit: scrive in coda azione `manual_close`, l'engine la consuma
|
|
||||||
al prossimo monitor cycle.
|
|
||||||
|
|
||||||
Auto-refresh: 10 secondi.
|
|
||||||
|
|
||||||
### 4. 📜 History
|
### 4. 📜 History
|
||||||
|
|
||||||
Storico trade chiusi.
|
Storico trade chiusi con filtri ed esportazione.
|
||||||
|
|
||||||
Sezioni:
|
Sezioni implementate:
|
||||||
|
|
||||||
- **Filtri**: range temporale, outcome (multiselect), P&L > 0 / < 0 / tutti.
|
- **Window picker**: All time, last 7/30/90 giorni, year-to-date.
|
||||||
- **Tabella trade chiusi** (`st.dataframe` sortable): proposal_id,
|
- **Filtri di dettaglio**: multiselect su `close_reason`, radio
|
||||||
opened_at, closed_at, expiry, n_contracts, credit_usd, debit_paid_usd,
|
vincitori/perdenti/tutti.
|
||||||
pnl_usd, outcome, days_held.
|
- **KPI strip a sei tile**: trades, win rate, total P&L, avg win,
|
||||||
- **KPI strip**: n trade, win rate, avg win, avg loss, edge per trade,
|
avg loss, edge per trade.
|
||||||
edge cumulato.
|
- **Tabella trade chiusi**: proposal_id (short), spread type, asset,
|
||||||
- **Confronto Monte Carlo**: side-by-side delle metriche reali vs
|
contracts, strikes, credit/max_loss, P&L, close_reason, days_held,
|
||||||
attese da simulazione, con delta in %.
|
opened/closed/expiry.
|
||||||
- **Export CSV**: bottone download per uso fiscale.
|
- **CSV export**: download diretto via `st.download_button`.
|
||||||
|
|
||||||
Auto-refresh: manuale (button).
|
Confronto Monte Carlo side-by-side non ancora implementato.
|
||||||
|
|
||||||
### 5. 🔍 Audit
|
### 5. 💼 Position
|
||||||
|
|
||||||
Log e audit chain.
|
Drilldown su una posizione specifica (open o ultime 10 chiuse).
|
||||||
|
|
||||||
Sezioni:
|
Sezioni implementate:
|
||||||
|
|
||||||
- **Live log stream**: ultimi 100 eventi, filtro per `level` e `event`.
|
- **Position selector** con label `proposal_id · spread_type ·
|
||||||
Auto-refresh 5 sec.
|
short/long · status`. Supporta deep-link via query string
|
||||||
- **Audit chain status**: bottone `Verify`. Mostra "✅ chain integra
|
`?proposal_id=…`.
|
||||||
fino a 14.382 eventi" o "❌ tampering rilevato a evento N".
|
- **Header tiles**: status, spread, contracts, credit USD; caption con
|
||||||
- **Search**: ricerca testuale negli ultimi 30 giorni di log.
|
proposal_id pieno + opened/expiry.
|
||||||
- **Stats engine**: numero kill switch armati nell'ultimo mese, MCP
|
- **Distance metrics**: short strike OTM%, days-to-expiry, days-held,
|
||||||
failure count per server, average decision loop latency.
|
delta at entry, width % of spot.
|
||||||
- **Export log**: download `.jsonl.gz` per analisi forensica.
|
- **Legs table** (snapshot al momento dell'entry, non live): leg, instrument,
|
||||||
|
strike, side, size, delta. Una caption ricorda che mid e greche live
|
||||||
|
non sono fetchate dalla GUI.
|
||||||
|
- **Payoff at expiry** (Plotly): curva P&L con annotazioni per short
|
||||||
|
strike, long strike, breakeven, entry spot. Tile riassuntivi per
|
||||||
|
max profit, max loss, breakeven. Implementato per `bull_put` e
|
||||||
|
`bear_call`; gli iron condor cadono su una curva piatta (placeholder).
|
||||||
|
- **Decision history**: tabella delle righe `decisions` legate al
|
||||||
|
`proposal_id`, newest-first, con outputs JSON canonici.
|
||||||
|
|
||||||
Auto-refresh: manuale.
|
Le greche/mid live e il force-close manuale richiedono che l'engine
|
||||||
|
esponga rispettivamente uno snapshot persistito e l'hook
|
||||||
|
`handle_force_close` — fuori scope della prima iterazione.
|
||||||
|
|
||||||
## Comunicazione GUI ↔ Engine
|
## Comunicazione GUI ↔ Engine
|
||||||
|
|
||||||
@@ -177,53 +200,70 @@ MCP. Tutto passa via:
|
|||||||
|
|
||||||
| Azione GUI | Effetto |
|
| Azione GUI | Effetto |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Visualizzazione stato | Read da `state/repository.py` (SQLite) |
|
| Visualizzazione stato | Read da `state/repository.py` (SQLite) tramite `gui/data_layer.py` |
|
||||||
| Equity / storico | Read da SQLite + `data/log/*.jsonl` |
|
| Equity / storico | Read da SQLite (`positions` con `status='closed'`) + audit log |
|
||||||
| MCP health | Read da `state.system_state.last_health_check` (l'engine fa il check) |
|
| MCP health | Read indiretto da `system_state.last_health_check` (l'engine fa il probe) |
|
||||||
| **Disarm kill switch** | Write su `system_state` con `kill_switch=0`; l'engine al prossimo health check rileva e log `KILL_SWITCH_DISARMED` |
|
| **Disarm kill switch** | `enqueue_disarm_kill(reason)` → riga in `manual_actions` con `kind="disarm_kill"`; consumer chiama `KillSwitch.disarm` (audit `KILL_SWITCH_DISARMED`, `source="manual_gui"`) |
|
||||||
| **Arm kill switch** | Write su `system_state` con `kill_switch=1, kill_reason="manual via GUI"` |
|
| **Arm kill switch** | `enqueue_arm_kill(reason)` → riga `kind="arm_kill"`; consumer chiama `KillSwitch.arm` |
|
||||||
| **Force close** | Insert riga in tabella `manual_actions` (nuova) con `kind="force_close", proposal_id=...`; l'engine al prossimo monitor cycle la consuma |
|
| Force close | Pianificato: `kind="force_close"`. Oggi il consumer marca `result="not_supported"`; richiede l'hook `Orchestrator.handle_force_close` |
|
||||||
| **Approve pending proposal** | Insert riga in `manual_actions` con `kind="approve_proposal", proposal_id=...` |
|
| Approve / reject pending proposal | Pianificato: `kind="approve_proposal"` / `"reject_proposal"`. Stesso stato (non implementato lato orchestrator) |
|
||||||
|
|
||||||
**Nuova tabella SQLite** (`05-data-model.md` da estendere):
|
La GUI **non** scrive direttamente su `system_state`: ogni transizione
|
||||||
|
del kill switch passa dal consumer e dalla classe `KillSwitch`, così
|
||||||
|
SQLite e audit chain restano sincronizzati come per le transizioni
|
||||||
|
automatiche.
|
||||||
|
|
||||||
|
**Schema SQLite** (vedi `05-data-model.md`):
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
CREATE TABLE manual_actions (
|
CREATE TABLE manual_actions (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
kind TEXT NOT NULL, -- approve_proposal, reject_proposal, force_close, etc.
|
kind TEXT NOT NULL,
|
||||||
proposal_id TEXT,
|
proposal_id TEXT,
|
||||||
payload_json TEXT,
|
payload_json TEXT,
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
consumed_at TEXT, -- NULL = ancora da processare
|
consumed_at TEXT,
|
||||||
consumed_by TEXT,
|
consumed_by TEXT,
|
||||||
result TEXT
|
result TEXT
|
||||||
);
|
);
|
||||||
CREATE INDEX idx_manual_actions_unconsumed ON manual_actions(consumed_at);
|
CREATE INDEX idx_manual_actions_unconsumed ON manual_actions(consumed_at);
|
||||||
```
|
```
|
||||||
|
|
||||||
L'engine include un nuovo job APScheduler `every 30s`:
|
**Consumer**: `runtime/manual_actions_consumer.consume_manual_actions`.
|
||||||
|
Registrato come job APScheduler `manual_actions` con cron
|
||||||
|
`*/1 * * * *` (latenza ≤ 1 minuto, sufficiente per kill-switch). Il
|
||||||
|
consumer drena tutta la coda a ogni tick e per ogni azione setta
|
||||||
|
`consumed_at`, `consumed_by="engine"` e `result` (`"ok"`,
|
||||||
|
`"not_supported"` o `"error: …"`).
|
||||||
|
|
||||||
```python
|
```python
|
||||||
async def consume_manual_actions():
|
# src/cerbero_bite/runtime/manual_actions_consumer.py — sintesi
|
||||||
actions = state.fetch_unconsumed_manual_actions()
|
async def consume_manual_actions(ctx, *, now=None):
|
||||||
for a in actions:
|
while (action := ctx.repository.next_unconsumed_action(...)) is not None:
|
||||||
if a.kind == "force_close":
|
if action.kind == "arm_kill":
|
||||||
await orchestrator.handle_force_close(a.proposal_id, a.payload)
|
ctx.kill_switch.arm(reason=payload.get("reason"), source="manual_gui")
|
||||||
elif a.kind == "approve_proposal":
|
elif action.kind == "disarm_kill":
|
||||||
await orchestrator.handle_proposal_approved(a.proposal_id)
|
ctx.kill_switch.disarm(reason=payload.get("reason"), source="manual_gui")
|
||||||
# etc.
|
else:
|
||||||
state.mark_action_consumed(a.id, result="ok")
|
result = "not_supported"
|
||||||
|
ctx.repository.mark_action_consumed(...)
|
||||||
```
|
```
|
||||||
|
|
||||||
Le azioni write **non bypassano** i risk control: una `force_close` deve
|
Le azioni write **non bypassano** i risk control: la transizione passa
|
||||||
comunque passare dal `safety.system_healthy()` e da una conferma typed
|
sempre per `KillSwitch.arm/disarm`, che valida lo stato e logga in
|
||||||
nella GUI prima di essere scritta in coda.
|
audit. La typed confirmation (`"yes I am sure"`) è gating lato GUI
|
||||||
|
prima dell'enqueue.
|
||||||
|
|
||||||
## Lock e concorrenza
|
## Lock e concorrenza
|
||||||
|
|
||||||
- L'engine tiene `data/.lockfile` esclusivo.
|
- L'engine tiene `data/.lockfile` esclusivo via `runtime/lockfile.py`.
|
||||||
- La GUI tiene `data/.gui-lockfile` esclusivo (impedisce due tab/Streamlit aperti).
|
- La GUI **non** acquisisce un lock dedicato; più tab Streamlit
|
||||||
- Entrambi possono leggere SQLite (modalità WAL).
|
contemporanee sono possibili (sconsigliate ma non impedite). Il
|
||||||
|
vincolo single-writer su SQLite è preservato perché ogni write
|
||||||
|
passa dalla riga `manual_actions` (auto-increment) e dal consumer
|
||||||
|
dell'engine.
|
||||||
|
- Entrambi possono leggere SQLite (le connessioni sono in modalità
|
||||||
|
short-lived: aperte per chiamata e chiuse subito).
|
||||||
- Le `manual_actions` sono il **canale di scrittura** condiviso, con
|
- Le `manual_actions` sono il **canale di scrittura** condiviso, con
|
||||||
primary key auto-increment e flag `consumed_at` per consumo idempotente.
|
primary key auto-increment e flag `consumed_at` per consumo idempotente.
|
||||||
|
|
||||||
@@ -251,26 +291,35 @@ Per chiarezza:
|
|||||||
Telegram resta il canale primario; la GUI è canale di **fallback**
|
Telegram resta il canale primario; la GUI è canale di **fallback**
|
||||||
per quando Adriano è davanti al laptop e non al telefono.
|
per quando Adriano è davanti al laptop e non al telefono.
|
||||||
|
|
||||||
## Stima di sforzo
|
## Stima di sforzo (storica)
|
||||||
|
|
||||||
Inserita come **Fase 4.5** nella roadmap, tra Orchestrator e Reporting:
|
La Fase 4.5 è stata implementata in quattro round (A–D). Lo spec
|
||||||
|
originale stimava ~4 giorni ed è stato consegnato in linea con la
|
||||||
|
stima, con il caveat che `streamlit.testing.v1.AppTest` non è ancora
|
||||||
|
cablato (le pagine sono validate manualmente via smoke test HTTP) e
|
||||||
|
che force-close + approve/reject restano fuori scope.
|
||||||
|
|
||||||
| Task | Giorni |
|
| Task | Giorni stimati | Stato |
|
||||||
|---|---|
|
|---|---|---|
|
||||||
| Setup `gui/main.py` + sidebar nav + autorefresh | 0.5 |
|
| Setup `gui/main.py` + sidebar nav + autorefresh | 0.5 | ✅ (autorefresh non attivo) |
|
||||||
| Pagina Status + MCP health grid + kill_switch panel | 0.5 |
|
| Pagina Status + kill_switch panel | 0.5 | ✅ (MCP health grid non implementata) |
|
||||||
| Pagina Equity + drawdown + plot mensili | 0.5 |
|
| Pagina Equity + drawdown + plot mensili | 0.5 | ✅ |
|
||||||
| Pagina Position + payoff plotly + decision history | 1.0 |
|
| Pagina Position + payoff plotly + decision history | 1.0 | ✅ (greche live e force-close differiti) |
|
||||||
| Pagina History + filtri + export CSV | 0.5 |
|
| Pagina History + filtri + export CSV | 0.5 | ✅ |
|
||||||
| Pagina Audit + search log + verify chain | 0.5 |
|
| Pagina Audit + verify chain | 0.5 | ✅ (search e export gz differiti) |
|
||||||
| `manual_actions` table + consumer job APScheduler | 0.5 |
|
| `manual_actions` consumer + APScheduler | 0.5 | ✅ (arm/disarm; force_close = `not_supported`) |
|
||||||
| Test integration (Streamlit AppTest framework) | 0.5 |
|
| Test integration (Streamlit AppTest) | 0.5 | ⏳ |
|
||||||
| **Totale** | **~4 giorni** |
|
| **Totale stimato** | **~4 giorni** | |
|
||||||
|
|
||||||
Definition of Done:
|
Definition of Done — stato attuale:
|
||||||
|
|
||||||
- `cerbero-bite gui` lancia la dashboard
|
- ✅ `cerbero-bite gui` lancia la dashboard su `127.0.0.1:8765`
|
||||||
- Tutte le 5 pagine raggiungibili e popolate (anche con dati fake)
|
- ✅ Tutte le 5 pagine raggiungibili e popolate dai dati di runtime
|
||||||
- Disarm da GUI loggato in audit chain ed effettivo entro 30 sec
|
- ✅ Disarm da GUI loggato in audit chain (`source="manual_gui"`) ed
|
||||||
- Force-close da GUI consumato dall'engine entro 30 sec
|
effettivo entro ~1 minuto (cron `*/1`)
|
||||||
- Test integration con `streamlit.testing.v1.AppTest` per ogni pagina
|
- ⏳ Force-close da GUI: l'enqueue è possibile, ma l'orchestrator non
|
||||||
|
ha ancora `handle_force_close`; il consumer marca `result="not_supported"`
|
||||||
|
- ⏳ Test integration con `streamlit.testing.v1.AppTest`: non scritti
|
||||||
|
|
||||||
|
Le voci aperte sono follow-up isolati e non bloccano l'uso quotidiano
|
||||||
|
della dashboard come tableau d'observation.
|
||||||
|
|||||||
Reference in New Issue
Block a user