Phase 2: persistence + safety controls
Aggiunge la persistenza SQLite, l'audit log a hash chain, il kill switch coordinato e i CLI di gestione documentati in docs/05-data-model.md e docs/07-risk-controls.md. 197 test pass, 1 skipped (sqlite3 CLI mancante), copertura totale 97%. State (`state/`): - 0001_init.sql con positions, instructions, decisions, dvol_history, manual_actions, system_state. - db.py: connect con WAL + foreign_keys + transaction ctx, runner forward-only basato su PRAGMA user_version. - models.py: record Pydantic, Decimal preservato come TEXT. - repository.py: CRUD typed con singola connessione passata, cache aware, posizioni concorrenti. Safety (`safety/`): - audit_log.py: AuditLog append-only con SHA-256 chain e fsync, verify_chain riconosce ogni manomissione (payload, prev_hash, hash, JSON, separatori). - kill_switch.py: arm/disarm transazionali, idempotenti, accoppiati all'audit chain. Config (`config/loader.py` + `strategy.yaml`): - Loader YAML con deep-merge di strategy.local.yaml. - Verifica config_hash SHA-256 (riga config_hash esclusa). - File golden strategy.yaml + esempio override. Scripts: - dead_man.sh: watchdog shell indipendente da Python. - backup.py: VACUUM INTO orario con retention 30 giorni. CLI: - audit verify (exit 2 su tampering). - kill-switch arm/disarm/status su SQLite reale. - state inspect con tabella posizioni aperte. - config hash, config validate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
"""Pydantic record types mirroring the SQLite tables.
|
||||
|
||||
Every numeric column documented as ``NUMERIC`` in
|
||||
``state/migrations/0001_init.sql`` is exposed as :class:`decimal.Decimal`
|
||||
on the Python side. The repository layer is responsible for serialising
|
||||
to ``TEXT`` (using ``str``) when writing and parsing back when reading,
|
||||
so precision is never lost via ``float`` coercion.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Literal
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
__all__ = [
|
||||
"DecisionRecord",
|
||||
"DvolSnapshot",
|
||||
"InstructionRecord",
|
||||
"ManualAction",
|
||||
"PositionRecord",
|
||||
"PositionStatus",
|
||||
"SystemStateRecord",
|
||||
]
|
||||
|
||||
|
||||
PositionStatus = Literal[
|
||||
"proposed",
|
||||
"awaiting_fill",
|
||||
"open",
|
||||
"closing",
|
||||
"closed",
|
||||
"cancelled",
|
||||
]
|
||||
|
||||
|
||||
class PositionRecord(BaseModel):
|
||||
"""Row of the ``positions`` table."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
proposal_id: UUID
|
||||
spread_type: str
|
||||
asset: str = "ETH"
|
||||
expiry: datetime
|
||||
short_strike: Decimal
|
||||
long_strike: Decimal
|
||||
short_instrument: str
|
||||
long_instrument: str
|
||||
n_contracts: int
|
||||
spread_width_usd: Decimal
|
||||
spread_width_pct: Decimal
|
||||
credit_eth: Decimal
|
||||
credit_usd: Decimal
|
||||
max_loss_usd: Decimal
|
||||
spot_at_entry: Decimal
|
||||
dvol_at_entry: Decimal
|
||||
delta_at_entry: Decimal
|
||||
eth_price_at_entry: Decimal
|
||||
proposed_at: datetime
|
||||
opened_at: datetime | None = None
|
||||
closed_at: datetime | None = None
|
||||
close_reason: str | None = None
|
||||
debit_paid_eth: Decimal | None = None
|
||||
pnl_eth: Decimal | None = None
|
||||
pnl_usd: Decimal | None = None
|
||||
status: PositionStatus
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class InstructionRecord(BaseModel):
|
||||
"""Row of the ``instructions`` table."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
instruction_id: UUID
|
||||
proposal_id: UUID
|
||||
kind: Literal["open_combo", "close_combo"]
|
||||
payload_json: str
|
||||
sent_at: datetime
|
||||
acknowledged_at: datetime | None = None
|
||||
filled_at: datetime | None = None
|
||||
cancelled_at: datetime | None = None
|
||||
actual_fill_eth: Decimal | None = None
|
||||
actual_fees_eth: Decimal | None = None
|
||||
|
||||
|
||||
class DecisionRecord(BaseModel):
|
||||
"""Row of the ``decisions`` table.
|
||||
|
||||
``id`` is :class:`int` and may be ``None`` before the row has been
|
||||
inserted; the repository sets it after the auto-increment fires.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
id: int | None = None
|
||||
decision_type: Literal["entry_check", "exit_check", "kelly_recalib"]
|
||||
proposal_id: UUID | None = None
|
||||
timestamp: datetime
|
||||
inputs_json: str
|
||||
outputs_json: str
|
||||
action_taken: str | None = None
|
||||
notes: str | None = None
|
||||
|
||||
|
||||
class DvolSnapshot(BaseModel):
|
||||
"""Row of the ``dvol_history`` table."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
timestamp: datetime
|
||||
dvol: Decimal
|
||||
eth_spot: Decimal
|
||||
|
||||
|
||||
class ManualAction(BaseModel):
|
||||
"""Row of the ``manual_actions`` table."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
id: int | None = None
|
||||
kind: Literal[
|
||||
"approve_proposal",
|
||||
"reject_proposal",
|
||||
"force_close",
|
||||
"arm_kill",
|
||||
"disarm_kill",
|
||||
]
|
||||
proposal_id: UUID | None = None
|
||||
payload_json: str | None = None
|
||||
created_at: datetime
|
||||
consumed_at: datetime | None = None
|
||||
consumed_by: str | None = None
|
||||
result: str | None = None
|
||||
|
||||
|
||||
class SystemStateRecord(BaseModel):
|
||||
"""Singleton row of the ``system_state`` table."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
id: int = Field(default=1)
|
||||
kill_switch: int = 0
|
||||
kill_reason: str | None = None
|
||||
kill_at: datetime | None = None
|
||||
last_health_check: datetime
|
||||
last_kelly_calib: datetime | None = None
|
||||
config_version: str
|
||||
started_at: datetime
|
||||
Reference in New Issue
Block a user