Files
Cerbero-Bite/src/cerbero_bite/runtime/scheduler.py
T
Adriano 42b0fbe1ab Phase 4: orchestrator + cycles auto-execute
Componente runtime/ che cabla core+clients+state+safety in un engine
autonomo notify-only: nessuna conferma manuale, ordini combo
piazzati direttamente quando le regole passano. 311 test pass,
copertura totale 94%, runtime/ 90%, mypy strict pulito, ruff clean.

Moduli:
- runtime/alert_manager.py: escalation tree
  LOW/MEDIUM/HIGH/CRITICAL → audit + Telegram + kill switch.
- runtime/dependencies.py: build_runtime() costruisce
  RuntimeContext con tutti i client MCP, repository, audit log,
  kill switch, alert manager.
- runtime/entry_cycle.py: flusso settimanale (snapshot parallelo
  spot/dvol/funding/macro/holdings/equity → validate_entry →
  compute_bias → options_chain → select_strikes →
  liquidity_gate → sizing_engine → combo_builder.build →
  place_combo_order → notify_position_opened).
- runtime/monitor_cycle.py: loop 12h con dvol_history per il
  return_4h, exit_decision.evaluate, close auto-execute.
- runtime/health_check.py: probe parallelo MCP + SQLite +
  environment match; 3 strikes consecutivi → kill switch HIGH.
- runtime/recovery.py: riconciliazione SQLite vs broker
  all'avvio; mismatch → kill switch CRITICAL.
- runtime/scheduler.py: AsyncIOScheduler builder con cron entry
  (lun 14:00), monitor (02/14), health (5min).
- runtime/orchestrator.py: façade boot() + run_entry/monitor/health
  + install_scheduler + run_forever, con env check vs strategy.

CLI:
- start: avvia engine bloccante (asyncio.run + scheduler).
- dry-run --cycle entry|monitor|health: esegue un singolo ciclo
  per debug/test in produzione.
- stop: documenta lo shutdown via SIGTERM al container.

Documentazione:
- docs/06-operational-flow.md riscritto per il modello
  notify-only auto-execute (no conferma manuale, no memory,
  no brain-bridge).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 00:03:45 +02:00

67 lines
1.8 KiB
Python

"""APScheduler bootstrap (``docs/06-operational-flow.md``).
Wraps :class:`AsyncIOScheduler` so the orchestrator can register the
documented cron jobs in one place. The scheduler is built but not
started; ``start()`` must be called from inside the running event
loop.
"""
from __future__ import annotations
import logging
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
__all__ = ["JobSpec", "build_scheduler"]
_log = logging.getLogger("cerbero_bite.runtime.scheduler")
@dataclass(frozen=True)
class JobSpec:
"""One row in the scheduler manifest."""
name: str
cron: str
coro_factory: Callable[[], Awaitable[None]]
def _parse_cron(expr: str) -> CronTrigger:
parts = expr.split()
if len(parts) != 5:
raise ValueError(f"cron must have 5 fields, got: {expr!r}")
minute, hour, day, month, day_of_week = parts
return CronTrigger(
minute=minute,
hour=hour,
day=day,
month=month,
day_of_week=day_of_week,
timezone="UTC",
)
def build_scheduler(jobs: list[JobSpec]) -> AsyncIOScheduler:
"""Return an :class:`AsyncIOScheduler` with all *jobs* registered.
The scheduler is *not* started — the caller is responsible for
invoking ``start()`` after constructing it on a running event loop.
"""
scheduler = AsyncIOScheduler(timezone="UTC")
for spec in jobs:
scheduler.add_job(
spec.coro_factory,
trigger=_parse_cron(spec.cron),
id=spec.name,
name=spec.name,
replace_existing=True,
coalesce=True,
misfire_grace_time=300,
)
_log.info("scheduled job %s with cron %s", spec.name, spec.cron)
return scheduler