"""ORB auto-scheduler standalone daemon. Runs independently of the web server so HMR/server restarts don't kill the scheduler. Started and stopped by the web API via subprocess management. Usage (usually via web API, but can be run manually): python -m apps.orb_trader.daemon --db-path data/paper/orb.db python -m apps.orb_trader.daemon --db-path data/paper/orb.db --sessions "Default,Session2" python -m apps.orb_trader.daemon --db-path data/paper/orb.db --dry-run """ from __future__ import annotations import argparse import asyncio import json import logging import time from pathlib import Path def main() -> None: parser = argparse.ArgumentParser(description="ORB auto-scheduler daemon") parser.add_argument("--sessions", default="", help="Comma-separated session names (empty = all active)") parser.add_argument("--db-path", required=True, help="Path to ORB SQLite DB") parser.add_argument("--dry-run", action="store_true", help="Dry run (no real orders)") args = parser.parse_args() # Route Python logger output to a file so engine logs are not lost # (daemon's stdout/stderr are /dev/null when started from the web server) log_file = Path(args.db_path).parent / "orb_daemon.log" file_handler = logging.FileHandler(log_file, encoding="utf-8") file_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")) logging.root.addHandler(file_handler) logging.root.setLevel(logging.INFO) # Mirror all ORB daemon logs to events.db so the Logs/Health UI can surface them. # Two channels: # 1. structlog sink processor — captures structured engine emits (orb_engine_*) # 2. EventsLoggingHandler — bridges plain stdlib log.info(...) lines try: from apps.web.services.events_store import EventsStore, EventsLoggingHandler from libs.common.logging import configure_logging EventsStore.get().ensure_schema() configure_logging(enable_events_sink=True) _events_handler = EventsLoggingHandler() _events_handler.setLevel(logging.INFO) logging.root.addHandler(_events_handler) except Exception: pass # events.db mirror is best-effort; don't break the daemon sessions = [s.strip() for s in args.sessions.split(",") if s.strip()] trigger_dir = Path(args.db_path).parent async def _run() -> None: from apps.web.orb_trading_service import ORBAutoScheduler sched = ORBAutoScheduler() sched.start(sessions=sessions, db_path=args.db_path, dry_run=args.dry_run) async def _poll_triggers() -> None: """Pick up run_session_now commands written by the web API.""" while True: await asyncio.sleep(3) for tf in sorted(trigger_dir.glob(".orb_trigger_*.json")): try: raw = tf.read_text() tf.unlink() cmd = json.loads(raw) # Ignore stale triggers (> 60 s old) if time.time() - cmd.get("ts", 0) > 60: continue if cmd.get("command") == "run_session_now": session = cmd.get("session", "") if session: asyncio.create_task(sched.run_session_now(session)) except Exception: pass asyncio.create_task(_poll_triggers()) try: if sched._task: await sched._task except asyncio.CancelledError: pass try: asyncio.run(_run()) except KeyboardInterrupt: pass if __name__ == "__main__": main()