跳到主要内容

量化选股与打分体系

概述​

本仓库的"选股"不是一条算法,而是四套互补、各不越界的体系:

体系输入产出入口
因子打分fox_stock_wide 单表列 + 指标表富化列11 个 0~100 具名因子分 + 加权综合分/factor-pick、POST /factor/screen
条件 DSL字段 + 比较算子布尔筛选/screener、/nl-pick(自然语言→DSL)
形态识别fox_kline_daily 复权序列方向列(±100)+ 度量列/patterns、GET /quant/patterns/{code}
LLM 研判上述全部 + 联网搜索五维分析 / 策略批量跑/dimension/:dimKey、/rule-pick

共同纪律:规则在前 · LLM 在后——确定性门控先否决,再谈打分;打分前先过 gates, 被拦标的进 rejected 并写明原因,而不是拿一个高分混进榜单。

因子引擎​

实现:backend/app/services/scoring/factor_engine.py。数据源是单表 fox_stock_wide (零 HTTP),另有 6 个富化列(vol_accel_5_5 + 涨停基因 5 列)由 quant/limit_gene.attach_genes 从 fox_stock_indicators_daily 最新日回填 (源头同样是本地库 em_limit_board 聚合,仍零 HTTP),每个因子由若干列做批内百分位 (横截面,无绝对阈值)后取秩均值 ×100。

为什么是批内百分位

"A 股今天涨 2% 算强吗"取决于当天分布。横截面分位让因子分在不同市场环境下可比, 代价是它是相对分不是绝对好坏——榜单读法必须配合门控与战绩一起看。

key中文标注权重输入指标(方向)
momentum动量real0.1520/60/一年涨幅 (high)
quality质量real0.13ROE/毛利率/净利率/ROA (high)
value价值partial0.11PE/PB/PS (low)
growth成长real0.10营收与净利同比 (high)
trend趋势结构proxy0.09MA5 乖离/YTD/10 日涨幅 (high)
capital资金real0.09主力净流入占比与净额 (high)
stability稳健低波proxy0.09日内振幅 (low)
shortTerm短期异动real0.083/5 日涨幅 + 涨速 + 量能加速度 5/5 (high)
liquidity流动性real0.08成交额/流通市值/换手 (high)
limitGene涨停基因real0.04近 20 日涨停次数/今日连板 (high) + 近 5 日炸板未回封 (low)
size规模real0.04总市值 (low,小盘溢价)
real / partial / proxy 是诚实标注,不是装饰

proxy 意味着该因子用可得列代理了真正想要的量:趋势结构本应看均线排列, 宽表没有该列,故用乖离 + YTD 代理;稳健低波本应看 ATR,宽表无列,故用振幅反向。 partial 表示口径不完整(价值因子缺 EV/EBITDA 与现金流口径)。 把 proxy 因子当真实因子解读会得出错误结论,前端图例必须原样呈现这个标注。

补充约定:缺失维度从分母剔除并归一化权重(不是补 0 分,补 0 会系统性惩罚数据缺失的票); 估值类列 ≤0 清洗为 None(_NONPOS_KEYS);low 方向取 1 - rank。 排行/展示随行携带的原始字段集中在 DISPLAY_FIELDS(18 项:code name industry market price change_pct turnover_pct volume_ratio pe_ttm pb total_market_cap float_market_cap vol_accel_5_5 limit_up_count_20d limit_days_today zt_stat_latest seal_fund_latest break_fail_days_5d)。

🔴 榜单行只能取自 DISPLAY_FIELDS:amount 不在其中,r.get("amount") 恒为 0 (多因子榜曾因此满屏「0.0 亿」,改用 turnover_pct)。

硬门控​

实现:backend/app/services/scoring/gates.py::check_gates,返回 {passed, blocked, code, reason, warnings}。

规则阈值处置
ST / *ST名称含 "ST"否决
ROE < 0ROE_HARD_LIMIT = 0.0否决(持续亏损)
资产负债率 > 90%DEBT_RATIO_HARD_LIMIT = 90.0否决(高杠杆)
财务完全缺失ROE/负债率/营收/利润全 None默认警告;严格调用传 block_on_missing_financials=True 才否决
营收 + 利润双负增长两者 < 0警告
资产负债率 70%~90%DEBT_RATIO_WARN = 70.0警告
缺 ROE 或缺负债率—警告

财务缺失默认降级为警告而不是否决,原因是实时打分常常拿不到财务列(次新股/盘中), 一律否决会让榜单在大半天里是空的。结果随 compute_stock_score 的 gate 字段返回, stock_scoring 的 level 对被拦标的置「否决」。

量化预设​

