This commit is contained in:
2026-09-04 10:00:35 +08:00
parent 2735ff1fd9
commit 76b422320b
14 changed files with 611 additions and 218 deletions

View File

@@ -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

View File

@@ -23,7 +23,7 @@ SYSTEM_PROMPT = """你是 A 股选股条件解析器。把用户的自然语言
【indicator 数组】技术指标条件,元素字段:
- "indicator": 指标名白名单kdj_k / kdj_d / kdj_jKDJ 的 K/D/J 值、rsi、macd_dif / macd_dea / macd_histMACD 的 DIF/DEA/柱、ma收盘价均线、boll_upper / boll_mid / boll_lower布林轨道、zhixing_dkx知行多空线四条收盘价均线的均值、zhixing_trend知行短期趋势线、close收盘价、pct_chg日涨跌幅%
- "params": 指标参数可选默认KDJ {"n":9,"m1":3,"m2":3}RSI {"period":14}MACD {"fast":12,"slow":26,"signal":9}MA {"period":20}BOLL {"period":20,"std":2};知行多空线 {"m1":14,"m2":28,"m3":57,"m4":114}
- "params": 指标参数可选默认KDJ {"n":9,"m1":3,"m2":3}RSI {"period":14}MACD {"fast":12,"slow":26,"signal":9}MA {"period":20}BOLL {"period":20,"std":2};知行多空线 {"m1":14,"m2":28,"m3":57,"m4":114};知行趋势线无参数(固定算法),不要给它填 params
- "op": "gt" | "ge" | "lt" | "le" | "between"
- "value": 比较数值between 时为下界),"value2": between 上界
- "value_indicator": 可选。指标与指标比较时填另一指标名(同白名单),如 "DIF大于DEA" -> indicator=macd_dif, op=gt, value_indicator=macd_dea, value=0"股价在布林带下轨之下" -> indicator=close, op=lt, value_indicator=boll_lower, value=0
@@ -57,7 +57,11 @@ SYSTEM_PROMPT = """你是 A 股选股条件解析器。把用户的自然语言
示例4
输入:近一个月股价曾经站上知行多空线的股票
输出:{"indicator":[{"indicator":"close","op":"gt","value":0,"value_indicator":"zhixing_dkx","params":{"m1":14,"m2":28,"m3":57,"m4":114},"lookback":20,"match":"any"}],"exclude_st":true,"exclude_delisted":true,"exclude_bj":true}"""
输出:{"indicator":[{"indicator":"close","op":"gt","value":0,"value_indicator":"zhixing_dkx","params":{"m1":14,"m2":28,"m3":57,"m4":114},"lookback":20,"match":"any"}],"exclude_st":true,"exclude_delisted":true,"exclude_bj":true}
示例5
输入:今天股价在知行趋势线上方的股票
输出:{"indicator":[{"indicator":"close","op":"gt","value":0,"value_indicator":"zhixing_trend","lookback":1,"match":"all"}],"exclude_st":true,"exclude_delisted":true,"exclude_bj":true}"""
class ScreenerError(RuntimeError):
@@ -68,10 +72,14 @@ def _endpoint(base_url: str) -> str:
"""归一化 base_url -> 完整 chat/completions URL。
兼容多种写法DeepSeek/OpenAI 的 .../v1、智谱 GLM 的 .../v4、或直接给完整路径。
.../anthropicDeepSeek 的 Anthropic 协议端点)剥后缀走本模块的 OpenAI 兼容协议,
key 两端点通用。
"""
base = base_url.rstrip("/")
if base.endswith("/chat/completions"):
return base
if base.endswith("/anthropic"):
base = base[: -len("/anthropic")]
if not re.search(r"/v\d+$", base): # 未带版本段则补 /v1DeepSeek/OpenAI 惯例)
base += "/v1"
return f"{base}/chat/completions"
@@ -164,7 +172,7 @@ EVENT_SYSTEM_PROMPT = """你是 A 股事件回测参数解析器。用户描述
【entry.indicator 数组】入场信号条件(必填,至少 1 条),元素字段与白名单:
- "indicator": kdj_k / kdj_d / kdj_jKDJ 的 K/D/J 值、rsi、macd_dif / macd_dea / macd_histMACD 的 DIF/DEA/柱、ma收盘价均线、boll_upper / boll_mid / boll_lower布林轨道、zhixing_dkx知行多空线、zhixing_trend知行短期趋势线、close收盘价、pct_chg日涨跌幅%
- "params": 指标参数可选默认KDJ {"n":9,"m1":3,"m2":3}RSI {"period":14}MACD {"fast":12,"slow":26,"signal":9}MA {"period":20}BOLL {"period":20,"std":2};知行多空线 {"m1":14,"m2":28,"m3":57,"m4":114}
- "params": 指标参数可选默认KDJ {"n":9,"m1":3,"m2":3}RSI {"period":14}MACD {"fast":12,"slow":26,"signal":9}MA {"period":20}BOLL {"period":20,"std":2};知行多空线 {"m1":14,"m2":28,"m3":57,"m4":114};知行趋势线无参数(固定算法),不要给它填 params
- "op": "gt" | "ge" | "lt" | "le" | "between""value"between 时为下界)、"value2"(上界)
- "value_indicator": 指标与指标比较时填另一指标名(同白名单),如 "DIF 大于 DEA" -> indicator=macd_dif, op=gt, value_indicator=macd_dea, value=0
- "value_params": 比较对象指标参数不同时指定,如 "MA5 上穿 MA20" -> indicator=ma, params={"period":5}, op=gt, value_indicator=ma, value_params={"period":20}, value=0