Phase 4 hardening: status CLI, lock file, backup job, hash enforce, pooling, real bias

Sei interventi mirati sui rischi operativi rilevati nell'audit
post-Fase 4. 317 test pass, mypy strict pulito, ruff clean.

1. status CLI: legge SQLite reale e mostra kill_switch, posizioni
   aperte, environment, config_version, last_health_check, started_at.
   Sostituisce il placeholder "phase 0 skeleton".

2. Lock file single-instance: runtime/lockfile.py acquisisce
   data/.lockfile via fcntl.flock al boot di run_forever; un secondo
   container fallisce subito con LockError.

3. Backup orario nello scheduler: nuovo job APScheduler 0 * * * *
   chiama scripts.backup.backup_database + prune_backups.

4. config_hash enforce su start: il CLI start verifica l'integrità
   del file (enforce_hash=True). Mismatch → exit 1 prima di toccare
   stato. dry-run resta enforce_hash=False per debug.

5. Connection pooling MCP: RuntimeContext espone un httpx.AsyncClient
   long-lived condiviso da tutti i wrapper (limits 20/10
   connections/keepalive). aclose() chiamato in run_forever finale.

6. Bias direzionale reale: deribit.historical_close +
   deribit.adx_14 popolano TrendContext con spot a 30 giorni e
   ADX(14) effettivi. Sblocca bull_put e bear_call. Quando i dati
   storici mancano l'engine emette alert MEDIUM e cade su no_entry
   in modo deterministico.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-28 00:15:28 +02:00
parent ca1e6379df
commit 411b747e93
11 changed files with 439 additions and 36 deletions
+54 -15
View File
@@ -80,14 +80,46 @@ def main(ctx: click.Context, log_dir: Path, log_level: str) -> None:
@main.command()
def status() -> None:
@click.option(
"--db",
type=click.Path(dir_okay=False, path_type=Path),
default=_DEFAULT_DB_PATH,
show_default=True,
)
def status(db: Path) -> None:
"""Print engine status snapshot."""
header = f"[bold cyan]Cerbero Bite[/bold cyan] v{__version__}"
if not db.exists():
console.print(
f"{header}\n"
"engine state: [yellow]never started[/yellow] "
"(state.sqlite missing)"
)
return
conn = connect_state(db)
try:
run_migrations(conn)
repo = Repository()
sys_state = repo.get_system_state(conn)
open_positions = repo.list_open_positions(conn)
finally:
conn.close()
if sys_state is None:
console.print(f"{header}\nengine state: [yellow]uninitialised[/yellow]")
return
armed = sys_state.kill_switch == 1
flag = "[red]ARMED[/red]" if armed else "[green]disarmed[/green]"
console.print(
f"[bold cyan]Cerbero Bite[/bold cyan] v{__version__}\n"
f"engine state: [yellow]idle[/yellow]\n"
f"kill_switch: [green]0 (disarmed)[/green]\n"
f"open positions: 0\n"
f"phase: 0 (skeleton)"
f"{header}\n"
f"kill_switch: {flag}"
f"{' reason=' + (sys_state.kill_reason or '?') if armed else ''}\n"
f"open positions: {len(open_positions)}\n"
f"config_version: {sys_state.config_version}\n"
f"started_at: {sys_state.started_at.isoformat()}\n"
f"last_health_check: {sys_state.last_health_check.isoformat()}"
)
@@ -145,8 +177,9 @@ def _build_orchestrator(
audit: Path,
environment: str,
eur_to_usd: float,
enforce_hash: bool = True,
) -> Orchestrator:
loaded = load_strategy(strategy_path, enforce_hash=False)
loaded = load_strategy(strategy_path, enforce_hash=enforce_hash)
token = load_token(path=token_file)
return make_orchestrator(
cfg=loaded.config,
@@ -170,14 +203,19 @@ def start(
eur_to_usd: float,
) -> None:
"""Start the engine main loop (scheduler + monitoring)."""
orch = _build_orchestrator(
strategy_path=strategy_path,
token_file=token_file,
db=db,
audit=audit,
environment=environment,
eur_to_usd=eur_to_usd,
)
try:
orch = _build_orchestrator(
strategy_path=strategy_path,
token_file=token_file,
db=db,
audit=audit,
environment=environment,
eur_to_usd=eur_to_usd,
enforce_hash=True,
)
except Exception as exc:
console.print(f"[red]boot aborted[/red]: {type(exc).__name__}: {exc}")
sys.exit(1)
console.print(
f"[bold cyan]Cerbero Bite[/bold cyan] starting "
f"(env={environment}, db={db}, audit={audit})"
@@ -213,6 +251,7 @@ def dry_run(
audit=audit,
environment=environment,
eur_to_usd=eur_to_usd,
enforce_hash=False,
)
async def _go() -> None: