Initial commit: stock analysis backend and prototype UI.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
144
backend/scheduler.py
Normal file
144
backend/scheduler.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""定时任务:收盘后自动入库。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
|
||||
import config
|
||||
import ingest
|
||||
import alerts
|
||||
import report
|
||||
import signals
|
||||
|
||||
_scheduler: BackgroundScheduler | None = None
|
||||
_lock = threading.Lock()
|
||||
_running = {"flag": False}
|
||||
|
||||
|
||||
def _job():
|
||||
with _lock:
|
||||
if _running["flag"]:
|
||||
return
|
||||
_running["flag"] = True
|
||||
try:
|
||||
ingest.run_daily_ingest()
|
||||
finally:
|
||||
_running["flag"] = False
|
||||
|
||||
|
||||
def trigger_async():
|
||||
"""手动触发一次入库(后台线程,不阻塞请求)。"""
|
||||
t = threading.Thread(target=_job, daemon=True)
|
||||
t.start()
|
||||
return {"started": True}
|
||||
|
||||
|
||||
def _job_all():
|
||||
with _lock:
|
||||
if _running["flag"]:
|
||||
return
|
||||
_running["flag"] = True
|
||||
try:
|
||||
ingest.ingest_quotes_all()
|
||||
finally:
|
||||
_running["flag"] = False
|
||||
|
||||
|
||||
def trigger_all_async():
|
||||
"""手动触发全市场回填(后台线程,耗时较长)。"""
|
||||
if _running["flag"]:
|
||||
return {"started": False, "msg": "已有任务在执行"}
|
||||
t = threading.Thread(target=_job_all, daemon=True)
|
||||
t.start()
|
||||
return {"started": True}
|
||||
|
||||
|
||||
def is_running():
|
||||
return _running["flag"]
|
||||
|
||||
|
||||
def _job_report():
|
||||
try:
|
||||
report.generate(push=True)
|
||||
except Exception as e:
|
||||
print("[report] generate error:", repr(e)[:160])
|
||||
|
||||
|
||||
def trigger_report_async(push=True):
|
||||
"""手动触发生成日报(后台线程)。"""
|
||||
t = threading.Thread(target=lambda: report.generate(push=push), daemon=True)
|
||||
t.start()
|
||||
return {"started": True}
|
||||
|
||||
|
||||
_sig_running = {"flag": False}
|
||||
|
||||
|
||||
def _job_signal_stats(sample=500, horizon=5):
|
||||
if _sig_running["flag"]:
|
||||
return
|
||||
_sig_running["flag"] = True
|
||||
try:
|
||||
signals.compute_signal_stats(sample_limit=sample, horizon=horizon)
|
||||
except Exception as e:
|
||||
print("[signals] compute error:", repr(e)[:160])
|
||||
finally:
|
||||
_sig_running["flag"] = False
|
||||
|
||||
|
||||
def trigger_signal_stats_async(sample=500, horizon=5):
|
||||
if _sig_running["flag"]:
|
||||
return {"started": False, "msg": "胜率回测进行中"}
|
||||
t = threading.Thread(target=lambda: _job_signal_stats(sample, horizon), daemon=True)
|
||||
t.start()
|
||||
return {"started": True}
|
||||
|
||||
|
||||
def _job_verify():
|
||||
try:
|
||||
signals.verify_predictions()
|
||||
except Exception as e:
|
||||
print("[predict] verify error:", repr(e)[:160])
|
||||
|
||||
|
||||
def start_scheduler():
|
||||
global _scheduler
|
||||
if _scheduler is not None:
|
||||
return _scheduler
|
||||
_scheduler = BackgroundScheduler(timezone="Asia/Shanghai")
|
||||
_scheduler.add_job(
|
||||
_job, CronTrigger(day_of_week="mon-fri", hour=config.INGEST_HOUR, minute=config.INGEST_MINUTE),
|
||||
id="daily_ingest", replace_existing=True, misfire_grace_time=3600,
|
||||
)
|
||||
_scheduler.add_job(
|
||||
_safe_check_alerts, IntervalTrigger(seconds=60),
|
||||
id="alert_check", replace_existing=True, max_instances=1,
|
||||
)
|
||||
# 收盘入库之后 10 分钟生成 AI 复盘日报并推送
|
||||
_rep_total = config.INGEST_HOUR * 60 + config.INGEST_MINUTE + 10
|
||||
_scheduler.add_job(
|
||||
_job_report, CronTrigger(day_of_week="mon-fri", hour=(_rep_total // 60) % 24, minute=_rep_total % 60),
|
||||
id="daily_report", replace_existing=True, misfire_grace_time=3600,
|
||||
)
|
||||
# 收盘后核验到期预测(实测准确率)
|
||||
_scheduler.add_job(
|
||||
_job_verify, CronTrigger(day_of_week="mon-fri", hour=(_rep_total // 60) % 24, minute=(_rep_total + 5) % 60),
|
||||
id="verify_pred", replace_existing=True, misfire_grace_time=3600,
|
||||
)
|
||||
# 每周六重算信号历史胜率
|
||||
_scheduler.add_job(
|
||||
_job_signal_stats, CronTrigger(day_of_week="sat", hour=9, minute=0),
|
||||
id="signal_stats", replace_existing=True, misfire_grace_time=7200,
|
||||
)
|
||||
_scheduler.start()
|
||||
return _scheduler
|
||||
|
||||
|
||||
def _safe_check_alerts():
|
||||
try:
|
||||
alerts.check_alerts()
|
||||
except Exception as e:
|
||||
print("[alert] check error:", repr(e)[:120])
|
||||
Reference in New Issue
Block a user