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.
80 lines
3.0 KiB
Python
80 lines
3.0 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)
|
|
|
|
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()
|