实现:backend/app/services/quant/presets.py::PRESETS,14 套。 每条结构:key / label / desc / rationale(为什么有效)/ watchOut(最容易亏在哪) / filters({factor, min, max} 因子门槛)/ sort / limit,可选 universeHint.industries 与 minComposite。

key中文key中文
momentumCore动量核心valueDeep深度价值
breakoutVol放量突破growthMomentum成长动量
volumeIgnition放量启动capitalFlow资金驱动
oversoldQuality超卖反弹(好公司坏日子)blueChipLiquidity大盘蓝筹
smallCapElastic小盘高弹性qualityCompounder高质量长牛
aiComputeRotation算力链轮动multiFactorResonance多因子共振
lowVolQuality低波质量(防御)limitUpGene涨停基因(短线强势)
watchOut 是必填项,不是免责声明

预设的设计约束是"每条必须诚实说明它最容易亏在哪":breakoutVol 写明假突破是主要亏损 来源(等收盘确认、不追盘中冲高,高位爆量可能是出货);smallCapElastic 写明流动性风险 最大(冲击成本吃收益,须卡成交额门槛)。新增预设不写 watchOut 视为未完成。

用户在同一因子上的手改覆盖预设值(_merge_filters,与预设 filters 按 factor 合并)。

形态识别:三层基元优先​

不为每个形态手搓一个探测器,而是分三层(🔴 依赖方向单向,禁止反向):

  • chart_geometry 只提供基元——"取哪些枢轴、突破哪条线"是形态层的事
  • STRUCTURAL_FIELDS 方向列 16 个:duck_buy duck_sell arc_bottom arc_top cup_handle + 图形形态 11 项(双顶/双底/三重顶/三重底/头肩顶/头肩底/矩形/三角/楔形/旗形/岛形)
  • STRUCTURAL_METRICS 度量列 9 个:duck_stage duck_neck_days duck_pullback_pct duck_vol_shrink_pct duck_breakout_pct + rectangle_height_pct triangle_kind wedge_kind flag_kind

四条硬约定:

约定原因
写行门槛 = has_signal(pat) or has_structural_signal(struct)(quant/engine.py:180)duck_stage 等度量列是筛选条件不是信号,但处于"鸭颈/鸭头回落"的股票必须落行,否则选股器(FILTER_SPECS 读本表)永远查不到
无未来函数fractal_* 只返回右侧已有 k 根确认的枢轴;鸭头取"当日之前"的最高点——否则突破当日会被自己的新高顶掉,买点②永不成立
突破首日才置方向值枢轴类图形形态用 _first_breakout(今日破线且昨日仍在线内),否则同一只票天天推;arc_bottom/arc_top/cup_handle 是状态判定,按"当前成立"消费
字段三处同改STRUCTURAL_FIELDS/STRUCTURAL_METRICS → registry.py 的 pattern_* → api/quant.py 的 structural·duck·metrics 三块输出。度量列不得混进 signals(否则"回调 8%"会被当成买入信号播报)
放量门槛按突破方向分档

vma 是含形态本段的滚动均量,顶部区间的均量被自身峰值抬高(实测下破候选日 vol/vma 中位 0.86 vs 上破 1.42)。两向共用一个倍数会让全部顶部形态恒零信号,而"永远不报顶" 比报错更难发现。故上破用 breakout_vol_mult=1.5、下破用 breakout_vol_mult_down=1.0。

阈值集中在 DuckParams / SwingParams 等 dataclass,可经 SysConfig pattern.{group}.{field} 覆盖(60s TTL,改后须重跑 quant_calc)。 边界线取"被最多枢轴触及的那条"(best_line,平局取跨度更长),不是拟合首尾枢轴—— 后者会被边缘平台枢轴带偏(实测把矩形斜率顶到 4.17%,越过 4% 门槛而漏报)。

指标层产出(quant_calc 17:30)​

表内容列族
fox_stock_indicators_daily技术指标均线 ma5..ma250、MACD、KDJ、BOLL、RSI、CCI/WR/VR/CR/ATR、乖离、OBV、位置 pos_5..180 与共振、贯穿突破
fox_stock_pattern_daily单根组合 + 结构形态方向列 ±100、度量列
fox_stock_cyq_daily筹码分布profit_ratio、avg_cost、90%/70% 区间上下沿与集中度

写入者统一为 quant/engine.py::run_quant_calc(先按 date 删目标日行再批量插入, 因此重跑即覆盖——改完 quant 代码重启 worker 后要手动重跑一次当日)。 🔴 必须在 fox_kline_sync(15:40)之后执行:它读 fox_kline_daily。

战绩闭环​

