# 牛来选股面板
**Repository Path**: jangviktor/niucap
## Basic Information
- **Project Name**: 牛来选股面板
- **Description**: 牛来选股面板 Niucap:本地自托管的 A 股量化看盘面板,覆盖选股、择时与复盘全流程,新手也能零门槛上手。内置小白选股、25+ 策略选股、条件选股、实时行情(约3分钟自动刷新)、连板梯队、情绪周期、题材雷达、龙虎榜后验、双源快讯情绪分析、策略回测(T+1 真实约束/夏普/最大回撤/卡玛/网格防过拟合)、虚拟盘模拟交易、60+ 技术指标个股分析(资金流向/筹码分布/基本面)、市场榜单与宽度……
- **Primary Language**: Unknown
- **License**: MIT
- **Default Branch**: master
- **Homepage**: https://github.com/jangviktor-web/niucap
- **GVP Project**: No
## Statistics
- **Stars**: 2
- **Forks**: 1
- **Created**: 2026-09-27
- **Last Updated**: 2026-11-08
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README

# 🐂 牛来选股面板 · Niucap
**本地自托管的 A 股量化「选股 + 择时 + 复盘」一体化面板**
*29 个选股策略 · 实时行情 · 连板梯队 · 题材雷达 · 龙虎榜后验 · 双源快讯 · 策略回测 · 虚拟盘 · 选股复盘闭环 · 因子实验室 · 牛熊温度计 · 昼夜双主题 · 命令面板*
免安装 · 打开即用 · 演示环境数据仅供预览
⚠️ 2026-10 域名变更:旧 *.app.workbuddy.host 已停用,现地址为 *.app.workbuddy.link(原因见下)
[](https://gitee.com/jangviktor/niucap)
[](CHANGELOG.md)



[](LICENSE)
[](https://www.python.org/)
[](https://fastapi.tiangolo.com/)
[](#-5-分钟跑起来)
[](#-5-分钟跑起来)
[](#-5-分钟跑起来)
> 仓库名 `niucap`(牛 = capture 牛股,A 股梗);产品中文名「牛来选股面板」。两者指同一个项目。
>
> **A self-hosted A-share quant dashboard** — screening, limit-up ladder, sector radar, backtesting & paper trading. **No API key. 100% local.**
---
## 🎯 为什么用它
🔑 零 API Key,一条命令起
行情全部走腾讯 / 新浪 / 同花顺 / 东财公开接口,情绪分析是本地词典引擎。不注册、不付费、不限流,克隆下来跑起来就能用。
⚡ 秒级全市场扫描
29 个内置策略 + 六类分组(趋势形态 / 量价涨停 / 反转波动 / 形态相似度 / 经典指标 / 分钟级),并集交集自由组合,全市场扫描稳定在秒级。
🔒 数据在自己手里
SQLite 本地库或云端 MySQL 协议库自适应,日线数据落盘自己掌控。不是把隐私交出去的网页版。
🧠 选完还能复盘
每次选股自动存档,事后可回看窗口胜率(模拟 09:30–09:50 买入当日收盘结算)、入选后表现、批量沉淀自选——不靠记性判断策略好坏。
---
## 📑 目录
📊 三表仪表 · 周期切换 —— TradingView「指标概览」同款 震荡 / 总结 / 均线三组半圆仪表,26 项细分信号汇成五档评级(强烈卖出→强烈买入);上方 1分~1月 八档周期条一键切换,与下方 K 线图共用同一周期状态——不会出现「图上是 5 分钟、仪表还是日线」的错位;切换时指针以补间动画扫过去而非瞬闪
 |
📊 三表仪表 · 夜间 —— 同一套令牌换肤 暗色下仪表色块、读数与周期条高亮随主题一起切换,指针角度与白天完全一致;周期条排成一行不换行,顺序与 TradingView 一致
 |
📱 三表仪表 · 窄屏 —— 390px 单行放下 8 档 周期条在 390px 视口收紧内距后排成一行,8 个标签完整可见、不切掉「1月」半格、不出现横向滚动条
 |
⚠️ 三表仪表 · 根数不足不编结论 200 周期均线需 300 根 K 线,月线数据源只提供 28 根、周线 120 根——此时整组仪表显示「本周期无评级」并说明原因,而不是少算几项、给出看似正常实则偏移的数字
 |
🌟 小白选股 —— 不填条件一键选股 四种风格对应四套方案(盾牌稳健 / 折线超跌 / 上升成长 / 火焰打板),逐条给理由与友好度评分,一键跳个股详情
 |
🔍 策略选股 —— 29 个策略自由组合 按趋势形态 / 量价涨停 / 反转波动 / 形态相似度 / 经典指标 / 分钟级六类组织,点击即选、并集交集、秒级扫全市场
 |
🎯 条件选股 —— 像填表一样选股 PE / PB / 市值 / 涨跌幅 / 换手率 / 成交额自由组合,内置「低估值蓝筹」「成长活跃」「小市值活跃」「深度价值」四套预设
 |
📊 市场榜单 —— 六大榜单 + 温度画像 涨幅 / 跌幅 / 成交额 / 换手率 / 市值 / 低估值,叠加市场温度 13 维画像与涨跌分布
 |
📰 双源快讯 —— 新浪 7×24 + 同花顺 双源互备去重,重大消息红条高亮;词典情绪引擎自动打「看涨 / 看跌 / 中性」标签,关联股票可点直达
 |
🧪 策略回测 —— 真实约束下的成绩单 T+1、手续费、印花税、滑点全模拟;支持止损止盈与金字塔加仓;网格回测与走查回测防过拟合
 |
📈 个股分析 —— K 线 + 60+ 项指标 日 / 周 / 月 + 1~60 分钟 K 线,四种通道叠加,技术面结论卡片;资金流向 / 筹码分布 / 基本面 / 研报公告
 |
🔥 连板梯队 —— 梯队分组 + 情绪周期 近 12 日 K 线算连板天数,首板 / 2 板 / 3 板 / 4+ 板分组;融合 6 阶段市场情绪(冰点→启动→主升→高潮→退潮→修复)
 |
🎯 题材雷达 —— 板块四维评分 行业板块融合去重 + 实时主线排行,热点一目了然
 |
ℹ️ 关于 —— 模块构成实时统计 指标数 / 策略数由后端实时统计,新增即自动同步;附技术实现、数据源与免责声明
 |
🩺 市场体检 · 白天 —— 破净率 / 风格罗盘 / 宽基 PE 第四套口径:市场的结构位置。破净率 9.60%(536/5585 只)是全 A 横截面;风格罗盘给三对指数比值的 250/60 日分位(立即可用的真历史);宽基 PE 当日快照。自建历史分位逐日积累,攒够 30 天才显示——攒分位,不编分位
 |
🩺 市场体检 · 夜间 —— 同一套令牌换肤 红涨绿跌跨主题恒定:风格近 20 日 -14.8% 绿、+39.3% 红。风格分位 0% 是合法读数(比值处一年最低),照实显示不隐藏
 |
🐂 牛熊速查 · 白天 —— 0~100 温度 + 六档仓位 四类加权(估值 / 资金 / 情绪 / 技术)压成一个分数,六段温度条上指针定位当前档位;下方是情绪面六维雷达与温度趋势图,四类明细可展开看 13 维原始分与上游状态文本(如「暴跌(均线空头排列…)」)
 |
🧱 沪深300 ETF 规模占比 · 白天 —— 被动资金相对流通盘有多大 分子是沪深300 ETF 合计规模(22 只);附历史百分位表(全部/近10年/近5年/近3年,510300 口径)与占比 × 指数收盘双轴图,分母是指数流通市值,同源同单位、不换算。上半张是季报期末份额,不插值、不接到今天——季报之间会发生大额申赎(华泰柏瑞近 4 期 888→241 亿份),连上就等于宣称「季末到今天线性变化」,那是没数据的推论;下半张是今日一个点的占比,因为指数流通市值没有任何历史源。两图单位不同(亿份 vs %)刻意分成两张,合成一根轴会让「亿份」被读成「占比」
 |
🧱 沪深300 ETF 规模占比 · 夜间 —— 同一套图表令牌换肤 轴标签走 --ec-text、网格走 --ec-split,61 期横轴按容器宽度动态抽稀(不写死「每 2 个」),最后一个日期完整留在视口内不被裁
 |
🧱 沪深300 ETF 规模占比 · 窄屏 —— 390px 不横向溢出 主读数「0.79%」与分子/分母说明在窄屏下换行不挤压;两图各自缩到容器宽,横轴再抽稀一档
 |
🐂 牛熊速查 · 夜间 —— 同一套令牌换肤 暗色下语义 tint 改为半透明叠加 + 提亮字,图表文字色随主题一起换;温度色(绿→黄→红)是跨主题恒定的独立令牌,不随涨跌色漂移——否则浅色下「黄档」会变成信息蓝
 |
🧪 因子实验室 · 白天 —— 12 因子 IC/ICIR 检验 股票池下拉直接标注每个池的「有效截面 N 日」,不够 250 日的置灰不给选;表格给 IC 均值 / ICIR / IC>0 / t 值 / BH 校正 q 值 / 有效率 / 单调性 / 判定,判定要「|ICIR|≥0.3 且 q≤0.1」两个条件同时满足;五档分层收益与市况分段折叠在下,默认收起不打扰
 |
🧪 因子实验室 · 夜间 —— 同一套表格换肤 分层收益条正负用涨跌色(红正绿负)而非第二套配色;q 值小于 0.001 收敛显示为「<0.001」避免科学计数法;有效率是 0~1 比例,显示统一 ×100(这里曾经踩过「0.658 显示成 1%」的坑)
 |
✨ AI 解读 · 白天 —— 检验结果翻译成小白大白话 点控制条「✨ AI 解读」:先给一句总览(有没有因子真有预测力、最值得关注谁),再逐因子讲「在算什么 / 能怎么用」,判定未通过的因子会明说「只适合观察」;末尾列使用注意点与今日配额。LLM 不可用时页面照常,只在卡片位置说明原因
 |
✨ AI 解读 · 夜间 —— 同一份解读换肤 所有数字来自上方检验结果,LLM 只负责翻译;因子名逐个校验,编一个不存在的名字整份作废;解读缓存 30 分钟内命中不扣配额(标「缓存复用 · 未扣次数」);与「小白讲解」「AI 深度分析」共用每日配额池
 |
📈 个股因子画像 · 白天 —— 同一把尺子递给每只票 个股页直读同一批 12 个因子,给当前值与在本股近 750 日中的历史分位。分位条只表达「位置」不表达好坏——同一个 80% 分位,对低波因子是「波动比历史大」,对动量因子是「涨幅比历史强」。易读反的档位单独措辞:最大回撤分位 79% 显示为「回撤较浅」而非「偏高」,否则会被读成回撤偏大
 |
📈 个股因子画像 · 夜间 —— 同一套行样式换肤 纯本地库计算,不联网、不调模型、不扣配额,单票 0.1~0.3 秒。个股刻意不做 IC:单只票只有一条时间序列,每期 1 个样本,IC 在统计上无定义——硬套只会产出看似专业、实则无意义的数字。样本不足 250 点时显示「样本不足」,不编分位糊弄
 |
🌗 白天模式 —— 浅色画布(默认) 浅色色值取自主题改造前的原始配色,语义芯片回到「浅底 + 深字」,主按钮蓝底白字;红涨绿跌在浅色下同样成立
 |
🌗 夜间模式 —— Binance 风格暗色 近黑画布 + 卡片分层,主强调为「黄底黑字」签名 CTA;切一次即记住,首屏内联脚本避免刷新闪错主题
 |
🏭 板块舆情 —— 快讯按板块聚合的多空热度 双源快讯近 1/3/6/12/24 小时滚动窗口,按 ticker 反查 + 关键词聚到板块,给出偏多/中性/偏空占比与净情绪;卡片化展示,非实时单条、非预测
 |
🌐 多源快讯 + 后台 API 密钥 —— 自建可自填密钥 快讯并联 Finlight(带情绪)/ FreeNews / OkSurf 多源;后台「运行参数」页把 API 密钥设为密码框,自托管用户可在网页后台直接填写,无需改代码
 |
---
## 🐂 牛熊速查 + ⚖️ 股债温度 + 🧱 ETF 规模占比 + 🌗 昼夜主题
### 🐂 牛熊速查:把「市场温度」压成一个能看位置的分数
市场温度散落在 13 个维度里,单看涨停家数或单看指数涨跌都容易以偏概全。这个页面做一件事:**把上游 13 维归并成一个 0~100 分 + 六档仓位建议**,让「现在处在周期的哪个位置」一眼可见。
**分数怎么来的**(不新增任何外部数据源,全部复用面板已有的 13 维):
| 类别 | 权重 | 支撑维度 |
|---|---|---|
| 估值 | 30% | 估值水平(PE 分位) |
| 资金 | 25% | 成交量能 |
| 情绪 | 25% | 情绪指标、个股宽度、板块宽度 |
| 技术 | 20% | 技术指标、短/中长期趋势方向与强度、行业/风格/大小盘轮动 |
各维原始 1~5 分归一化到 0~100,类内等权、类间加权。
**六档仓位映射**(反直觉,务必先看这句):
| 分数 | 档位 | 建议仓位 | 信号灯 |
|---|---|---|---|
| 0~20 | 冰点区 | 60~80% | 🟢 |
| 20~40 | 低迷区 | 40~60% | 🟢 |
| 40~60 | 震荡区 | 30~50% | 🟡 |
| 60~80 | 活跃区 | 20~40% | 🟠 |
| 80~90 | 过热区 | 10~20% | 🔴 |
| 90~100 | 疯狂区 | 0~10% | 🔴 |
> **分数越高 = 市场越热 = 建议仓位越低。**「热」不是好事,是风险累积。90 分以上是「人人谈股」的疯狂区,只配 0~10%;20 分以下的冰点区才配到 60~80%。
**页面上有什么**:
- **温度条**:六个分段色块 + 指针,分段边界就是档位切换点(分段而非渐变——那是数据不是装饰)
- **情绪面六维雷达**:情绪 / 赚钱 / 题材 / 龙头 / 轮动 / 趋势,六维全部由现有 13 维支撑(「龙头」= 行业轮动,上游原文即「头部在涨 / 尾部在跌」)。与上面四类是两套切分:六维只看情绪面,估值与量能不在雷达里
- **四类明细**:展开可看每个维度的 5 点刻度与上游状态文本(如「暴跌(均线空头排列…)」「缩量震荡(成交额 90~110% 均值)」)——单看 1~5 的分数没人知道 1 是什么意思,状态文本才是人话
- **恐贪指数子块**:参考站恐贪指数的本地降配版,0~100 **越高越贪婪**(与温度语义相反,红=恐慌/绿=贪婪)。波动率与相对换手用沪深300 K 线算 10 年滚动分位,RSI 现算,市场宽度与涨跌停比是当日快照绝对映射(无历史,不冒充分位);两融无稳定免费源跳过,卡内如实标注。缺失子指标权重再分配,绝不返回 0 分
- **温度趋势图**:每天收盘后落一行(默认 16:45,`TICK_BULLBEAR_AT` 可改),攒够 2 天起画趋势线,背景铺六档色带
**几个刻意的设计取舍**(都会在页面上如实标注,不藏着):
- **资金维目前只有一个维度**(成交量能)。两融余额 / 10Y 国债 ERP / 破净股数都无稳定免费源,北向资金自 2024-08 起交易所已停止实时披露——接了也是半残数据,全部不接,页面标注覆盖率
- **缺失维度做权重再分配**,不做「缺了按 50 分算」:把没取到数伪装成「市场不冷不热」,等于把故障显示成结论。重新分配时会标「权重已重分配」
- **取数失败绝不写 0 分**:0 分=冰点=最强买入暗示,历史序列里绝不能用 0 表达「无数据」
- **历史无法回补**:13 维历史此前从未落库,序列只能从启用当天往后攒,初期会显示「已积累 N 天」而不是画一条假趋势线
- **权重未经本地拟合**:30/25/25/20 沿用外部模板的给定值,没有历史区间数据去证伪它,本地调参只会把主观意见伪装成统计量
> 以上为统计口径的位置参考,**不构成投资建议**。
### ⚖️ 股债温度:沪深300 相对国债,是贵了还是便宜了
牛熊速查页的第二张卡。牛熊温度回答「市场现在多热」,股债温度回答「股票**相对债券**多贵」——两者是不同口径,刻意并排展示而不是混成一个分数(一个过热、一个便宜,混算正好「震荡」,信息全丢)。

**指标怎么算**:`lr = ln(沪深300 / 国债ETF)`,取 500 个交易日的滚动 z 值(默认 2 年,可切 1/2/3/5 年),±2σ 通道;`z > 1.5` 算贵、`z < −1.5` 算便宜,中间是通道内。
**图是双轴对照**:左轴 z(暖黄,结论线)、右轴沪深300 点位(蓝灰,参照线)。两条一起看才知道每次触轨时大盘正在发生什么——实测:2015-04-22 是本段 z 的最高点 **+3.44**(沪深300 4739),2022-10-31 是低点 **−2.63**(3508)。右轴**只作参照,不参与任何计算**。

**为什么必须用滚动 z 而不是全样本均值**:债券 ETF 13 年涨 40.78%,那是**票息**不是行情(约 2.7%/年)。直接相除得到的比值带结构性趋势,用全样本均值画通道等于拿未来数据画今天的线。
**三条写在页面上的实话**(都不许省):
1. **债券腿是 ETF 价格,不是国债收益率。** 没有任何免费源能给出 2005 年起的完整国债收益率曲线——中债源 2021-01 就停更、东财三个报表全返空、`push2his` 直接 403。所以本指标与万得「股债利差」**方向相反、刻度不同,数值不可对读**。
2. **触轨不稀有。** ±2σ 的实测命中率是上轨 9.9% / 下轨 4.5%,远高于正态的 2.3%——比值非正态且有自相关,所谓「触上轨」平均每 10 天就有 1 天。
3. **IC 的两个 n 都给了。** 250 日前瞻的相邻样本重叠 249 天,名义 t=−21.6,去重叠重采样后有效样本只剩 128 个、t=−4.9。只看大的那个会高估。
**当前状态(2026-10-21)**:z = −0.55,通道内偏便宜一侧,历史分位 29%。三区历史统计(250 日前瞻,股票减债券的对数超额):便宜区 370 天平均 **+6.6%**、通道内 1801 天 **+2.0%**、贵区 377 天平均 **−18.3%**。
**数据源**:主源走**通达信(eltdx)**批量取数,`adjust='none'` **强制**——它默认 qfq,会把 511010 的历史整体压低 5.4% 且 13 年里阶梯式变化,实测污染后 z 从 −0.55 偏到 −0.85,且偏多少取决于看哪一年、无法事后校正。自检里有一条硬断言专门防这个。eltdx 不可用时自动回落新浪(实测两者 z 完全一致)。
**刻意不做的**:不产出买卖信号、不做 AI 解读。这里的结论是算术(z 在哪、分位多少、三区历史均值多少),AI 插进来只会把确定数字写成含糊话。
> 本面板只做诊断,不构成投资建议。方案与口径论证见 [docs/股债系统-开发计划.md](docs/股债系统-开发计划.md)。
### 🧱 沪深300 ETF 规模占比:被动资金相对流通盘有多大
牛熊速查页的第三张卡。三张卡是**三套互不替代的口径**,别混读:
| 卡 | 回答的问题 | 单位 |
|---|---|---|
| 🐂 牛熊温度 | 市场现在**多热**(13 维横截面 + 时序) | 0~100 分 |
| ⚖️ 股债温度 | 股票**相对债券**多贵(滚动 z) | 标准差 |
| 🧱 ETF 规模占比 | **被动资金**相对流通盘有多大 | % |

**口径**:分子 = 全部沪深300 ETF 的**合计规模**(不是只算 510300,也不是含场外指基);分母 = 沪深300 **指数流通市值**。两腿都取腾讯快照,同源同单位,不换算。
**为什么是「季报骨架 + 今日实时点」而不是一条连续的占比线**——这是这张卡唯一需要解释的设计:
- ETF **份额**有两种可得频率:季报期末(东财 F10,61 期回溯到 2012-05)和当日快照。
- 指数**流通市值**只有当日快照,**没有任何历史源**。
- 所以「占比」只在今天这一个点上成立。历史能画的只有「ETF 份额(亿份)」。
季报点之间**不插值、不连到今天**。连上等于宣称「季末到今天份额线性变化」,那是没数据的推论。实测差距有多大:华泰柏瑞 510300 季报 2026-09-30 是 **241.20 亿份**,今日实时隐含 **363.78 亿份**,差 **51%**;四期连跌 888.30 → 448.29 → 189.15 → 241.20。这类季中变动季报完全看不见,正好是这张卡想说的东西。
**历史占比是近似重建(510300 口径)**:分子用 510300 季报**期末净资产**(全池只有龙头有完整季报历史),分母用「今日流通市值 × 历史收盘/今日收盘」折算——等价于假设**流通股本 2012 年以来不变**。股本增发、样本调整都会引入偏差,所以这条线只看**相对位置与方向**,绝对值别当真。口径校验:本重建给出的全部样本百分位 82.8%、近10年 75.0%,与百分位网(全口径、日频、独立数据)的 83.7% / 76.5% 高度吻合——两条独立路径指向同一位置。
**上图双轴**:蓝线 510300 占比(左轴 %),红线沪深300 收盘(右轴,参考百分位网同款)——扩容/收缩与指数涨跌对照着看,才知道占比起伏主要来自申赎还是价格。双轴只比较方向、不比较数值高低。
**百分位表**:全部 / 近10年 / 近5年 / 近3年四档(口径 = 严格小于当前的样本 ÷ 窗口样本,当前值不计入)。窗口样本不足 8 个不给(所以没有「近1年」——季频一年只有 4 个点,那种百分位没有意义)。**只对 510300 口径给**:历史线是 510300 一只的,拿全池今日占比去比等于换尺子。
**与参考站的差距(如实)**:baifenwei 有日频净申购数据因此能画日频线并给「近20日净申购」,我们拿不到该源;季频是诚实的选择,季报之间的大额申赎(单季 888→448 亿份)在两条线上都看不见。
**两张图不共轴**:`hist[].shares` 是**亿份**,`today.ratio` 是**百分比**。合成一根 y 轴,「亿份」会被读成「占比」(0.79% 和 700 亿份放同一根轴上,读者分不出哪个是哪个)。所以上图季报份额折线、下图今日占比单点柱,各自一根轴。
**占比是慢变量**:0.78% → 0.80% 是 +2.6% 的相对增长,不是「涨了 0.02 个点」。它衡量被动资金相对流通盘的**体量**,不判断涨跌——占比高不代表要涨,只代表这部分筹码被被动锁定了。
**实测(2026-10-29)**:
```
今日占比 0.8015%(全池 22 只)
510300 占比 0.3163%(与历史序列同一把尺子)
分母 482679 亿(48.27 万亿,指数流通市值)
季报 58 期(净资产列可解析的),回溯到 2012-06-30
百分位 全部 82.8% · 近10年 75.0% · 近5年 50.0% · 近3年 16.7%
(参考站全口径:83.7% / 76.5% / 52.9% —— 独立口径交叉验证)
池子前 6:
510300 沪深300ETF华泰柏瑞 1494.4 亿
510310 沪深300ETF易方达 882.1 亿
510330 沪深300ETF华夏 583.5 亿
159919 沪深300ETF嘉实 461.4 亿
515330 沪深300ETF天弘 94.4 亿
510360 沪深300ETF广发 39.7 亿
```
**取数口径为什么这么绕**(踩过的坑都写在这):
- **名称必须精确匹配**。宽松的「含沪深300」会混进 `300红利低波ETF`、`沪深300成长ETF`、`沪深300价值ETF`、`ESG300ETF`——它们跟踪的是衍生指数;「含300 且含ETF」又会混进 `中证1000ETF`、`A100ETF`。最终用白名单正则 `^沪深300(ETF[一-龥A-Za-z]*)?$` + 黑名单关键词。
- **静态种子表是完整性的唯一保障**,不是优化。全市场沪深300 ETF 有 30+ 只,腾讯搜索接口单次**最多回 10 条**(硬限制,不是参数没给够),极新的小产品会漏。种子表兜底,名称一律以实时返回为准。
- **数据源换过一轮**。原方案走新浪 ETF 清单:连续 3 次 26s 超时;东财 `push2/clist` 首屏 0.4s、第二次请求即 `RemoteDisconnected`。现全部改走腾讯(搜索 0.0s、批量行情 0.1~0.2s,连测无失败)。
**如实记录的边界**:
1. 季报之间份额变化可能极大(见上面 888→241),图上一律不插值,卡片顶部有提示条明说「折线是季报期末,不是今天」。
2. 指数流通市值无历史源,「占比」只在今日一个点成立,没有历史占比曲线可看。
3. 种子表 + 搜索的组合有盲区:腾讯搜索一次只回 10 条,极新的小产品可能延迟被发现(漏掉的小产品合计约 3%,会轻微低估占比)。
**自检**:`tests/check_etf_ratio.py`(22 条,含后端池子非空、失败信息不谎称无标的、百分位表、跨度切换真重画、双轴 legend)+ `server/hs300_etf_ratio.py` 内置 `selfcheck()` 26 条。
### 🌗 白天 / 夜间模式
顶栏「☀ 白天 / 🌙 夜间」一键切换,选择存 localStorage,`` 内联脚本在首屏前同步 `data-theme`,刷新不会闪错主题。**默认白天**。
实现走**令牌而不是两套样式**:`:root[data-theme="light"]` 只覆盖 CSS 变量(浅色值取自主题改造前的原始配色),图表里 65 处硬编码色(tooltip 背景 / 轴线 / 网格 / 轴标签 / 图例)全改读 CSS 变量,所以两种模式下图表一起换色,不用维护两份 option。
配套处理的细节:
- `--on-primary` 变量化:暗色黄底黑字、浅色蓝底白字(顺手修了浅色下主按钮蓝底黑字只有 2.5:1 的老问题)
- 语义 tint 自动适配:暗色下半透明叠加 + 提亮字,浅色下回到「浅底 + 深字」
- **温度色是跨主题恒定的独立令牌**(`--bb-cold/mild/warm/hot/over/mad`)——温度语义(绿=冷 → 红=热)不该随涨跌色漂移,否则浅色下「黄档」会变成信息蓝
- 分数等文字用中性色,仪表色只上色块(黄字白底对比度只有 1.6:1)
> 已知简化:切换主题会整体 reload——K 线数据没缓存在 `state` 里(只有分钟模式有),按主题重画需重新取数。要改成只重画不刷新,得先把最近一次响应存进 `state`。
---
## 🧪 因子实验室:IC/ICIR 因子检验(v1.1.0 新增)
这一页回答一个很具体的问题:**哪些指标对 A 股未来收益真有预测力?**
和「策略体检」是两件事,别混:
| | 策略体检(`strategy_eval`) | 因子实验室(`factor_lab`) |
|---|---|---|
| 评估对象 | 0/1 **选股信号**(命中/不命中) | **连续因子**(动量、低波、反转、流动性…) |
| 回答 | 这个策略最近灵不灵 | 这个因子本身有没有 alpha |
| 用途 | 决定要不要开某个策略 | 决定轮动/组合用什么做原料 |
因子是轮动策略的原料:**没有预测力的因子,轮动就只是随机换仓。**
### 检验怎么做的
1. 构造因子矩阵(交易日 × 股票),**只用 t 日及之前的数据**,无前视;
2. 未来收益 `ret[t] = close[t+1+forward] / close[t+1] - 1`——t 日收盘出信号、**t+1 收盘才买入**(贴近实盘,避免「用信号日收盘价成交」的前视);
3. 每个交易日做一次**横截面 Spearman 秩相关**,得到 IC 序列;
4. 汇总 IC 均值 / ICIR / IC>0 占比 / t 值 / **BH 校正后的 q 值** / 五档分层收益 / 市况分段(用成分股自构等权指数划分涨 / 跌 / 平,不依赖外部指数数据)。
内置 12 个因子,方向**不预设**「越大越好」,由 IC 符号定:
| 类别 | 因子 |
|---|---|
| 动量 | 20 / 60 / 120 日动量 |
| 反转 | 5 日反转 |
| 低波 | 60 / 120 日波动率、60 日最大回撤 |
| 超买超卖 | RSI(14)、MA20 乖离、布林 %B |
| 流动性 | 60 日均成交额(对数) |
| 量能 | 5 日 / 60 日量比 |
### 两个刻意设计,别在后续迭代里改掉
**① 判定是「|ICIR| ≥ 0.3 且 q ≤ 0.1」,不是只看 ICIR。**
一次跑 12 个因子,就算全部无效,按 `p<0.05` 平均也会捞出 0.6 个「显著」——这就是多重检验。ICIR 单看分不清「真有 alpha」和「运气好」,所以加了 Benjamini-Hochberg FDR 校正:q 值把「同时检验了 N 个」这件事折进去。**只满足 ICIR 不满足 q 的因子,判定列不给 ✅。**
**② 数据不够就直接拒绝跑,不硬产出结果。**
池内有效截面不足 **250 个交易日(约一年)**时,后端返回 409 并说明原因。原因是 ICIR 这类指标在样本不足时会给出一个**看起来很正常的数字**,用户没法从结果本身判断它是不是噪声;而在入口处明说「不够」,至少知道该去补数据而不是信一个假信号。宁可拒绝,不可糊弄。
> 实测口径(2026-10-19 库内数据):沪深300 有效截面 2817 日、中证500 517 日、中证1000 2971 日、中证2000 524 日——四个池都够跑;「全部(四大指数合并)」在池下拉里是置灰的,因子库只支持单一指数池。
### 性能与缓存
- 默认只取**近 750 个交易日(约 3 年)**:全历史 3106 天跑 12 个因子的 IC 要几十秒,而几年前的 IC 对当下几乎没有参考价值。可在「回看区间」里改 250 / 1500 / 全部历史;
- 结果按 `(池, 收益窗口, 回看区间, 是否分段)` 缓存 **30 分钟**,二次打开从 5.5 秒降到 0.07 秒;
- 首次进入页面只读股票池覆盖情况(`<1s` 的轻量查询),**点「开始检验」才开始跑重的**;
- 改任何参数自动重跑(缓存 key 含这三个参数,不会拿到旧结果)。
### 一键 AI 解读(给小白的大白话)
检验表格对小白不友好——ICIR、q 值、分层单调性,每个词都得查一遍。控制条上的「✨ AI 解读」把当前这份检验结果发给大模型,翻译成三样东西:
- **总览**:这批因子整体有没有预测力,最值得关注的一两个是谁(引用给定数字);
- **逐因子**:这个因子在算什么、算出来的数说明什么、能怎么用(如「可作轮动池的排序依据」);判定未通过的因子会明说「只适合观察、暂不宜当决策依据」;
- **注意点**:使用前必须知道的事(样本期多长、市况变了可能失效、分层不单调意味着预测力不稳定)。
四个刻意设计,别在后续迭代里改掉:
1. **只喂前 5 个因子,且名字写进 prompt 逐个校验。** 后端先按 |ICIR| 降序取前 5 名,返回的因子名不在这份名单里,整份解读作废。少了这道,LLM 会自己挑因子、甚至编一个不存在的名字——而用户看不出区别;
2. **所有数字必须来自传入的检验结果。** LLM 的活是「翻译 + 解释怎么用」,不是「算数」。检验结果是确定性算出来的,不能反过来被 LLM 改写;
3. **解读缓存命中不扣配额。** 同一份检验结果 30 分钟内的解读直接复用(标「缓存复用 · 未扣次数」),不重复花 Token;顺序上是先查缓存、再扣次数;
4. **失败绝不伪装成成功。** LLM 未启用 / 网络错 / 输出不过 schema,一律 `llm=null` + 原因文案,页面照常可用(脚注口径常驻),不报错、不白扣配额。
与「小白选股 AI 讲解」「AI 深度分析」共用同一配额池(默认 10 次/天,后台「AI 用量」面板可调),模块开关 `TICK_AUTH_REQ_FACTORAI` 默认要求登录。
### 个股页面:因子画像(不做 IC)
个股页也有一块「因子画像」卡片,但**它和上面这张 IC/ICIR 表不是同一件事**,两者不可互相替代:
| | 因子实验室(池级) | 个股因子画像 |
|---|---|---|
| 算法 | 横截面 **IC / ICIR** | 单序列**历史分位** |
| 回答 | 按这因子排序,能不能选出更好的一批 | 这只票现在的因子值,相对它**自己的历史**什么水平 |
| 样本 | 池内数百只股票的每日截面 | 该股近 750 个交易日 |
| 联网 / LLM | 本地库 + 可选 AI 解读 | 纯本地库,不联网、不调 LLM、不扣配额 |
**为什么个股不做 IC**:IC 是横截面统计量(同日池内排名 vs 未来收益排名的相关性)。单只股票只有一条时间序列,每期 1 个样本,IC 在统计上**无定义**——不是「算出来很差」,而是根本算不出来。硬套只会产出一个看似专业、实则无意义的数字,所以这里刻意只做分位。
实现要点(`server/factor_single.py`):
- **完全复用 `factor_lab.build_factors`**:那批 `_f_*` 函数都是按列运算,N=1 的矩阵照常工作,口径与因子实验室严格一致(两处数字打架是最难查的 bug);
- **分位口径是「严格小于的比例 + 平局折半」**,所以最小值落在 0 而非 `1/N`;重复值多时(RSI、成交额对数常见)不会把分位系统性抬高;
- **方向易读反的因子单独措辞**:`mdd_60` 是负值,分位 79% 的含义是「回撤比历史上 79% 的时候**更浅**」,直接标「偏高」会被读成「回撤偏大」,故覆盖为「回撤较浅 / 回撤很深」;`vol_surge` 同理改为「显著放量 / 缩量明显」;
- **样本不足就明说**:有效历史少于 250 点时 `pct=null`,显示「样本不足」,不编一个分位糊弄;
- **当前值取最后一个有效点**:停牌 / 新股末格为空时,取 `[-1]` 会让整个因子莫名空缺。
接口 `GET /api/stock/factors?code=sh600519`,单票约 0.1~0.3s(纯内存计算),同步返回即可,不需要「提交 + 轮询」;结果按代码缓存 10 分钟。
> 档位是**位置描述**,不是好坏判断——同一个 80% 分位,对低波因子是「波动比历史大」,对动量因子是「涨幅比历史强」,好坏取决于因子方向。