You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
94 lines
3.7 KiB
Python
94 lines
3.7 KiB
Python
"""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()
|