feat(trader): C-S3 scheduler(APScheduler定时+启动恢复live job)

This commit is contained in:
2026-07-07 12:08:32 +08:00
parent 14088eac13
commit cacdb5ae24
2 changed files with 99 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
"""模拟盘 C 实走调度(APScheduler 定时 + 启动恢复,spec §9.2)。
每日 20:30 触发 step(拉当日 bar + engine.step)。APScheduler 是 pip 依赖,
lazy import;本机无则单测 mock get_scheduler。
"""
import logging
import sqlite3
logger = logging.getLogger(__name__)
_scheduler = None
def get_scheduler():
global _scheduler
if _scheduler is None:
from apscheduler.schedulers.background import BackgroundScheduler
_scheduler = BackgroundScheduler()
return _scheduler
def start_scheduler():
s = get_scheduler()
if not s.running:
s.start()
return s
def register_paper_job(account_id: int, hour: int = 20, minute: int = 30,
job_func=None) -> str:
"""注册每日定时 job,返回 job_id(存 paper_accounts.scheduler_job_id)。"""
s = start_scheduler()
job_id = f"paper_{account_id}"
s.add_job(
job_func or _default_step,
"cron", # 字符串 trigger,避免函数内 import CronTrigger
hour=hour, minute=minute,
args=[account_id], id=job_id, replace_existing=True,
)
return job_id
def remove_paper_job(account_id: int) -> None:
s = get_scheduler()
job_id = f"paper_{account_id}"
try:
s.remove_job(job_id)
except Exception:
logger.warning("remove_job %s not found", job_id)
def _default_step(account_id: int) -> None:
"""部署时注入真 stepakshare 拉当日 + engine.step);默认 stub。"""
logger.info("paper account %s daily step (stub)", account_id)
def restore_live_jobs(db_path: str, job_func=None) -> int:
"""容器启动:遍历 status=running & mode=live 重建 jobspec §9.2 H-3)。"""
with sqlite3.connect(db_path) as conn:
rows = conn.execute(
"SELECT id FROM paper_accounts WHERE status='running' AND mode='live'"
).fetchall()
for (aid,) in rows:
register_paper_job(aid, job_func=job_func)
return len(rows)
+35
View File
@@ -0,0 +1,35 @@
"""scheduler 测试(mock APSchedulerspec §9.2)。"""
from unittest.mock import MagicMock
from sanguo_trader import scheduler
from sanguo_trader.persistence import (
init_db, save_account, update_account_status,
)
def test_register_paper_job(monkeypatch):
fake = MagicMock()
monkeypatch.setattr(scheduler, "start_scheduler", lambda: fake)
job_id = scheduler.register_paper_job(123, job_func=lambda a: None)
assert job_id == "paper_123"
fake.add_job.assert_called_once()
def test_remove_paper_job(monkeypatch):
fake = MagicMock()
monkeypatch.setattr(scheduler, "get_scheduler", lambda: fake)
scheduler.remove_paper_job(123)
fake.remove_job.assert_called_once_with("paper_123")
def test_restore_live_jobs_filters_running_live(monkeypatch, tmp_path):
db = str(tmp_path / "s.db")
init_db(db)
a_live = save_account(db, {"name": "t1", "mode": "live"})
a_replay = save_account(db, {"name": "t2", "mode": "replay"})
update_account_status(db, a_live, "running")
update_account_status(db, a_replay, "running")
fake = MagicMock()
monkeypatch.setattr(scheduler, "start_scheduler", lambda: fake)
n = scheduler.restore_live_jobs(db, job_func=lambda a: None)
assert n == 1 # 只 live & running