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>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user