提交
This commit is contained in:
@@ -5,6 +5,8 @@ pandas 逐股计算(复用 app/indicators,指标按 (族, 参数) 去重计
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Callable
|
||||
@@ -108,6 +110,10 @@ _IND_SUFFIX = {"kdj_k": "K", "kdj_d": "D", "kdj_j": "J",
|
||||
"boll_upper": "上轨", "boll_mid": "中轨", "boll_lower": "下轨",
|
||||
"zhixing_dkx": "多空线", "zhixing_trend": "趋势线"}
|
||||
|
||||
# 不消费族参数的指标(展示名不拼参数串):趋势线固定 EMA(EMA(C,10),10),
|
||||
# m1~m4 只属于多空线,拼上会误导(如「知行 趋势线(14,28,57,114)」)
|
||||
_IND_PARAMLESS = {"zhixing_trend"}
|
||||
|
||||
|
||||
def _family_of(indicator: str) -> str:
|
||||
fam = INDICATOR_FAMILY.get(indicator)
|
||||
@@ -139,6 +145,8 @@ def indicator_label(indicator: str, params: dict) -> str:
|
||||
fam = FAMILIES[_family_of(indicator)]
|
||||
suffix = _IND_SUFFIX.get(indicator)
|
||||
name = f"{fam.label} {suffix}".strip() if suffix else fam.label
|
||||
if indicator in _IND_PARAMLESS:
|
||||
return name
|
||||
return name + _params_str(params)
|
||||
|
||||
|
||||
@@ -352,8 +360,17 @@ def _item_from_row(row) -> dict:
|
||||
}
|
||||
|
||||
|
||||
async def run_screen(session: AsyncSession, conds: ScreenConditions, limit: int) -> dict:
|
||||
"""主流程:预筛 -> 逐股指标过滤 -> 组装 items + indicator_labels。"""
|
||||
async def run_screen_events(session: AsyncSession, conds: ScreenConditions, limit: int):
|
||||
"""事件流版主流程:预筛 -> 逐股指标过滤 -> 组装 items + indicator_labels。
|
||||
|
||||
逐步 yield 进度事件(含累计耗时 ms),最后 yield {"type": "result", "result": {...}},
|
||||
供 /screener/run 流式响应下发;阶段事件结构见 api.py 的 NDJSON 约定。
|
||||
"""
|
||||
t0 = time.perf_counter()
|
||||
|
||||
def _ev(**kw) -> dict:
|
||||
return {"ms": round((time.perf_counter() - t0) * 1000), **kw}
|
||||
|
||||
if not conds.indicator and not conds.snapshot:
|
||||
raise ValueError("筛选条件为空")
|
||||
|
||||
@@ -362,6 +379,7 @@ async def run_screen(session: AsyncSession, conds: ScreenConditions, limit: int)
|
||||
)
|
||||
if target_date is None:
|
||||
raise DataNotReadyError("全市场数据未同步:请先在选股页点击「同步市场数据」")
|
||||
yield _ev(type="stage", key="date", msg=f"数据基准日 {target_date:%Y-%m-%d}")
|
||||
|
||||
if any(c.field != "close" for c in conds.snapshot):
|
||||
if await session.scalar(select(func.max(DailySnapshot.trade_date))) is None:
|
||||
@@ -370,7 +388,9 @@ async def run_screen(session: AsyncSession, conds: ScreenConditions, limit: int)
|
||||
"市值/市盈率等条件无法使用,纯指标条件不受影响"
|
||||
)
|
||||
|
||||
yield _ev(type="stage", key="prefilter", msg="最新交易日截面预筛…")
|
||||
cand = await _prefilter(session, conds, target_date)
|
||||
yield _ev(type="candidates", count=len(cand), msg=f"预筛完成:{len(cand)} 只候选")
|
||||
|
||||
labels: dict[str, str] = {}
|
||||
for c in conds.indicator:
|
||||
@@ -388,6 +408,7 @@ async def run_screen(session: AsyncSession, conds: ScreenConditions, limit: int)
|
||||
else:
|
||||
# 圈定 K 线窗口:按 candles 全量交易日序列回溯 needed 根(不再受同步窗口限制)
|
||||
need = _max_needed_bars(conds)
|
||||
yield _ev(type="stage", key="bars", msg=f"载入 K 线窗口(每股回溯 {need} 根)…")
|
||||
dates_res = await session.execute(
|
||||
select(Candle.ts).where(Candle.timeframe == "1d").distinct()
|
||||
.order_by(Candle.ts.desc()).limit(need)
|
||||
@@ -395,8 +416,12 @@ async def run_screen(session: AsyncSession, conds: ScreenConditions, limit: int)
|
||||
min_date = min(r[0] for r in dates_res)
|
||||
bars = await _load_bars(session, cand["ts_code"].tolist(), target_date, min_date)
|
||||
cand_rows = {r["ts_code"]: r for _, r in cand.iterrows()}
|
||||
total = len(cand)
|
||||
|
||||
for ts_code, g in bars.groupby("ts_code", sort=False):
|
||||
for i, (ts_code, g) in enumerate(bars.groupby("ts_code", sort=False), 1):
|
||||
if i % 500 == 0 or i == total:
|
||||
yield _ev(type="progress", done=i, total=total)
|
||||
await asyncio.sleep(0) # 让出事件循环,流式响应即时下发
|
||||
row = cand_rows.get(ts_code)
|
||||
if row is None:
|
||||
continue
|
||||
@@ -415,13 +440,22 @@ async def run_screen(session: AsyncSession, conds: ScreenConditions, limit: int)
|
||||
ind_values[c.value_indicator] = None if s is None or s.iloc[-1] != s.iloc[-1] else float(s.iloc[-1])
|
||||
if ok:
|
||||
items.append(_item_from_row(row) | {"indicators": ind_values})
|
||||
yield _ev(type="stage", key="filter_done", msg=f"指标过滤完成:{len(items)} 只命中")
|
||||
|
||||
# 默认总市值降序(缺失排最后),截断 limit
|
||||
items.sort(key=lambda x: (x["total_mv"] is None, -(x["total_mv"] or 0)))
|
||||
return {
|
||||
yield _ev(type="result", result={
|
||||
"conditions": conds,
|
||||
"trade_date": target_date,
|
||||
"total": len(items),
|
||||
"items": items[:limit],
|
||||
"indicator_labels": labels,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
async def run_screen(session: AsyncSession, conds: ScreenConditions, limit: int) -> dict:
|
||||
"""兼容包装:跑完事件流,返回最终 result(一次性拿全量,无进度)。"""
|
||||
async for ev in run_screen_events(session, conds, limit):
|
||||
if ev["type"] == "result":
|
||||
return ev["result"]
|
||||
raise RuntimeError("run_screen_events 未产出 result") # pragma: no cover
|
||||
|
||||
Reference in New Issue
Block a user