统一前向收益口径(唯一实现)​

backend/app/services/forward_return.py:

要素口径
买入入选日次一交易日开盘价(缺失退化为收盘)
卖出持有 HOLD_DAYS=5 个交易日收盘
胜收益 > 0;形态榜按方向对齐 aligned = ret_5d × sign(+100 看涨 / −100 看跌),aligned>0 即走对

builtin_track、pattern_review、scorecard 都调它而不各写一份—— 同一批信号用两套口径算出的胜率无法互相解释。

排雷榜的语义相反

gate(硬门控排雷榜)的 semantics="avoid":被否决的标的跌得多才说明拦得对。 聚合文案反向表述,不硬套"胜率"。命中率刻意不报浮亏——同一份 max_drawdown_pct 在 −100 的桶里含义翻转(成了最大浮盈)。

每日留痕​

任务时点落表底表
builtin_track18:15(须晚于 dragon_score_sync 17:40,VPA 候选池取自当日龙头评分)app_builtin_board_pick(三榜 top 20)vpa / factor / gate
preset_track18:25app_preset_pick_daily(14 预设 top 20)fox_stock_wide

🔴 两条共同的硬约定:重跑只重建本日 pending 行,已复盘的行连同买点与收益原样保留 (否则每天重算会把历史战绩洗掉)。底表 fox_stock_wide 是覆盖式快照、无业务日期列, 所以 as_of 取 max(updated_at) 并经 trading_calendar.latest_trade_date() 归一到 「截至该时刻已收盘的最近交易日」——写入时刻不是业务日期;拿不到 as_of 就跳过, 不许拿今天的排名冒充历史。窗口未满保持 pending,长期停牌按现有根数记 partial, 都不提前结账。榜单是收盘后落库口径,刻意不做盘中实时化(横截面分掺进实时价 会让排名半天内反复横跳),as_of 在卡片与面板上如实显示。

只读卡片与跨体系成绩单​

  • services/strategy_audit.py:把既有三套打分(龙头五维 app_dragon_score / 量价 VPA / 多因子)+ 硬门控聚合成只读卡片与榜单,与用户配置的 /strategies 刻意分家 (卡片无参数、点开即用)。端点走独立前缀 /builtin-strategies (/strategies/builtin 会被 /{strategy_id} 吞掉)。同一份进程缓存服务三处 (体检/因子快照/榜单各 300s,整榜缓存后切片——limit=1 与 limit=50 不许各算一遍); 每套体系独立 try/except,故障降级 neutral 并附原因,不适用如实标 na
  • services/scorecard.py:9 个来源统一成同一行契约 + 同一套汇总 (dragon5 vpa factor gate pattern tactic quant_strategy quant_preset strategy_run), 端点 /scorecard/{sources,picks,breakdown,overview,stock/{code}},前端四个入口共用
成绩单三条不能省掉的诚实性

① 汇总与表格取自同一批行——后端按筛选后的行算汇总,口径文案(buy_model/note) 也由后端下发,前端一个收益数字都不自己算; ② has_hit_rate=false 的来源不显示那个假数字:龙头五维的买入价取断板日最低价、 收益窗口含买入日 → max_return_pct 结构性 ≥0、胜率恒 100%,是构造产物非战绩,只给 win_rate_na 的理由; ③ 明细被等距抽样时(truncated)面板顶部黄条 + coverage 显示实际覆盖日期范围, 并如实说明汇总基于抽样后的行。

形态战绩(GET /quant/pattern-performance)不另起留痕表—— fox_stock_pattern_daily 本身就是留痕(PK (code,date)、方向列只写 ±100)。 只评测 STRUCTURAL_FIELDS ∩ ORM 列:显式传 duck_stage 这类度量列必须被拒 (它是筛选条件,当信号回测等于把"处于鸭颈期的票"当买点)。

端点速查​

前缀文件主要端点
/factorapi/factor.pyGET /factors(图例)、GET /presets、GET /strategy-hits/{code}、POST /screen(打分 + 筛选分页)
/quantapi/quant.pyGET /indicators/{code}、/patterns/{code}、/cyq/{code}、/signal-pool、POST /recalc、/strategies 与其 picks/validation、/screener 与字段、VPA、/pattern-performance、/pattern-catalog、/relative-strength、公式
/screenerapi/screener.pyPOST /find(DSL)、GET /filters、/presets、POST /parse-dsl、POST /nl-find
/scorecardapi/scorecard.pyGET /sources、/overview、/picks、/breakdown、/stock/{code}
/builtin-strategiesapi/builtin_strategies.pyGET ""(卡片清单)、/{key}/board、/{key}/performance

相关文档​