From bec4e8149c0eca3dbdccc8cee798b0953663593a Mon Sep 17 00:00:00 2001 From: cirry <812852553@qq.com> Date: Wed, 2 Sep 2026 16:51:52 +0800 Subject: [PATCH] =?UTF-8?q?=E7=9F=A5=E8=A1=8C=E5=A4=9A=E7=A9=BA=E7=BA=BF?= =?UTF-8?q?=E6=8C=87=E6=A0=87=E6=8E=A5=E5=85=A5=E9=80=89=E8=82=A1=E4=B8=8E?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E5=9B=9E=E6=B5=8B=EF=BC=9B=E5=A4=A7=E7=9B=98?= =?UTF-8?q?=E6=80=BB=E8=A7=88=E8=B5=B0=20quicksync=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=EF=BC=9Bcandles=20=E6=89=B9=E9=87=8F=20upser?= =?UTF-8?q?t=20=E5=88=86=E6=89=B9=E9=98=B2=20asyncpg=20=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E8=B6=85=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/data/market_overview.py | 6 +++--- backend/app/indicators.py | 10 ++++++++++ backend/app/screener/engine.py | 12 +++++++++++- backend/app/screener/llm.py | 14 +++++++++----- backend/app/screener/market_sync.py | 27 +++++++++++++++------------ 5 files changed, 48 insertions(+), 21 deletions(-) diff --git a/backend/app/data/market_overview.py b/backend/app/data/market_overview.py index 033056a..45d2d81 100644 --- a/backend/app/data/market_overview.py +++ b/backend/app/data/market_overview.py @@ -60,10 +60,10 @@ def _d(v) -> str | None: def _get_pro(): if not settings.tushare_token: raise MarketOverviewError("未配置 TUSHARE_TOKEN,无法获取大盘行情(backend/.env)") - import tushare as ts + # 走统一入口:15000 积分档 token 只认 quicksync 镜像(直连 api.tushare.pro 会 40101) + from .tushare_provider import get_pro - ts.set_token(settings.tushare_token) - return ts.pro_api() + return get_pro() def _fetch_index_sync(pro, ts_code: str) -> pd.DataFrame: diff --git a/backend/app/indicators.py b/backend/app/indicators.py index 1c4cfde..b2da9fb 100644 --- a/backend/app/indicators.py +++ b/backend/app/indicators.py @@ -64,3 +64,13 @@ def ema2(close: pd.Series, span: int = 10) -> pd.Series: def avg_ma(close: pd.Series, periods: tuple[int, ...] = (10, 20, 30, 60)) -> pd.Series: """知行多空线:(MA(M1)+MA(M2)+MA(M3)+MA(M4))/4。""" return sum(ma(close, p) for p in periods) / len(periods) + + +def zhixing_dkx(close: pd.Series, m1: int = 14, m2: int = 28, m3: int = 57, m4: int = 114) -> pd.Series: + """知行多空线(通达信口径):(MA(M1)+MA(M2)+MA(M3)+MA(M4))/4,M1~M4 缺省 14/28/57/114。 + + MA 为严格窗口(窗口不满为 NaN,任一为空则多空线为空),与前端 zhixing.ts / + 通达信原公式一致(前 m4-1 根无值);区别于 avg_ma 的 min_periods=1 渐进口径。 + """ + mas = [close.rolling(p, min_periods=p).mean() for p in (m1, m2, m3, m4)] + return sum(mas) / len(mas) diff --git a/backend/app/screener/engine.py b/backend/app/screener/engine.py index 7931c88..91b2a91 100644 --- a/backend/app/screener/engine.py +++ b/backend/app/screener/engine.py @@ -59,6 +59,12 @@ def _boll(df: pd.DataFrame, p: dict) -> dict[str, pd.Series]: return {"boll_upper": out["upper"], "boll_mid": out["mid"], "boll_lower": out["lower"]} +def _zhixing(df: pd.DataFrame, p: dict) -> dict[str, pd.Series]: + dk = ind.zhixing_dkx(df["close"], m1=int(p["m1"]), m2=int(p["m2"]), + m3=int(p["m3"]), m4=int(p["m4"])) + return {"zhixing_dkx": dk, "zhixing_trend": ind.ema2(df["close"])} + + def _close(df: pd.DataFrame, p: dict) -> dict[str, pd.Series]: return {"close": df["close"]} @@ -82,6 +88,8 @@ FAMILIES: dict[str, FamilyDef] = { "rsi": FamilyDef("RSI", ("period",), {"period": 14}, 25, _rsi), "ma": FamilyDef("MA", ("period",), {"period": 20}, 25, _ma), "boll": FamilyDef("BOLL", ("period", "std"), {"period": 20, "std": 2}, 25, _boll), + "zhixing": FamilyDef("知行", ("m1", "m2", "m3", "m4"), + {"m1": 14, "m2": 28, "m3": 57, "m4": 114}, 114, _zhixing), "close": FamilyDef("收盘价", (), {}, 1, _close), "pct_chg": FamilyDef("日涨跌幅", (), {}, 1, _pct_chg), } @@ -91,12 +99,14 @@ INDICATOR_FAMILY: dict[str, str] = { "macd_dif": "macd", "macd_dea": "macd", "macd_hist": "macd", "rsi": "rsi", "ma": "ma", "boll_upper": "boll", "boll_mid": "boll", "boll_lower": "boll", + "zhixing_dkx": "zhixing", "zhixing_trend": "zhixing", "close": "close", "pct_chg": "pct_chg", } _IND_SUFFIX = {"kdj_k": "K", "kdj_d": "D", "kdj_j": "J", "macd_dif": "DIF", "macd_dea": "DEA", "macd_hist": "柱", - "boll_upper": "上轨", "boll_mid": "中轨", "boll_lower": "下轨"} + "boll_upper": "上轨", "boll_mid": "中轨", "boll_lower": "下轨", + "zhixing_dkx": "多空线", "zhixing_trend": "趋势线"} def _family_of(indicator: str) -> str: diff --git a/backend/app/screener/llm.py b/backend/app/screener/llm.py index d9de2f7..0adf7ab 100644 --- a/backend/app/screener/llm.py +++ b/backend/app/screener/llm.py @@ -22,8 +22,8 @@ SYSTEM_PROMPT = """你是 A 股选股条件解析器。把用户的自然语言 (indicator 与 snapshot 至少一个非空;用户没有提到的条件不要编造) 【indicator 数组】技术指标条件,元素字段: -- "indicator": 指标名,白名单:kdj_k / kdj_d / kdj_j(KDJ 的 K/D/J 值)、rsi、macd_dif / macd_dea / macd_hist(MACD 的 DIF/DEA/柱)、ma(收盘价均线)、boll_upper / boll_mid / boll_lower(布林轨道)、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} +- "indicator": 指标名,白名单:kdj_k / kdj_d / kdj_j(KDJ 的 K/D/J 值)、rsi、macd_dif / macd_dea / macd_hist(MACD 的 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} - "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 @@ -53,7 +53,11 @@ SYSTEM_PROMPT = """你是 A 股选股条件解析器。把用户的自然语言 示例3: 输入:近 5 天曾经 MACD 金叉(DIF 上穿 DEA),换手率大于 5%,流通市值小于 100 亿 -输出:{"indicator":[{"indicator":"macd_dif","params":{"fast":12,"slow":26,"signal":9},"op":"gt","value":0,"value_indicator":"macd_dea","lookback":5,"match":"any"}],"snapshot":[{"field":"turnover_rate","op":"gt","value":5},{"field":"circ_mv","op":"lt","value":100}],"exclude_st":true,"exclude_delisted":true,"exclude_bj":true}""" +输出:{"indicator":[{"indicator":"macd_dif","params":{"fast":12,"slow":26,"signal":9},"op":"gt","value":0,"value_indicator":"macd_dea","lookback":5,"match":"any"}],"snapshot":[{"field":"turnover_rate","op":"gt","value":5},{"field":"circ_mv","op":"lt","value":100}],"exclude_st":true,"exclude_delisted":true,"exclude_bj":true} + +示例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}""" class ScreenerError(RuntimeError): @@ -159,8 +163,8 @@ EVENT_SYSTEM_PROMPT = """你是 A 股事件回测参数解析器。用户描述 {"entry": {"indicator": [...], "snapshot": [], "exclude_st": true, "exclude_delisted": true, "exclude_bj": true}, "entry_timing": "next_open", "holding_days": 3, "exit_timing": "close"} 【entry.indicator 数组】入场信号条件(必填,至少 1 条),元素字段与白名单: -- "indicator": kdj_k / kdj_d / kdj_j(KDJ 的 K/D/J 值)、rsi、macd_dif / macd_dea / macd_hist(MACD 的 DIF/DEA/柱)、ma(收盘价均线)、boll_upper / boll_mid / boll_lower(布林轨道)、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} +- "indicator": kdj_k / kdj_d / kdj_j(KDJ 的 K/D/J 值)、rsi、macd_dif / macd_dea / macd_hist(MACD 的 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} - "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 diff --git a/backend/app/screener/market_sync.py b/backend/app/screener/market_sync.py index f09bfbe..cafade9 100644 --- a/backend/app/screener/market_sync.py +++ b/backend/app/screener/market_sync.py @@ -245,18 +245,21 @@ async def _upsert_candle_day(session: AsyncSession, rows: list[dict], listed: se ] if not batch: return - stmt = pg_insert(Candle).values(batch) - stmt = stmt.on_conflict_do_update( - index_elements=["symbol", "timeframe", "ts"], - set_={ - "open": stmt.excluded.open, "high": stmt.excluded.high, - "low": stmt.excluded.low, "close": stmt.excluded.close, - "volume": stmt.excluded.volume, - "amount": func.coalesce(Candle.amount, stmt.excluded.amount), - }, - ) - await session.execute(stmt) - await session.commit() + # on_conflict 语句整批渲染为占位符(非 executemany),asyncpg 单语句参数上限 32766, + # 10 列 x 3000 行 = 30000 参数留出余量 + for i in range(0, len(batch), 3000): + stmt = pg_insert(Candle).values(batch[i : i + 3000]) + stmt = stmt.on_conflict_do_update( + index_elements=["symbol", "timeframe", "ts"], + set_={ + "open": stmt.excluded.open, "high": stmt.excluded.high, + "low": stmt.excluded.low, "close": stmt.excluded.close, + "volume": stmt.excluded.volume, + "amount": func.coalesce(Candle.amount, stmt.excluded.amount), + }, + ) + await session.execute(stmt) + await session.commit() async def _run_sync(days: int, force: bool) -> None: