42b0fbe1ab
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>
67 lines
1.8 KiB
Python
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
|