feat(trader): C-S3 scheduler(APScheduler定时+启动恢复live job)
This commit is contained in:
@@ -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:
|
||||
"""部署时注入真 step(akshare 拉当日 + 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 重建 job(spec §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)
|
||||
@@ -0,0 +1,35 @@
|
||||
"""scheduler 测试(mock APScheduler,spec §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
|
||||
Reference in New Issue
Block a user