实时计划接口加当日缓存:装配要三四十秒,PMS 页面一次加载拉几路就超时;键带真实数据日,默认十分钟,nocache=1 强制重算,重新生成计划时清空
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
85b025a9c5
commit
12a2ab159d
59
api.py
59
api.py
|
|
@ -15,6 +15,8 @@ cron 的 docker exec 构建/出计划照旧,互不影响。局域网内部服
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from fastapi import FastAPI, HTTPException, Request
|
from fastapi import FastAPI, HTTPException, Request
|
||||||
|
|
@ -59,18 +61,62 @@ def plan_dates(limit: int = 30):
|
||||||
for x in df["trade_date"]]}
|
for x in df["trade_date"]]}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 实时计划的进程内缓存(2026-09-09)。装配一次要三四十秒:读基座因果论断 20 秒、四类券商
|
||||||
|
# 事件 7.6 秒、事件日字段 6 秒(09-08 加的两路),PMS 页面一次加载要拉几路,累计超过它
|
||||||
|
# 60 秒的超时,人在页面上看到的就是"上游的选股计划读不到"。
|
||||||
|
# 同一数据日、同一参数的装配结果在一天里是确定的(用的全是昨收与昨夜的数据,盘中不变),
|
||||||
|
# 所以缓存是安全的:键带数据日与三个参数,过期时间可配,默认十分钟;nocache=1 强制重算。
|
||||||
|
# 计划文件每早重新生成,数据日一变缓存自然失效。
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
_PLAN_CACHE: dict = {}
|
||||||
|
_PLAN_CACHE_LOCK = threading.Lock()
|
||||||
|
PLAN_CACHE_SEC = int(os.environ.get("PLAN_CACHE_SEC", "600"))
|
||||||
|
_PLAN_CACHE_MAX = 8 # 不同参数组合最多留几份,防内存慢涨
|
||||||
|
|
||||||
|
|
||||||
|
def _plan_cached(date, top, obs_top, theme_cap, nocache: bool):
|
||||||
|
"""返回 (data, 命中与否)。data 是 plan.collect 的结果(已去掉 _full)。
|
||||||
|
|
||||||
|
键里放**真实数据日**而不是请求里的 date:不传 date 时数据日随构建更新,明早换日后
|
||||||
|
旧那一份必须立刻失效,不能等缓存到期。取最新数据日是一条毫秒级查询。"""
|
||||||
|
ds_key = date
|
||||||
|
if not ds_key:
|
||||||
|
try:
|
||||||
|
ds_key = plan._latest_date("t_factor_akg_score") # noqa: SLF001 —— 同仓自用
|
||||||
|
except Exception: # noqa: BLE001 —— 取不到就退回按请求参数缓存
|
||||||
|
ds_key = ""
|
||||||
|
key = (ds_key or "", int(top), int(obs_top), int(theme_cap))
|
||||||
|
now = time.time()
|
||||||
|
if not nocache and PLAN_CACHE_SEC > 0:
|
||||||
|
with _PLAN_CACHE_LOCK:
|
||||||
|
hit = _PLAN_CACHE.get(key)
|
||||||
|
if hit and now - hit[0] < PLAN_CACHE_SEC:
|
||||||
|
return hit[1], True
|
||||||
|
data = plan.collect(date, top, obs_top, theme_cap)
|
||||||
|
data.pop("_full", None)
|
||||||
|
if PLAN_CACHE_SEC > 0:
|
||||||
|
with _PLAN_CACHE_LOCK:
|
||||||
|
_PLAN_CACHE[key] = (now, data)
|
||||||
|
if len(_PLAN_CACHE) > _PLAN_CACHE_MAX:
|
||||||
|
for k in sorted(_PLAN_CACHE, key=lambda x: _PLAN_CACHE[x][0])[:-_PLAN_CACHE_MAX]:
|
||||||
|
_PLAN_CACHE.pop(k, None)
|
||||||
|
return data, False
|
||||||
|
|
||||||
|
|
||||||
@app.get("/plan")
|
@app.get("/plan")
|
||||||
def get_plan(request: Request, date: str | None = None, format: str = "json",
|
def get_plan(request: Request, date: str | None = None, format: str = "json",
|
||||||
top: int = 20, obs_top: int = 10, theme_cap: int = 5):
|
top: int = 20, obs_top: int = 10, theme_cap: int = 5, nocache: int = 0):
|
||||||
"""向下兼容承诺(2026-09-02 方案 2.7):main / observe 的装配、排序、裁剪与既有字段
|
"""向下兼容承诺(2026-09-02 方案 2.7):main / observe 的装配、排序、裁剪与既有字段
|
||||||
一字不动,每行只多联入判决类字段;顶层只新增 generated_at、plan_version、regime、
|
一字不动,每行只多联入判决类字段;顶层只新增 generated_at、plan_version、regime、
|
||||||
card_counts、candidates、watch、segments_pointed、snapshot,2026-09-03 再加 market
|
card_counts、candidates、watch、segments_pointed、snapshot,2026-09-03 再加 market
|
||||||
(环境段市场四项,与 regime 一样只从当日快照读,快照缺失为空字典)。PMS 按字段名取值、忽略未知键。"""
|
(环境段市场四项,与 regime 一样只从当日快照读,快照缺失为空字典)。PMS 按字段名取值、忽略未知键。"""
|
||||||
|
t0 = time.time()
|
||||||
try:
|
try:
|
||||||
data = plan.collect(date, top, obs_top, theme_cap)
|
data, cached = _plan_cached(date, top, obs_top, theme_cap, bool(nocache))
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
raise HTTPException(status_code=404, detail=str(e))
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
data.pop("_full", None)
|
data = dict(data) # 下面几个键按请求写,不污染缓存里那一份
|
||||||
ds = data["date"]
|
ds = data["date"]
|
||||||
reg = regime.read_from_snapshot(ds)
|
reg = regime.read_from_snapshot(ds)
|
||||||
data["snapshot"] = "present" if os.path.exists(regime.snapshot_path(ds)) else "missing"
|
data["snapshot"] = "present" if os.path.exists(regime.snapshot_path(ds)) else "missing"
|
||||||
|
|
@ -78,10 +124,11 @@ def get_plan(request: Request, date: str | None = None, format: str = "json",
|
||||||
"source": "当日快照无环境段(08:45 追加未跑或快照缺失)"}
|
"source": "当日快照无环境段(08:45 追加未跑或快照缺失)"}
|
||||||
data["market"] = regime.read_section(ds, "market") or {}
|
data["market"] = regime.read_section(ds, "market") or {}
|
||||||
_access.info("plan client=%s date=%s regime=%s generated_at=%s version=%s "
|
_access.info("plan client=%s date=%s regime=%s generated_at=%s version=%s "
|
||||||
"top=%s obs_top=%s theme_cap=%s",
|
"top=%s obs_top=%s theme_cap=%s cache=%s 耗时=%.1fs",
|
||||||
request.client.host if request.client else "-", ds,
|
request.client.host if request.client else "-", ds,
|
||||||
data["regime"].get("status"), data.get("generated_at"),
|
data["regime"].get("status"), data.get("generated_at"),
|
||||||
data.get("plan_version"), top, obs_top, theme_cap)
|
data.get("plan_version"), top, obs_top, theme_cap,
|
||||||
|
"hit" if cached else "miss", time.time() - t0)
|
||||||
if format == "md":
|
if format == "md":
|
||||||
return PlainTextResponse(plan.render_md(data),
|
return PlainTextResponse(plan.render_md(data),
|
||||||
media_type="text/markdown; charset=utf-8")
|
media_type="text/markdown; charset=utf-8")
|
||||||
|
|
@ -94,6 +141,8 @@ def refresh(date: str | None = None):
|
||||||
out = plan.generate(date)
|
out = plan.generate(date)
|
||||||
except SystemExit as e:
|
except SystemExit as e:
|
||||||
raise HTTPException(status_code=404, detail=str(e))
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
|
with _PLAN_CACHE_LOCK: # 重新生成了计划,实时接口的缓存整体作废
|
||||||
|
_PLAN_CACHE.clear()
|
||||||
return {"ok": True, "file": out}
|
return {"ok": True, "file": out}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue