量化因子平台 · 使用手册
按 因子 → 策略 → 回测 → 选股/推荐 → 评估 五大主线展开,每步讲透「为什么这么做、不这么做会怎样、解决了什么实际问题」
零、平台基础
平台基础功能覆盖市场行情查看、个股深度分析、AI推理、盘中实时监控等,是量化策略研究的数据底座和执行入口。
0.1 总览仪表盘
作用与价值
一屏展示市场核心数据:指数实时行情、涨跌家数、北向资金、情绪指标、最新推荐摘要。快速感知当日市场全貌,决定是否深入分析或执行交易。
实现思路
- 后端 10 秒高频轮询从腾讯接口拉取指数行情并缓存
- 涨跌家数从 ClickHouse
stock_daily按最新交易日聚合 - 推荐摘要从
stock_recommendation表读取当日 Top5
0.2 行情数据
作用与价值
全市场截面行情浏览和单股日K查询,替代传统行情软件的基础数据浏览功能。截面数据从 ClickHouse 按日期查询,日K按 symbol+日期范围返回 OHLCV。
0.3 大盘温度计
作用与价值
量化市场情绪冷热程度,综合 PE 分位、PB 分位、均线温度、股债收益比、QVIX 波动率五维度,输出 0-100 恐慌贪婪指数。<25 极度恐慌适合建仓,>75 极度贪婪需警惕回调。
五维度指标体系
| 维度 | 权重 | 计算方式 | 阈值含义 |
|---|---|---|---|
| PE 分位 | 25% | 全市场等权 PE-TTM 在近 3 年历史中的分位(反转:分位越高=越贵=越恐慌) | <20% 极度低估;20~40% 低估;40~60% 合理;60~80% 高估;>80% 极度高估 |
| PB 分位 | 15% | 全市场等权 PB 在近 3 年历史中的分位(反转计算) | 与 PE 分位结合,排除周期股 PE 失真 |
| 均线温度 | 25% | 沪深 300 的 MA20 vs MA60 趋势信号,近 20 日多头占比归一化到 0~100 | ≥70 多头;≤30 空头;其他 震荡 |
| 股债收益比 | 20% | A 股等权 E/P ÷ 10 年国债收益率,映射到 0~100 分 | 比值 >3.0 → 90 分(股票极具吸引力);≤1.5 → 10 分 |
| QVIX 波动率 | 15% | 300ETF 期权隐含波动率,归一化到 0~100(P5=14.66, P95=28.03) | >30 市场恐慌;<15 极度乐观;均值 19.73 |
七档情绪标签
| 档位 | 评分范围 | 操作建议 |
|---|---|---|
| 极度恐慌 | 0~14 | 市场严重超跌,估值极具吸引力,可提升仓位至 80%+ |
| 恐慌 | 15~28 | 情绪偏空,可能接近底部区域,可开始关注超跌优质标的 |
| 偏恐慌 | 29~42 | 情绪偏谨慎,定投可加大金额 |
| 中性 | 43~56 | 情绪正常,无明显方向,维持 50~60% 仓位 |
| 偏贪婪 | 57~70 | 情绪偏乐观,可适度加仓,精选个股 |
| 贪婪 | 71~85 | 情绪过热,估值偏高,注意仓位控制 |
| 极度贪婪 | 86~100 | 严重泡沫化信号,降至 30% 以下,考虑对冲 |
0.4 个股分析
作用与价值
输入股票代码,获取全维度分析报告。涵盖四维度评分(技术面+基本面+资金面+情绪面)、K线图、形态检测(5大起涨形态)、卖出信号(7种卖点)、资金流向、估值分位、同业对比、缠论K线、多空辩论、HTML报告导出等 18+ 项功能。
实现思路
- 四维度评分:技术面(均线/形态/量能)+ 基本面(PE/PB/ROE)+ 资金面(主力资金/北向)+ 情绪面(新闻/研报)
- 并行化:CompletableFuture 5 线程并行(原串行 for 循环)
- 多空辩论:自动生成 Bull/Bear 论据 + 结论 + 偏向
四维度评分细则
四维度总分 135 分:技术面(50) + 资金面(25) + 事件面(25) + 基本面(30)。
| 维度 | 满分 | 核心子指标 |
|---|---|---|
| 技术面 | 50 | 趋势状态(10) + MACD综合(10) + RSI14(6) + BOLL轨道(6) + ADX(3) + 量比(5) + 近高/近低(8) + BOLL带宽(2);含惩罚项:高位背离(+6)、均线空头(+3)、KDJ死叉(+2)、SAR翻空(+2)等 |
| 资金面 | 25 | 主力净流入(6) + 主力净流入占比(5) + 融资余额变化(3) + 5日累计主力(3) + 股东户数变化(2) + 量比(2) + 换手率偏离(3) + 内外盘比(2) |
| 事件面 | 25 | 连续涨停(4) + 炸板率(3) + 强势股(3) + 龙虎榜(3) + 机构调研(6) + 基金持仓(4) + 公告(2) + 新闻(2);大盘蓝筹(总市值≥1000亿)使用蓝筹模式 |
| 基本面 | 30 | ROE(4) + 净利率(3) + PE估值(3) + 营收增速(3) + 净利增速(3) + PB估值(2) + 毛利率(3) + 资产负债率(2) + 现金流质量(2) + 偿债能力(2) + 研报评级(3) |
操作建议档位
| 总分 | 建议 | 仓位 | 信心度 |
|---|---|---|---|
| ≥84 | 强烈买入 | 80% | 90% |
| ≥63 | 买入 | 50% | 70% |
| ≥42 | 持有 | 30% | 50% |
| ≥21 | 减仓 | 10% | 60% |
| <21 | 清仓 | 0% | 80% |
RS 相对强弱解读
RS Ratio = 个股近 60 日累计收益 ÷ 行业累计收益,衡量个股相对行业的强弱。
| RS Ratio | 含义 |
|---|---|
| >1.5 | 显著强于行业 |
| 1.1~1.5 | 明显强于行业 |
| 0.9~1.1 | 与行业同步 |
| 0.5~0.9 | 弱于行业 |
| <0.5 | 显著弱于行业 |
三方分析师独立评分
系统同时运行保守/中性/激进三方分析师,各自输出 0~10 分评分和仓位建议,供不同风格投资者参考。
| 分析师 | 导向 | 核心逻辑 | 仓位范围 |
|---|---|---|---|
| 保守 | 防守 | PE/PB 高估扣分,低估值加分,缠论信号加分 | 0%~30% |
| 中性 | 均衡 | 四维度总分 ÷ 13.5 映射到 10 分 | 跟随系统建议 |
| 激进 | 进攻 | 趋势/缠论/量能/增速加分,容忍高估值 | 10%~70% |
0.5 AI推理分析
作用与价值
基于 LLM 对推荐候选股进行深度推理分析,结合因子数据、K线走势、财务指标、新闻事件等多源信息。因子引擎负责"选什么",LLM 负责"为什么选"——输出可解释的投资逻辑。
实现思路
- LLM 模块只读 ClickHouse,不依赖 Java 计算器
- 批量推理:先生成推荐候选,再对 TopN 逐一分析
- 结果含评分/建议/买入价/止损价/目标价/风险提示
llm_analysis.stop_loss,智能推荐取 suggested_buy_price × 0.92。0.6 盘中监控
作用与价值
解决"推荐了但不知道什么时候该买"的问题。盘中实时监控推荐股价格,信号触发即通过 SSE 推送到前端。
实现思路
- 10秒高频轮询 + 4线程并行拉取K线
- SSE 推送,无需前端轮询
- 同时监控上证/深证/创业板等 9 大指数
监控的股票从哪来
盘中监控列表里的股票不是手工维护的静态清单,而是后端在每个交易日 9:30 监控启动时(或点击「刷新 / 手动触发扫描」时)按下面三个来源实时聚合出来的,列表「来源」列用三种颜色区分:
| 来源(页面标签) | 数据表 | 取数规则 | 买入区间 / 止损 / 目标价如何确定 |
|---|---|---|---|
| 推荐(蓝) | stock_recommendation | 取最新交易日 recommend_date 且 action_tag='BUY'、suggested_buy_price 非空 | 区间 = 建议买入价 ±5%(buyPrice×[0.95, 1.05]);止损优先读 suggested_stop_loss,无则回退 buyPrice×0.92;目标价优先读 suggested_target_price,无则回退 收盘价×1.20 |
| LLM(紫) | llm_analysis | 取最新 analysis_date 且 recommendation='BUY'、buy_price_high 非空 | 直接采用 LLM 给出的 buy_price_low / buy_price_high / stop_loss / target_price |
| 客户定义(金) | monitor_custom_stock | 用户在监控页「添加自定义」手工录入并持久化到数据库 | 用户自行填写买入区间、止损价、目标价 |
何时刷新候选池:监控只在交易时段(09:30–11:30、13:00–15:00)运行,非交易日 / 周末 / 节假日不会重建。页面顶部「数据日期」优先显示推荐日期(一定是交易日),无推荐时回退到 LLM 日期并对齐到前一个交易日。
列表为空怎么办:提示「暂无监控股票,请先通过『智能推荐』生成 BUY 推荐」。即先在「智能推荐」或「AI推理分析」模块跑出当日 BUY 推荐,再到盘中监控点「刷新」即可自动纳入。若
market_regime_calendar 连续 ≥3 个交易日为 BEAR,系统会自动暂停推荐生成,列表可能为空或停留在最近有效推荐,页面以黄色告警提示「连续 N 日熊市,已暂停推荐生成」。关键算法 — 信号判定
2. buyLow ≤ price ≤ buyHigh → IN_RANGE(在买入区间内)
3. buyLow×(1-2%) ≤ price ≤ buyHigh×(1+2%) → WATCH(接近买入区间)
4. price < buyLow → BELOW(低于下沿)
5. price > buyHigh → ABOVE(高于上沿)
0.7 交易日历
作用与价值
统一交易日历逻辑,避免各模块各自判断节假日。定时任务依赖此日历决定是否执行,数据更新依赖此日历确定增量区间。
一、因子
因子是量化投资的原材料——把股票的某种特征抽象为一个数值,用来预测未来收益。平台的因子体系覆盖从定义、计算、测试到优化的完整生命周期。
1.1 因子基础
作用
将股票的某种特征抽象为一个数值,用于横截面排序或打分。例如"过去20日收益率"(MOM20)就是一个因子。
为什么需要因子?
主观选股依赖经验直觉,存在三个硬伤:不可验证("这只股票近期涨得好"——好到什么程度?和谁比?)、不可复制(今天觉得好和明天觉得好可能标准不同)、不可回测(无法在历史数据上验证逻辑是否有效)。
因子把投资逻辑数字化:MOM20 = (今日收盘价 - 20日前收盘价) / 20日前收盘价。有了精确数值,就能在全市场排序、在历史上回测、在不同股票间横向比较。因子是让投资逻辑从"我感觉"变成"数据说"的第一步。
四种类型与治理差异
| 类型 | 实现方式 | 为什么需要这种类型 |
|---|---|---|
| BUILTIN | Java 代码实现,预注册 | 高频计算场景需要极致性能,Java 原生比脚本快 10-100 倍。通用因子做成内置避免每次走脚本引擎 |
| SCRIPTED | Groovy 脚本,沙箱执行 | 用户自定义因子不可能每次都改 Java 重新部署。Groovy 运行时编译、沙箱隔离(禁文件/网络),在灵活性和安全性间平衡 |
| COMPOSITE | 多基础因子线性组合 | 投资逻辑经常是"A+B 因子组合"(如"高盈利+低估")。合成因子定义一次,后续自动展开 |
| PATTERN | 伪因子,走 builtinCalculators | 形态信号(W底/头肩底)不是简单数值计算而是模式识别。PATTERN 类型让形态检测也能进入因子框架统一管理 |
核心因子示例
🚀 动量因子 MOM
公式:MOM_N = (Close_t - Close_t-N) / Close_t-N
捕捉趋势延续——近期涨得多的股票短期倾向于继续涨。背后逻辑是信息扩散不对称:利好消息需要时间被所有投资者消化。
📊 波动率因子 VOL
公式:VOL_N = StdDev(Return_1..N)
衡量价格波动程度。低波动异象的核心:市场对高波动股票过度偏好,导致低波动股票被低估。
💰 市值因子 SIZE
公式:SIZE = log(MarketCap)
从 stock_info 表获取总股本×收盘价。取对数消除量纲差异(千亿和十亿不能直接比较)。
📈 业绩超预期 EARNINGS_SURPRISE
源:stock_earnings_report(业绩预告/财报)。逻辑:实际业绩相对市场一致预期的偏离度。IC≈+0.09、IR≈1.16,是当前预测力最强的新因子之一,自适应权重 1.2(正向加权)。
🐉 龙虎榜机构净买 LHB_INST_NET
源:stock_sentiment_lhb_inst(龙虎榜机构买卖)。逻辑:近 20 日机构龙虎榜净买入累计。机构流入代表聪明钱认可,但 IR 偏弱(≈-0.66),系统自适应降权至 0.5。
🔍 机构调研 INST_RESEARCH
源:stock_sentiment_survey(机构调研事件)。逻辑:近 90 日机构调研次数(inst_count 累计)。关注度上升常先于基本面变化,数据源由旧表 stock_institution_research 迁移而来,已切换为 5 万+条的高覆盖表。
因子分类标签
平台因子按投资逻辑分为以下类别,每类有独立的 IC 分析和推荐管线处理方式:
| 类别标签 | 代表因子 | 特点 |
|---|---|---|
| TECHNICAL 技术 | MOM5/20/60, VOL20, VOLUME_RATIO, TURNOVER_ANOMALY | 短期有效性强,含动量/波动/量价/趋势 4 个子类 |
| FINANCIAL 财务 | FIN_ROE, FIN_NET_PROFIT_YOY, FIN_EARNINGS_QUALITY, FIN_RD, EARNINGS_SURPRISE | 长期有效,稳定性好,季频更新 |
| VALUATION 估值 | VAL_PE_PERCENTILE, VAL_PB, VAL_DIVIDEND_YIELD, VAL_FCF_YIELD | 均值回归特性,需配合历史分位 |
| SENTIMENT 情绪 | MARGIN_BUY_RATIO, LHB_INST_NET, INST_RESEARCH, 龙虎榜信号, 研报热度 | 反映市场情绪,短期波动大 |
| PATTERN 形态 | W底, 头肩底, 双底, 三角突破 | 模式识别类伪因子,走 builtinCalculators |
| SIZE 规模 | SIZE (log市值) | 独立分类,IC+0.052 全市场最有效因子之一 |
1.2 因子计算
作用
批量计算全市场因子值并入库,是所有后续分析的数据基础。
为什么要预计算而不是用时算?
全市场 5000+ 只股票、35+ 个因子,如果每次选股或回测时实时计算,单次请求需要数分钟。预计算后存入 ClickHouse,查询时直接读取,响应在毫秒级。
更关键的是增量计算:每日收盘后只需计算最新一天的因子值(昨天之前的数据没变)。全量算 3 年数据可能要 30 分钟,增量算 1 天只需几秒。
三种计算模式
全量模式
从指定开始到结束日期逐日计算。何时用:首次部署、数据补全、因子逻辑修改后重算历史。
增量模式
只算最新交易日。何时用:每日收盘后例行更新。系统自动查最新日期,从次日开始计算。
单因子模式
只算选中的单个因子。何时用:调试新因子、修正某因子的历史数据错误。
关键算法
- 计算器路由:BUILTIN/PATTERN 走 builtinCalculators Map;SCRIPTED 走 Groovy 沙箱。统一返回 FactorValue 对象
- 并行化:CompletableFuture 5 线程并行。原串行 5000 只需 ~10 分钟,并行后 ~2 分钟。线程设 5 是因为 ClickHouse 并发查询限制
- 因子状态机:DRAFT → TESTING → ACTIVE → DEPRECATED;另有 DEGRADED(降级,自动从推荐管线过滤)。新建因子必须先经过测试验证才能进入生产
- 数据源分离:技术因子从 CH stock_daily 读K线(日频);财务因子从 MySQL 财务表读(季频)
1.3 因子值查看
作用
查询指定日期、指定因子的全市场因子值,支持搜索和排序。
为什么需要验证?
因子计算涉及多数据源拼接、复权处理、缺失值填充,任何环节出错都会导致因子值异常。比如某只股票 MOM20 突然变为 500%(正常不超过 50%),可能是复权因子错误或除零异常。因子值查看是发现问题最快的方式。
1.4 因子测试
作用
评估因子的预测能力——因子值能否预测下期收益。这是决定"用不用这个因子"的核心依据。
为什么不能"凭经验判断因子有没有用"?
人脑擅长事后解释("这个因子当然有效,因为它衡量了动量"),但不擅长事前预测。一个因子可能在 2020 年表现极好、2021 年完全失效,如果只看 2020 年的数据就会得出错误结论。
因子测试的核心目标:用统计方法替代主观判断,在足够长的历史区间上验证因子是否有稳定的、统计显著的预测能力。平台用IC 法 + 分组法双重验证,因为两者各有所长、互为补充。
方法一:IC 法(信息系数法)
IC 为 Pearson 相关系数,取值 [-1, 1]
对每日计算 IC 后汇总统计:IC 均值 / ICIR / 正值率 / t-stat
IC 为什么能衡量因子有效性?
IC 本质上回答一个问题:"因子值高的股票,下期收益是不是也高?" 如果是,IC 为正且越大越好。
IC 是横截面相关系数——同一天对所有股票,算"因子值排名"和"下期收益排名"的相关性。如果这个相关性在大多数交易日都为正,说明因子确实有预测方向的能力。
但 IC 有三个盲区:
- 只看线性关系:如果因子对 Top 10% 极有效、对中间 80% 完全无效,IC 会把这两部分平均掉,显示"中等有效"——但实际上因子只在极端区域有效
- 不告诉你收益幅度:IC=0.05 意味着有正相关,但不告诉你 Top 组比 Bottom 组多赚多少。可能统计显著但经济无意义(年化多赚 0.1%)
- 不告诉你单调性:因子值从第 1 名到第 10 名的收益是否依次递增?IC 只给一个汇总数字,看不出分布形状
| 指标 | 含义 | 为什么需要这个指标 |
|---|---|---|
| IC 均值 | 所有交易日 IC 的算术平均 | 单日 IC 波动极大,需要均值判断整体方向。|IC|>0.03 有意义 |
| ICIR | IC均值 / IC标准差 | IC 均值高但如果波动也大(时好时坏),实际不可用。ICIR 衡量"是不是只有偶尔有效" |
| IC 正值率 | IC > 0 的交易日占比 | 即使 IC 均值为正,如果只有 40% 天数为正,说明因子经常反向。>55% 才有意义 |
| IC t-stat | 统计显著性检验 | 排除"IC 均值碰巧为正"的可能。|t|>1.96 表示 95% 置信度下 IC 不是随机噪声 |
方法二:分组回测法
将股票按因子值从大到小分 N 组(通常 5 或 10 组),持有一定期限后比较各组收益。
分组法为什么能弥补 IC 的盲区?
分组法直接回答 IC 无法回答的问题:"如果我按因子值选股,实际能赚多少?"
- 看分布形状:分组后看每组收益,如果从 Q1 到 Q5 收益依次递增,说明因子在整个分布上都有效(单调性好)。如果只有 Q1 大幅赚钱、Q2-Q5 差不多,说明因子只在极端区域有效——容量有限,只适合小资金
- 看收益幅度:Top 组年化 15%、Bottom 组年化 -5%,多空收益 20%——这是 IC 无法告诉你的经济意义
- 看非线性:如果 Q1 赚钱、Q2 亏钱、Q3 又赚钱,说明因子和收益不是线性关系——IC 值会很低(因为 IC 假设线性),但分组法能清楚看到问题
| 指标 | 含义 | 为什么需要这个指标 |
|---|---|---|
| 单调性 | 分组收益是否单调递增 | 单调性好意味着因子值越高收益越高——这是因子能用于排序选股的前提。不单调的因子只适合做阈值筛选 |
| 多空年化收益 | 做多 Top 组 + 做空 Bottom 组 | 衡量因子的经济价值——统计显著但只赚 0.5% 的因子没有实际意义 |
| 分组 IR | (Top-Bottom)年化 / 多空标准差 | 类似 Sharpe 的概念,衡量多空组合的稳定性 |
为什么必须 IC + 分组双重验证?
两者结论可能不一致,而不一致本身就是重要信息:
- IC 显著但分组单调性差:因子整体有方向预测力,但不是"值越高越好"。可能需要做非线性变换(如取绝对值或分段)
- 分组单调但 IC 不显著:因子在极端区域有效(Top 10% 大幅赚钱),但中间区域无效。IC 把大部分权重给了中间区域拉低了平均值。这种因子适合做阈值筛选而非全市场排序
- 两者都显著:因子既方向正确又经济有效,最理想的情况
- 两者都不显著:因子无效,直接弃用
平台判断标准:满足 8 条标准中任意 3 条以上才有参考价值。以分组法为主——因子最终要能选出赚钱的股票。
为什么 |IC| < 0.015 的因子直接剔除而非反转使用?
IC 为负的因子可以反向使用——值越低收益越高,这本身是有价值的信息。但 |IC|<0.015 时,IC 值在统计上不显著——可能是随机波动的噪声,也可能是真实但极弱的信号。
如果对噪声因子做反转,相当于"对随机噪声取反"——结果仍然是随机噪声。更糟糕的是,噪声因子加入多因子组合后会稀释有效因子的信号。所以直接剔除是比反转更安全的选择。
1.5 因子正交化
作用
去除因子之间的共线性,提取各因子的独立信息。
不做正交化会怎样?
假设策略同时用 MOM20 和 MOM60,相关性 0.8。你给它们各 20% 权重,以为动量总权重是 40%。但实际上它们携带的信息有 80% 重叠——实际动量信息权重接近 36%,而非 40%。
更严重的是,如果再加一个 MOM5(与 MOM20 相关 0.9),你以为加了三个不同因子做了分散化,实际上你在同一信息上加了三倍杠杆。一旦动量因子失效,三个因子同时失效,组合遭遇集中打击。
正交化就是:对每个因子,把它能被其他因子解释的部分剥离掉,只保留"其他因子无法解释的独立信息"。正交后因子间相关性为零,每个因子的权重真正代表它对组合的独立贡献。
两种方法的选择逻辑
施密特正交化(Gram-Schmidt)
按给定顺序依次处理。为什么顺序重要:第一个因子保持完整含义,后续因子被剥离与前者的相关部分。排在前面的因子保留更多信息。一般按因子重要性从高到低排列。
对称正交化(SVD)
基于矩阵奇异值分解,对所有因子平等处理。什么时候用:不确定哪个因子更重要时。代价是所有因子的原始含义都被改变。
关键算法
proj_u(v) = (v·u / u·u) × u
1.6 因子衰减分析
作用
研究因子预测能力随持有期延长的变化规律,确定最优调仓频率。
为什么因子会衰减?
市场是自适应的——如果一个因子持续有效,越来越多的投资者会发现并使用它,套利行为会逐步消除这个 Alpha。这就是"因子拥挤"的本质。
衰减速度因类型而异:动量因子衰减快(半衰期通常 3-10 天);价值因子衰减慢(半衰期可达 30-60 天)。
知道衰减速度有什么用?调仓频率必须匹配衰减速度。衰减快的因子如果月频调仓,信号早已过期——你交易的是上个月的动量。衰减慢的因子如果日频调仓,交易成本远超 Alpha 收益——你在为噪声支付佣金。
关键算法
半衰期 < 5天 → 日频;5~15天 → 周频;> 20天 → 月频
推荐管线 P2 步骤即半衰期检测:根据衰减速度为因子分配调仓建议。
1.7 因子相关性分析
作用
研究不同因子之间的相关性,是正交化前的诊断步骤。
为什么要先诊断再正交化?
正交化是"治疗",相关性分析是"诊断"。不诊断就治疗,可能过度处理——把相关性 0.3 的因子也做正交化,反而损失了有用信息。
诊断逻辑:|corr|>0.7 是危险区(信息重叠严重,必须处理);0.4-0.7 是警戒区(可保留但注意权重);<0.4 是安全区(可直接使用)。
经验规律:同类技术因子之间天然高相关(MOM5/MOM20/MOM60 都衡量动量,只是窗口不同);基本面与情绪因子通常低相关,组合使用能获得更好的信息增量。
关键算法
1.8 因子权重优化
作用
为多因子组合找到最优的因子权重配置。
为什么不等权?
等权(每个因子 1/N)看起来公平,但实际不公平——它假设所有因子同等重要。但 IC=0.06 的因子和 IC=0.01 的因子显然不该有相同权重。等权配置会让弱因子"搭便车",稀释强因子的贡献。
更隐蔽的问题是:如果某些因子高相关(如三个动量因子),等权配置下动量的总权重 = 3/N,可能远超你预期的配置比例。这本质上是无意中的风险集中。
三种方法的本质区别
| 方法 | 原理 | 什么时候用,什么时候不用 |
|---|---|---|
| 等权 EQUAL | w_i = 1/n | 用:因子少(3-5个)、相关性低、缺历史数据。不用:因子多且强弱差异大时,等权浪费强因子 |
| 均值-方差 MARKOWITZ | max Sharpe = (w^T μ) / √(w^T Σ w) | 用:因子收益稳定、有足够历史数据。不用:因子收益波动大时会产生极端权重(某因子占 80%),样本外极不稳定 |
| 风险平价 RISK_PARITY | RC_i = 1/n | 用:希望各因子风险贡献均衡。优势:权重相对稳定,不会像 Markowitz 产生极端配置 |
关键算法
- Markowitz:求解二次规划
max w^T μ - λ × w^T Σ w。本质是在"收益最大化"和"风险最小化"间找平衡 - Risk Parity:迭代求解使各因子风险贡献相等。不是权重相同,而是风险相同——低波动因子获得更高权重
1.9 因子健康监控
作用
自动检测因子预测能力的衰减,对失效因子降级处理,对恢复的因子自动复活。
为什么需要自动监控?
因子不是一劳永逸的——市场环境变化会导致有效因子失效。SIZE 因子在 2017 年小盘行情中表现极好,但在 2021 年大盘蓝筹行情中完全失效。如果系统继续使用已失效的因子,推荐质量会持续恶化。
人工检查 35+ 个因子每天的状态是不现实的。FactorHealthMonitor 每日 22:00 执行,滑动窗口对比近期 IC 均值与历史均值。若近期 IC 显著低于历史均值,因子状态设为 DEGRADED。
降级的效果:DEGRADED 因子在推荐管线 P4 步骤自动被跳过,不再参与选股。这不是删除——如果后续 IC 恢复到阈值以上,因子自动复活为 ACTIVE。这种"弹性降级"避免了"一刀切"的风险:暂时失效不等于永久失效。
1.10 新建因子
作用
创建自定义因子,扩展平台的因子库。
新建因子必须经过测试验证(步骤3)才能进入 ACTIVE 状态,确保不会因为一个未验证的因子污染推荐管线。
SCRIPTED 因子 Groovy 脚本开发
SCRIPTED 类型因子使用 Groovy 脚本在沙箱中执行(禁文件/网络访问)。脚本可使用以下预注入变量:
| 变量 | 类型 | 说明 |
|---|---|---|
history | List<Map> | 近 N 日 K 线数据(含 date/open/high/low/close/volume/amount) |
bar | Map | 当日 K 线数据 |
close | BigDecimal | 当日收盘价 |
n | int | 计算窗口天数 |
symbol | String | 股票代码(纯数字) |
calcDate | LocalDate | 计算日期 |
脚本示例 — 自定义动量因子
def prices = history.collect { it.close }
if (prices.size() < n) return null
def pastPrice = prices[prices.size() - n]
return (close - pastPrice) / pastPrice
因子值标准化
计算完成后,因子值经过 Z-Score 标准化处理(减均值除标准差),消除量纲差异,使不同因子可在同一尺度上比较和加权。标准化后的值称为 rank_value,范围 0~1(百分位归一化)。
二、策略
策略是将因子组合起来形成完整选股逻辑的载体。单个因子只能告诉你"哪个方向好",策略才能告诉你"选哪些股票、各占多少仓位、什么时候调仓"。
2.1 策略定义
作用
定义选股规则、因子权重和持仓管理参数,形成可执行的量化选股策略。
为什么要把选股逻辑"正式化"为策略?
"买入盈利能力强、估值低的股票"是一句正确的废话——它无法执行。什么是"盈利能力强"?ROE > 10% 还是 > 15%?选多少只?每只买多少?
策略把模糊逻辑转化为精确配置:因子方向、因子权重、阈值过滤、持仓数量、仓位限制。正式化后,策略变得可回测(在历史数据上验证)、可对比(和基准或其他策略比较)、可优化(调整参数找最优配置)、可复制(明天执行和今天执行结果一致)。
策略三要素
① 选股规则
定义因子方向:direction=1 越大越好,direction=-1 越小越好。支持阈值过滤如 FIN_ROE(>10%)——直接剔除不符合最低要求的股票
② 因子权重
设置各因子在综合评分中的权重。可通过因子权重优化模块自动寻找最优权重
③ 持仓管理
最大持仓数量、个股仓位限制。支持买入条件(技术面+估值面建议价)和止盈止损(基于 ATR)
关键算法
w_j 为第j个因子权重,z_ij 为第i只股票第j个因子的标准化值
2.2 预设组合
作用
12 个经过验证的预设因子组合,覆盖不同投资风格,开箱即用。
| 组合名称 | 风格 | 核心因子 |
|---|---|---|
| ⚖️ 经典多因子 | 中长期稳健型 | SIZE(-1), MOM20, VOL20(-1), FIN_ROE, FIN_REVENUE_YOY |
| 🚀 小盘成长 | 高弹性/高风险 | SIZE(-1,2x), FIN_REVENUE_YOY(>10%), FIN_ROE(>8%) |
| 🛡️ 低波动红利 | 稳健型 | VOL20(-1,2x), FIN_EARNINGS_QUALITY(>0.5) |
| 📊 技术动量 | 短线/技术面 | MOM20(1.5x), MOM60, VOLUME_RATIO, BOLL_POS |
| 💎 价值投资 | 长期持有 | FIN_EARNINGS_YIELD(1.5x), FIN_BPS(>3), FIN_GROSS_MARGIN(>20%) |
| 📈 趋势突破 | 趋势跟踪 | MOM60(>5%,2x), VOLUME_RATIO(>1,1.5x) |
| 🏆 高盈利质量 | 价值成长/机构 | FIN_ROE(>10%,2x), FIN_EARNINGS_QUALITY(>0.8) |
| ⚡ 成长加速 | 成长型/VC思路 | FIN_REVENUE_YOY(>15%,2x), FIN_NET_PROFIT_YOY(>20%,2x) |
| 🔄 低估反转 | 逆向投资 | REVERSAL5(1.5x), MOM5(≤40,1.5x), FIN_BPS(>2) |
| 🔥 量价异动 | 短线博弈 | TURNOVER_ANOMALY(>1.2,2x), VROC12(>0) |
| 🏥 财务健康 | 防御型/风控优先 | FIN_CURRENT_RATIO(>1.5), FIN_DEBT_TO_ASSET(<50%,1.5x) |
| 🎯 综合打分 | 平衡型/长期配置 | FIN_ROE, FIN_REVENUE_YOY, MOM60, VOL20, SIZE, 八因子均衡 |
2.3 因子策略选股
作用
根据策略配置的因子+权重,在全市场范围内筛选并排序股票。
为什么选股不是"算个分数取 Top N"这么简单?
直接用因子原始值加权求和取 Top N,会遇到三个致命问题:
- 量纲灾难:PE 可以从 5 到 500,ROE 从 0.05 到 0.50。如果不做标准化,PE 的数值范围远大于 ROE,综合得分实际上被 PE 主导——你配置的权重完全失效
- 极端值污染:某只股票 PE=10000(可能是亏损导致 PE 异常),这个极端值会把整个评分拉偏
- 趋势盲区:因子只衡量"静态属性",不衡量"趋势方向"。一只股票可能有极好的基本面因子值,但正处于下跌趋势中——买入后继续跌
所以选股流程需要 6 步处理,每步解决一个具体问题:
关键算法
- 极值处理(Winsorize):将超出 N 倍标准差的值拉回边界。解决极端值污染。为什么不删除?因为极端值可能代表真实信息(如高增长股确实 PE 很高),截尾保留信息但限制影响
- 标准化(Z-Score / Rank):将不同量纲的因子值统一到相同尺度。Z-Score = (x-μ)/σ,处理后均值0标准差1。Rank 归一化将值转为排名百分位(0-1),更抗极端值但损失幅度信息
- 均线位置过滤:批量计算候选股票 MA 值和位置关系。MA30(约1.5月/短期)、MA60(约3月/中期)、MA100(约5月/最严格)。多选取交集——必须同时站上 MA30 和 MA60 才入选。解决趋势盲区:只在上涨趋势中买入,避免"接飞刀"
- 买入建议价:技术面(MA支撑+布林带+ATR)+ 估值面(BPS价值锚+PE历史位)双维度。不只是选股,还告诉你"在什么价格买"
2.4 策略置信度系统
作用
根据策略的历史表现数据量,动态调整推荐股票数量——新策略少推、成熟策略多推。
为什么要分级,而不是一开始就推满?
一个刚上线的策略没有任何历史表现数据——你不知道它是否真的有效。如果第一天就推荐 20 只股票,等于让用户用真金白银验证一个未经验证的策略。
置信度系统的逻辑是"先验→后验"逐步更新:策略刚开始时用先验中性分(55分,略偏乐观),只推荐少量股票(3-5只);随着每次推荐的实际表现被追踪回来,系统逐步了解策略的真实水平,推荐数量逐步放开到 Top N。
这样做的好处:如果策略实际效果差,早期推荐的少→损失可控→系统检测到低命中率后自动降级。如果策略效果好,逐步放量→最终达到满仓推荐→同时积累了足够的历史数据支撑这个决策。
P0-P3 四级冷启动
| 级别 | 条件 | 行为 | 为什么这么做 |
|---|---|---|---|
| P0 | 样本量 < 5 次 | 返回 Math.max(3, topN/5) 探底数量 | 5 次以下统计无意义。先验中性分 55,推荐最少 3 只"试水" |
| P1 | 5-20 次 | ICW/STATIC 分离 + append 模式 | 有少量数据但不够稳定,ICW 权重和 STATIC 权重并行计算取较保守者 |
| P2 | 20-100 次 | stdDev<2 返回 5.0 + 非叠乘 | 波动率数据开始稳定,但如果标准差异常小说明可能数据不足,用固定值兜底 |
| P3 | > 100 次 | 完整置信度计算 | 样本充足,统计结果可信。推荐数量根据命中率动态调整 |
三、回测
回测是在历史数据上模拟交易,验证策略在过去是否有效。核心原则:回测不是预测未来,而是验证策略逻辑的稳健性——如果策略在过去 3 年的各种市场环境下都能稳定盈利,它在未来有效的概率比一个只在 1 个月内表现好的策略高得多。
3.1 回测配置
作用
设置回测的参数条件,模拟策略在历史区间内的交易表现。
为什么参数配置决定了结果的可信度?
回测最大的风险不是"策略不好",而是"回测好但实盘不行"。这种差距通常来自配置不当引入的偏差——最典型的是未来函数偏差。
未来函数的意思是:你在 t 时刻用了 t 时刻才知道的信息做决策。比如用收盘价作为买入价——你"看到"了收盘价才决定买,但实际操作中你不可能精确地在收盘价成交。这会系统性地高估收益,让一个无效的策略看起来很赚钱。
6 项核心参数
| 参数 | 说明 | 为什么这个参数重要 |
|---|---|---|
| 回测区间 | 起止日期,建议至少 2 年 | 区间太短可能恰好处于单边行情,无法检验策略在不同市场环境下的稳健性 |
| 初始资金 | 默认 1,000,000 元 | 影响仓位计算和流动性判断。资金太少无法分散;太大可能超出个股流动性容量 |
| 基准选择 | 沪深300/上证指数等 | 超额收益 = 策略收益 - 基准收益。选错基准会误判 Alpha——用沪深300 做基准评估小盘策略会高估 Alpha |
| 交易费用 | 佣金+印花税0.1%+过户费+滑点(默认VOLUME模型) | 不设费用的回测严重高估收益。滑点默认采用 VOLUME 成交量模型:滑点 = 基础费率 × (1 + √(交易额/日成交额) × 系数),大额交易自动惩罚。另有 8% 最大参与率约束,单笔买入不超过当日成交量的 8% |
| 调仓频率 | 每日D/每周W/每月M | 频率越高成本越大,但能捕获快速衰减的 Alpha。必须和因子衰减半衰期匹配 |
| 成交模式 | CLOSE / NEXT_OPEN / VWAP | 最关键的偏差控制,详见下文 |
成交模式为什么是最关键的配置?
CLOSE 模式:用当日收盘价作为成交价。问题在于——收盘价是 t 时刻的最后价格,你用因子值(也是 t 时刻的数据)算出信号,然后在同一个收盘价成交。这意味着你"看到了"收盘价才决定买不买,但实际中你不可能在收盘的瞬间知道收盘价并下单。
NEXT_OPEN 模式:用次日开盘价成交。信号在 t 日收盘后生成,在 t+1 日开盘执行。这模拟了真实操作——你今天收盘后看到信号,明天开盘下单。这是最接近实盘的成交模式。
CLOSE 模式引入的未来函数偏差可能让年化收益虚高 5-15%。一个在 CLOSE 模式下年化 25% 的策略,切到 NEXT_OPEN 可能只有 12%——差距全部来自未来函数偏差。
3.2 绩效指标
作用
量化评估回测策略的表现,提供收益、风险、风险调整收益三个维度共 15 项指标。
为什么不能只看年化收益率?
年化 30% 的策略听起来很好,但如果最大回撤 50% 呢?意味着在某个阶段你的账户从 100 万跌到 50 万——大多数人会在底部割肉出局,根本等不到反弹。
更隐蔽的问题是波动率:两个策略都年化 20%,但一个每月稳定 +1.5%,另一个大起大落(某月 +30%,下月 -20%)。前者可以安心持有,后者你大概率会在某次大跌后恐慌卖出。
所以需要 15 个指标从三个维度交叉评估:收益告诉你"赚了多少",风险告诉你"受了多少折磨",风险调整收益告诉你"每承受一单位风险换来了多少收益"。
关键指标与公式
| 类别 | 指标 | 公式 | 为什么需要这个指标 |
|---|---|---|---|
| 📈 收益 | 年化收益率 | (1+总收益率)^(250/交易日数)-1 | 把不同区间收益统一到年化口径,便于跨策略比较 |
| 超额收益 | 策略收益 - 基准收益 | 如果策略赚 15% 但基准赚 20%,策略实际是亏的。超额收益才反映真正的选股 Alpha | |
| 胜率 | 盈利交易次数 / 总交易次数 | 胜率低但盈亏比高的策略也能赚钱,但胜率 <30% 心理承受压力太大 | |
| ⚠️ 风险 | 最大回撤 | max(Peak-Valley)/Peak | 最直观的风险感受——"我从最高点跌了多少"。>30% 大多数人扛不住 |
| 年化波动率 | StdDev(日收益率) × √250 | 衡量收益的稳定性。波动率高=收益不可预测 | |
| 下行波动率 | StdDev(负收益日) × √250 | 只衡量下跌风险——上涨的波动是好事,不需要惩罚 | |
| ⚖️ 风险调整 | 夏普比率 | (年化收益-无风险利率)/年化波动率 | 每承受一单位总风险换来的收益。>1.5 意味着收益远超风险代价 |
| 索提诺比率 | (年化收益-目标)/下行波动率 | 夏普的改进版——只惩罚下行风险。>Sharpe 说明下行控制好 | |
| Calmar比率 | 年化收益/最大回撤 | 收益和最大回撤的比值。>2.0 意味着每 1% 回撤换来 2% 收益,性价比极高 | |
| ⚖️ 风险调整 | 信息比率 | 超额收益均值/跟踪误差 | >0.5 稳定超额;>1.0 优秀;<0.3 不稳定 |
| 盈亏比 | 平均盈利/平均亏损(绝对值) | >1.5 较好;30%胜率×3盈亏比=不错策略 | |
| ⚠️ 风险 | VaR(95%) | Percentile(日收益, 5%) | 95% 置信下单日最大亏损,越小越好 |
指标评价标准速查
| 指标 | 极优秀 | 较好 | 较弱 |
|---|---|---|---|
| 年化收益率 | >15% | 8~15% | <8% |
| 超额收益(年化) | >5% | 2~5% | <2% |
| 最大回撤 | <10% | 10~20% | >30% |
| 夏普比率 | >2.0 | 1.0~2.0 | <1.0 |
| Calmar 比率 | >2.0 | 1.0~2.0 | <1.0 |
| 信息比率 | >1.0 | 0.5~1.0 | <0.3 |
| 胜率 | >50% | 40~50% | <40% |
| 盈亏比 | >2.5 | 1.5~2.5 | <1.5 |
对比选优 5 步建议
- 先看风险:剔除最大回撤超过承受能力的策略(如 >30%)
- 再看收益:剩余策略中优先选年化收益最高的
- 综合评价:用夏普比率排序,夏普 >1.0 的策略具有实战价值
- 验证稳健:选择在不同市场区间(牛/熊/震荡)均有超额收益的策略
- 考虑容量:换手率高、交易次数多的策略实盘容量有限
3.3 回测报告
作用
提供完整的回测分析报告,从收益、风险、交易、归因多维度解读策略表现。
为什么区分"已实现收益"和"浮动盈亏"?
账户净值 = 已实现盈亏 + 浮动盈亏。浮盈是"还没落袋的利润"——明天可能全部蒸发。回测报告同时展示已实现收益曲线(FIFO 配对),让你区分"纸面富贵"和"真金白银"。
如果一个策略净值持续新高但已实现收益一直为负,说明它完全靠浮盈撑着——一旦趋势反转,利润会迅速消失。这种策略的风险远高于净值曲线所显示的。
📈 净值曲线
策略 vs 基准净值。越平滑越好——锯齿状曲线意味着高波动
📊 超额收益曲线
策略-基准,持续在0轴上方说明跑赢基准
💰 已实现收益曲线
仅已平仓交易盈亏(FIFO配对),避免"纸面富贵"
📉 回撤曲线
从历史高点到当时的亏损幅度。回撤深且持续久 = 高风险
📋 交易明细
每次买卖的日期、股票、价格、数量、手续费、盈亏
🔬 归因分析
收益来源分解(见评估模块)
3.4 策略对比
作用
同时比较 2-4 个策略的回测表现,帮助选择最优策略。
为什么需要对比?
单独看一个策略年化 20%、Sharpe 1.2 觉得不错。但如果另一个策略年化 18%、Sharpe 1.8 呢?收益略低但风险调整后收益更好。
更关键的是:不同策略可能在不同市场环境下表现不同。策略 A 在牛市跑赢,策略 B 在熊市跑赢。如果不做对比,你可能选了一个"顺境冠军逆境废柴"的策略——而你自己不知道。
| 维度 | 核心指标 | 选优逻辑 |
|---|---|---|
| 收益对比 | 年化/总/超额/Alpha | 年化高且超额持续为正 |
| 风险对比 | 最大回撤/波动率/VaR | 回撤小、波动低。同等收益下风险越小越好 |
| 风险调整 | Sharpe/Sortino/Calmar | 综合最优——兼顾收益和风险 |
| 交易效率 | 胜率/盈亏比/换手率 | 换手率低=交易成本低 |
| 一致性 | 月度正收益占比/滚动Sharpe | 每月都赚钱的策略比"大起大落"的更可靠 |
3.5 参数优化(网格搜索)
作用
系统性地遍历参数组合,找到最优配置。
为什么需要网格搜索?为什么又必须检查过拟合?
策略参数(因子权重、调仓频率、持仓数量)有多种组合。网格搜索遍历所有组合,找到使 Sharpe 最大的配置。
但这里有一个致命陷阱:如果你在 1000 种参数组合中找到 1 个 Sharpe=2.5 的,这个"最优"可能是纯粹的运气——就像扔 1000 次硬币总有一次连续 10 次正面。这个参数组合在过去表现极好,但在未来可能完全无效。这就是过拟合。
平台用两种方法识别过拟合:
- 参数敏感性分析:如果最优参数附近的小幅变动导致 Sharpe 大幅下降(如权重从 0.3 变到 0.28,Sharpe 从 2.5 降到 0.8),说明这个"最优"是脆弱的——对参数极其敏感的配置几乎一定是过拟合
- 样本外验证:把数据分为训练集和测试集,用训练集找最优参数,用测试集验证。如果测试集表现大幅下降,就是过拟合。Walk-Forward 验证(5.6 节)是更严格的样本外检验
可优化参数(8 个)
| 参数 | 说明 | 建议取值范围 |
|---|---|---|
| 止损比例 | 持仓亏损达到此比例自动卖出 | 3%~10% |
| 止盈比例 | 持仓盈利达到此比例自动卖出 | 5%~20% |
| 最大持仓数 | 同时持有的最大股票数量 | 5~30 只 |
| 初始资金 | 回测初始资金规模 | 10万~1000万 |
| 回看天数 | 计算技术指标的历史K线数量 | 10~120 天 |
| 持仓周期 | 买入后强制持有天数 | 5~60 天 |
| 入选分数阈值 | 综合评分最低要求 | 50~80 分 |
| 滑点比例 | 成交价相对理想价的偏离 | 0.1%~0.5% |
目标函数选择
| 目标函数 | 公式 | 适用场景 |
|---|---|---|
| Sharpe(推荐首选) | (年化收益-无风险)/年化波动率 | 综合考虑收益和风险,稳健不易过拟合 |
| 年化收益率 | (期末/期初)^(252/交易日)-1 | 追求绝对收益最大化 |
| Calmar | 年化收益/最大回撤 | 对回撤敏感,追求极端风险平衡 |
结果判断标准
| 指标 | 优秀 | 可接受 | 需警惕 |
|---|---|---|---|
| Sharpe 比率 | >2.0 | 1.0~2.0 | <1.0 |
| 年化收益率 | >30% | 15~30% | <15% 或 >100%(过拟合) |
| 最大回撤 | <15% | 15~30% | >30% |
| 胜率 | >55% | 45~55% | <45% |
| 盈亏比 | >2.5 | 1.5~2.5 | <1.5 |
两阶段优化策略
- 粗搜:范围模式,大步长快速扫描(如止损 [3%, 5%, 8%, 10%, 15%])
- 细搜:在优秀区域用列表模式精确测试(如发现 5%~8% 最优,精细测 [5%, 6%, 7%, 8%])
3.6 从回测到实战
作用
分析回测与实盘的差距来源,提供实盘前的最后检查清单。
为什么回测好不等于实盘好?
回测和实盘之间至少存在 4 个系统性差异:
- 滑点:回测默认采用 VOLUME 成交量模型,交易额越大滑点越高;同时有 8% 最大参与率约束,大额买单自动缩量至当日成交量的 8% 以内。但实盘中极端情况(涨停买不进、跌停卖不出)仍无法完全模拟
- 流动性约束:回测有容量约束和组合流动性风控,但实盘中涨停股买不进、跌停股卖不出仍是回测无法覆盖的。回测不会告诉你"你想买的时候买不到"
- 行为偏差:回测中你严格执行策略信号,实盘中你可能在亏损 20% 时恐慌卖出——即使策略说"持有"。行为偏差是最难量化但影响最大的因素
- 数据延迟:回测用历史数据,实盘数据有延迟。因子计算依赖的收盘价在实盘中可能因为数据源延迟而晚几分钟才可用
经验法则:实盘收益通常是回测的 50-70%。如果回测 Sharpe < 1.0,实盘大概率亏损。在回测 Sharpe > 1.5 时,才值得考虑实盘。
四、选股/推荐
选股和推荐是将策略配置转化为具体股票列表的执行环节。平台提供因子选股(手动执行策略配置)和智能推荐(P1-P6 管线自动处理)两条路径。
4.1 智能推荐管线(P1-P6)
作用
全自动选股管线——从因子池出发,经过 6 步筛选和加权,输出当日最优股票推荐列表。
为什么不直接"算个综合分数取 Top N"?为什么要 6 步?
最简单的选股方法:把所有因子加权求和,取分数最高的 N 只股票。这看起来合理,但实际有 6 个致命的失败模式——每一步管线解决其中一个:
- 噪声因子污染:因子池里有 35+ 个因子,并非全部有效。IC≈0 的因子加入打分等于加入随机噪声,会稀释有效因子的信号
- 信号过期:因子可能在历史上有效但已经衰减。用已失效的因子选股,等于用昨天的新闻做今天的决策
- 因子拥挤:一个因子如果被太多人使用,它的 Alpha 会被套利掉。大家都买低 PE 股,低 PE 就不再是 Alpha 来源
- 数据过期:财务因子季频更新,3 月份用的是去年 12 月的数据。如果不做校正,系统误以为这些数据是"最新"的
- 不稳定因子:有些因子在某些年份极好、某些年份极差。只用均值 IC 会误判这种因子为"有效",但实际使用时盈亏交替不可控
- 权重失衡:如果不用 IC 加权而用等权,弱因子和强因子同等对待。如果不加权重上限,一个因子可能占 80%,一旦失效全盘崩溃
6 步管线详解
P1 IC 筛选 — 滤掉噪声
做什么:用 IC 和 ICIR 筛选有效因子,剔除 ICIR<0.1 的因子。|IC|<0.015 的因子直接剔除(NOISE_DROPPED)。
为什么放第一步?
这是最便宜的质量门——IC 已经在因子测试阶段算好了,这里只是做阈值过滤,计算成本为零。但它能一次剔除 30-50% 的无效因子,大幅减少后续步骤的处理量。
为什么 ICIR 阈值设 0.1?ICIR = IC均值/IC标准差,衡量 IC 的稳定性。ICIR<0.1 意味着 IC 的波动是均值的 10 倍以上——这个因子时好时坏,和随机猜测几乎没有区别。
P2 半衰期 — 判断信号时效
做什么:根据因子衰减速度(半衰期)分配调仓建议。半衰期 <5天→日频,5-15天→周频,>20天→月频。
为什么在 P1 之后?为什么不能和 P1 合并?
半衰期计算需要先知道因子有效(P1 的结论),然后才能分析"有效多久"。如果对无效因子算半衰期,算出来的只是噪声的衰减速度——没有意义。
半衰期解决的核心问题:信号时效性。一个 IC=0.05、半衰期=3 天的动量因子,如果你周频调仓,信号在第 3 天就过期了,后 4 天你交易的是噪声。反过来,半衰期=30 天的价值因子如果日频调仓,你每周多付 5 倍交易成本来捕获几乎不存在的增量 Alpha。
P3 拥挤度 — 避免跟风
做什么:检测因子值分布的集中度。如果太多股票有相似的因子值,说明这个因子信号被市场广泛使用,Alpha 可能已被套利消失。
为什么放在 P2 之后?拥挤度意味着什么?
一个因子可能 IC 高、半衰期长,但如果所有量化基金都在用它,它的 Alpha 早就被吃完了。拥挤度检测的是"这个因子还有没有被市场消化的空间"。
具体来说:如果一个因子的 Top 组股票和市场上热门持仓高度重合,说明这些股票已经被充分定价——你买入的价格已经反映了因子的信号,没有超额收益空间。
为什么在 P2 之后:先确认因子有效(P1)且时效合理(P2),再检查它是否仍然可交易(P3)。一个无效的因子不需要检查拥挤度——它本就不该用。
P4 季频校正 — 修正数据时效
做什么:对财务因子做季频校正——前向填充 + 季频对齐。DEGRADED 状态的因子在此步骤自动跳过。
为什么财务因子需要特殊处理?
技术因子(MOM/VOL)是日频更新的——今天的因子值用的是今天的数据。但财务因子(ROE/净利润增速)是季频更新的——3 月份的财务数据用的是去年 12 月的季报。
如果不做校正,系统会在 3 月 1 日把去年 12 月的 ROE 当作"当前 ROE"使用。这在回测中引入了前视偏差:1 月份你不可能知道 12 月的年报数据(年报 3 月才发布),但系统假设你知道。
前向填充的解决方案:每个时间点使用当时实际可获得的最新财报。1 月份用三季报数据(9 月 30 日的数据,10 月公布),3 月份才切换到年报数据。
为什么在此步过滤 DEGRADED 因子:这是管线中最后一个"数据准备"步骤,后续步骤开始实际选股。在进入选股前做最后清理,确保不使用已失效的因子。
P5 一致性 — 验证稳健性
做什么:验证因子在不同时间区间的 IC 表现一致性。多窗口 IC 检验——把历史数据分成多个窗口,看因子在每个窗口是否都有效。
为什么"平均有效"不够,还要"每个窗口都有效"?
假设一个因子在 2019 年 IC=0.15(极强),2020 年 IC=-0.05(反转),2021 年 IC=0.15(极强)。三年平均 IC=0.08,看起来有效。但实际使用时,你会在 2020 年亏损——因为那一年因子方向反了。
一致性检验就是为了发现这种"平均有效但实际不可用"的因子。只有因子在每个时间窗口都方向一致且显著,才能认为它稳健——不会在某一年突然反转让你亏损。
为什么在 P4 之后:一致性检验需要干净的、经过季频校正的数据。用未校正的数据检验一致性,等于在脏数据上做统计——结果不可靠。
P6 ICW 加权 — 最优权重分配
做什么:用 IC 加权(ICW)为存活到这一步的因子分配权重。IC>0 正向使用,IC<0 反转使用(取绝对值加权),单因子权重上限 35%。IC 按当前市场体制(BULL/BEAR/SIDEWAYS)分别计算——只用同体制交易日的 IC 算衰减均值,消除跨体制 IC 平均导致的反向权重。
为什么用 IC 做权重?为什么限制 35%?IC<0 为什么要反转?
用 IC 做权重的逻辑:IC 衡量因子的预测能力。IC=0.06 的因子比 IC=0.02 的因子强 3 倍,理应获得 3 倍的权重。等权配置忽视了这个差异,让弱因子"搭便车"。
IC<0 为什么要反转而不是丢弃:IC 为负意味着因子值越高、下期收益越低——这本身是有价值的信息。你只需把因子方向取反(值越低排越前),就能把它变成一个正向因子。反转使用 ≠ 硬凑——前提是 |IC| 要显著(通过了 P1 的阈值),不是噪声。
35% 上限的逻辑:如果没有上限,一个 IC=0.08 的因子可能获得 60% 的权重。一旦这个因子失效(市场环境变化),你的策略会直接崩溃。35% 上限强制分散——即使最强的因子也不能独占主导权,确保策略不依赖单一因子。这本质上是用少量的预期收益换取大量的风险分散。
默认权重 0.05 的逻辑:新因子没有 IC 历史数据时,权重设为 0.05(而非 1.0)。防止新因子因为"没有负面记录"就获得高权重——没有证据不等于有效。
为什么是这个顺序?——漏斗设计原理
6 步管线是一个成本递增、候选递减的漏斗:
- P1 最便宜:只需查 IC 阈值,O(1) 操作。一次剔除 30-50% 的无效因子
- P2 较便宜:需要 IC 历史(P1 已算好),做衰减分析。对存活因子操作,数量已减少
- P3 中等成本:需要因子值分布分析。对更少的存活因子操作
- P4 中等成本:需要财务数据对齐。对更少的存活因子操作
- P5 最昂贵:需要多窗口 IC 计算。但此时候选因子最少(可能只剩 10-15 个),计算量可控
- P6 必须最后:需要前面所有步骤的结果才能算权重
如果把昂贵步骤放在前面,会对大量无效因子做无用功。漏斗设计确保每一步只处理"上一步认为值得继续"的因子,计算效率最优。
高级选项
- 中性化:NONE / INDUSTRY(行业中性)/ MARKET_CAP(市值中性)/ BOTH。去除因子值中的行业或市值偏差——比如 ROE 高的股票恰好都是某个行业,中性化后只保留"同行业内 ROE 高"的信息
- 正交化:NONE / SCHMIDT。管线内可选的因子正交化,和 1.5 节原理相同
- 极值处理:NONE / MAD / SIGMA3 / PERCENTILE
- 标准化:NONE / ZSCORE / MINMAX / RANK
- 均线位置过滤:MA30/60/100 多选,只选站上指定均线的股票
输出优化与 Regime 感知(2026-07 增强)
为什么推荐不能"算完分数就直接发 Top N"?
实测 7600+ 条历史推荐发现:综合分数(final_score)若直接取 Top N,61% 的推荐会落在 0.5–0.7 的"噪声区"——该区间次日超额收益接近 0 甚至为负。真正能跑赢的是 0.7–0.9 高分档(次日超额 +0.71%、胜率 57.5%)。因此管线在输出前做了四件事:
- 分位排名归一化:候选批次内对综合分做分位排名((i+0.5)/n 映射到 0–1),强制拉开分数分布。修复前 96% 的股票分数挤在 ≥0.70,无法区分;修复后标准差 0.113→0.263,高分档和低分档清晰分离
- 高置信门槛(相对分位):取当日候选池 final_score 的前 30%(按分数降序取分位值作为门槛);若筛出的标的少于 5 只,则保底按分数取 Top N(N 为策略配置的推荐数量,已含置信度与命中率动态调整)。注:早期版本使用固定门槛 0.70,但实际分数普遍落在 0.62–0.75(分析侧六维分位偏低),达门槛的常年仅 0–2 只,导致推荐恒为 5 只、topN 配置失效,故改为相对分位门槛。
- 熊市动量反转:行业动量融合改为 Regime 感知——BEAR 市况下不再追高动量行业,改为低动量均值回归(高动量行业融合加成 -0.06、低动量 +0.06)。动量因子 IC 在熊市转负,追高反而踩雷
- 噪声因子清理:VOLUME_RATIO、MA5 等 IC 为负或接近噪声的因子已置 DEPRECATED,自动从推荐管线过滤,不再稀释有效信号
- 连续熊市暂停:当市场体制连续 3 日为 BEAR 时,直接不生成推荐——连熊下行中动量反转和低仓位防守都无法有效止损,空仓是最优策略
实测效果:某主策略(strategy 73)推荐数从 32 只收敛到 5 只,且全部落在盈利档(电池、贵金属、工业金属、消费电子),不再包含噪声区标的。
4.2 权重模式
作用
控制推荐管线 P6 步骤中因子权重的计算方式。
为什么需要三种模式?
- STATIC(固定等权):所有因子等权,跳过 IC 修正。用:因子数量少、IC 历史数据不足、或想保持固定配置不受近期 IC 波动影响。优势是简单可控——你知道每个因子的权重是多少
- IC(动态加权):根据近期 IC 自动调整权重,IC 高的因子获得更高权重。用:有足够 IC 历史数据(至少 60 个交易日),希望系统自适应。代价是波动性——IC 波动会导致权重波动,推荐结果不稳定
- EQW(等权基准):所有因子权重相等。用途:作为基准——用 IC 模式的推荐结果和 EQW 比较,如果 IC 模式没有明显跑赢 EQW,说明 IC 加权没有带来增量价值,不如用简单的等权
实际采用的权重:ICW 自适应
系统默认采用 ICW(IC 加权)自适应权重:因子权重 = 其近期 IC 衰减均值(decayWeightedMean)归一化后的结果,并内置三道自管理规则:
- 噪声自动剔除:|IC| < 0.015 的因子直接剔除(NOISE_DROPPED),不进入加权
- 负 IC 自动反转:IC<0 的因子取绝对值加权(方向取反),无需手动配置
- 新因子保守起步:无 IC 历史时权重 0.05,而非 1.0,避免"没证据=有效"
- 按市场体制分别算 IC:IC 只取当前 regime(BULL/BEAR/SIDEWAYS)的交易日计算,避免牛市 IC 和熊市 IC 互相稀释导致权重方向错误。体制由
market_regime_calendar日历表提供
新上线的三个 alpha 因子自适应权重示例:EARNINGS_SURPRISE → 1.2(IC≈+0.09 被加权)、LHB_INST_NET → 0.5(IR≈-0.66 被降权)、INST_RESEARCH → 0.8(稀疏信号略降)。权重每日随因子 IC 刷新自动调整。
4.3 推荐表现追踪与命中率
作用
追踪每次推荐的实际表现,计算命中率和收益统计,形成闭环反馈。
为什么推荐不是"发完就完"?
推荐的质量需要持续验证。如果不追踪推荐实际表现,你永远不知道推荐到底好不好——是赚了还是亏了?命中率多少?哪些策略推荐得好?哪些差?
表现追踪的核心指标是次日超额收益:推荐股票次日收益减去沪深300收益。如果推荐的 20 只股票中 15 只次日跑赢沪深300,命中率 75%——这远超随机水平(50%),说明推荐管线有效。
更重要的是闭环反馈:命中率数据反馈给置信度系统(2.4 节),命中率低的策略推荐数量自动缩减;因子推荐表现反馈给因子健康监控(1.9 节),持续失效的因子被自动降级。整个系统是自我纠错的——不是"设好了就不管",而是持续追踪、持续优化。
关键算法
存储单位:百分比数值(如 -7.03 表示 -7.03%)
跟踪闭环:为什么周/月收益有时会暂时为空?
推荐表现分三档追踪:次日(约 1 个交易日)、周(约 5 个交易日)、月(约 20 个交易日)。系统在每日收盘后的定时任务里自动回填——判定一条推荐"已跟踪完成"的标志是月收益(next_month_return)已填,而非次日收益。
因此,推荐日期距今不足约 5 个交易日时,周收益客观上还不存在(收益数据尚未发生),会显示为 NULL,这是正常时间窗口限制,不是缺陷。等对应推荐各自满 5 个交易日,定时任务会自动补齐,无需人工干预。若想更早看到周收益,唯一的办法是缩短窗口,但会牺牲收益样本的完整性,不建议。
4.4 因子选股
作用
手动执行一次策略选股——选择策略+日期,系统立即计算并返回符合条件的股票列表。
因子选股 vs 智能推荐的区别
因子选股是手动触发、即时返回,不经过 P1-P6 管线(不做 IC 筛选、拥挤度检测等),直接按策略配置的因子+权重+预处理选项计算。适合快速验证策略配置或做一次性选股。
智能推荐是定时自动执行、经过完整 P1-P6 管线,适合日常运行的全自动选股。两者的关系:因子选股是"手动挡",智能推荐是"自动挡"——日常用自动挡,需要精确控制时用手动挡。
五、评估
评估是量化投研的"镜子"——它不产生收益,但能照出收益的真实来源。很多策略在回测里年化 30%,实盘却亏钱,核心原因不是市场变了,而是评估维度不够:只看了"赚多少",没看"怎么赚的"、"能不能持续"、"换个环境还行不行"。本模块提供 9 个评估工具,覆盖归因、稳定性、统计显著性和实盘验证四个维度。
5.1 Brinson 归因
作用
把组合收益拆解为"选股能力"和"配置能力"两部分,回答一个核心问题:你的超额收益到底是选对了股,还是选对了行业?
为什么需要归因?只看超额收益不够吗?
假设你的组合跑赢沪深 300 5%,看起来很厉害。但这 5% 可能来自两种完全不同的路径:
- 路径 A(选股贡献):你在每个行业里都选了超越行业均值的股票。这证明你的因子模型选股能力有效,策略可持续。
- 路径 B(配置贡献):你选的股票其实跑输行业均值,但你恰好超配了某几个涨幅大的行业。这本质上是"赌对了行业方向",不是选股能力。下期如果行业方向判断错了,收益立刻反转。
不归因,你就无法区分这两种情况。把运气当能力继续加注,是量化策略失效的最常见原因。
实现思路
采用 Brinson 单层归因模型,以行业为分组维度:
选股效应(Selection) = Σ Wp_i × (Rp_i - Rb_i)
交互效应(Interaction) = Σ (Wp_i - Wb_i) × (Rp_i - Rb_i)
超额收益 = 配置效应 + 选股效应 + 交互效应
其中 Wp_i 为组合中行业 i 的权重,Wb_i 为基准中行业 i 的权重,Rp_i / Rb_i 为组合 / 基准在行业 i 的收益。
为什么用单层而非多层 Brinson?交互效应怎么处理?
多层 Brinson(如行业→子行业→个股)理论上更精细,但实务中有两个问题:一是分组越细,组内股票越少,选股效应的统计意义越弱;二是多层拆解后交互效应膨胀,拆出来反而看不清主要矛盾。
平台采用等权基准方式简化交互效应——交互效应通常较小且不稳定,将其并入选股效应处理,让归因结果更干净、更可解释。这也是业界主流做法(如 BARRA 模型通常也将交互项归入选股)。
关键设计
- 基准选择:默认沪深 300,可切换中证 500 / 中证 1000 / 等权基准
- 行业分类:采用申万一级行业(28 类),避免过度细分导致统计噪声
- 数据对齐:组合和基准的行业分类必须一致,权重按收盘市值计算
5.2 因子风格归因
作用
把组合收益归因到具体的因子风格(Size/Value/Momentum/Volatility/Quality/Growth),回答:你的收益主要来自哪种风格暴露?
有了 Brinson 为什么还要因子风格归因?
Brinson 按行业拆,因子风格按因子维度拆。两者回答不同问题:
- Brinson:你在"食品饮料"行业选股能力强 → 回答"在哪里赚的"
- 因子风格:你赚的是"小盘因子暴露"的钱 → 回答"凭什么赚的"
同一个组合可能 Brinson 显示选股效应为主,但因子风格归因显示其实是小盘因子暴露——说明你选的股票只是碰巧小盘股多,不是选股能力好。这个洞察 Brinson 给不了。
实现思路
采用因子组合法(Factor Portfolio Approach):
- 构建纯因子组合:对每个风格因子(如 Size),按因子值排序分 5 组,做多 Top 组做空 Bottom 组,得到纯 Size 因子收益序列
- 计算组合因子暴露:用组合持仓的因子值加权,得到组合在每个风格上的暴露度
- 归因分解:因子收益 × 因子暴露 = 该因子的收益贡献
其中 β_i 为组合在因子 i 上的暴露,F_i 为因子 i 的收益,ε 为特异性收益
为什么不用回归法而用因子组合法?
回归法(如时间序列回归 R_portfolio = α + β×F + ε)的问题是:它只能告诉你组合收益和因子收益"相关",但不能保证这种相关是因果的——可能是巧合。因子组合法直接计算"你持有多少因子暴露 × 该暴露产生了多少收益",因果关系明确。
另外,回归法在样本量不足时(如回测只有 1 年)参数估计极不稳定,而因子组合法不依赖回归,结果更稳健。
5.3 Fama-French 三因子(FF3)归因
作用
用 MKT/SMB/HML 三个经典风险因子解释组合收益,剥离市场系统性风险后评估真实 Alpha。
为什么不直接用"超额收益"衡量 Alpha?FF3 有什么额外价值?
超额收益 = 组合收益 - 基准收益。但这忽略了两个系统性风险溢价:
- 规模溢价(SMB):小盘股长期跑赢大盘股(至少在 A 股的部分历史区间内成立)。如果你的组合恰好小盘股多,超额收益可能只是规模溢价,不是你的选股能力。
- 价值溢价(HML):高账面市值比股票长期跑赢低账面市值比股票。如果组合偏价值,超额收益可能只是价值溢价。
FF3 把这两个溢价剥离后,剩下的才是"真 Alpha"——不能用已知风险因子解释的那部分收益。FF3 Alpha 为正且显著,说明你的策略确实有信息增量;如果 FF3 Alpha 不显著,你的策略只是承担了已知风险、赚了风险溢价,并非真正的 Alpha。
实现思路
- 因子收益计算:平台通过 ClickHouse 的
factor_premium表存储每日 MKT/SMB/HML 因子收益 - MKT = 全市场等权收益 - 无风险利率
- SMB = 小盘组合收益 - 大盘组合收益(按市值三分组)
- HML = 高账面市值比组合收益 - 低账面市值比组合收益(按 B/M 三分组)
- 时间序列回归:R_portfolio - R_f = α + β_mkt×MKT + β_smb×SMB + β_hml×HML + ε
- Alpha 检验:对 α 做 t 检验,p<0.05 认为显著非零
为什么用 FF3 而不是 FF5?
FF5(五因子模型)在 FF3 基础上加了盈利(RMW)和投资(CMA)因子。理论上更完整,但实务中:
- FF5 的因子之间相关性很高(RMW 和 HML 相关性 >0.5),多元共线性导致系数估计不稳定
- A 股市场的盈利和投资因子有效性不如美股稳定,加入反而增加噪声
- FF3 的三个因子已经能解释大部分系统性风险,多加两个因子边际收益递减但复杂度递增
因此平台选择 FF3 作为主归因模型,在需要时可扩展为 FF5。
5.4 Alpha 滚动监控
作用
用滚动窗口(默认 60 日)持续计算 Alpha,观察策略有效性的时间变化趋势——Alpha 是在增强还是在衰减?
为什么不用全样本 Alpha?为什么要滚动?
全样本 Alpha 是整个回测期间的平均值,它掩盖了策略可能存在的时变性问题:
- 策略前期 Alpha=0.1,后期 Alpha=0.01——全样本 Alpha=0.05 看起来还行,但策略实际已经接近失效
- 策略 Alpha 大幅波动(时而 0.2 时而 -0.1)——全样本 Alpha 掩盖了这种不稳定性
滚动 Alpha 像"心电图"——它显示策略是不是在"健康跳动"还是在"逐渐衰竭"。如果滚动 Alpha 持续下行,这是策略失效的早期预警信号,比全样本 Alpha 更早发现问题。
实现思路
- 滚动窗口:默认 60 日(约 3 个月),可配置 20/40/120 日
- 窗口内回归:每个窗口内做 FF3 回归得到该窗口的 Alpha
- 趋势检测:对滚动 Alpha 序列做线性回归,斜率 <0 且 p<0.05 判定为"显著衰减"
- 可视化:折线图展示 Alpha 的时间序列 + 趋势线 + 置信区间
为什么窗口选 60 日?太短和太长各有什么问题?
- 太短(如 20 日):一个月的数据做回归,参数估计极不稳定。一天异常收益就能让 Alpha 从正变负。信噪比太低,产生太多虚假信号。
- 太长(如 250 日):一年才更新一次,策略可能已经失效半年了你还没发现。丧失了"早期预警"的价值。
- 60 日:约 3 个月,既能过滤短期噪声,又能在策略失效后 3 个月内发出预警。这是行业常用的"季度体检"周期。
5.5 蒙特卡洛 Bootstrap
作用
通过重采样检验策略收益的统计显著性——你的超额收益是真实的,还是运气好?
为什么需要蒙特卡洛?回测年化 20% 不就说明策略好吗?
回测年化 20% 可以来自两种截然不同的情况:
- 情况 A(真 Alpha):策略有稳定的选股逻辑,每月持续产生小额超额收益,日收益分布的均值显著偏正。
- 情况 B(运气):策略平时不赚不亏,但回测期间恰好抓到了一次大行情(如某次板块暴涨),这一个月的收益贡献了全部超额。换个时间段可能就没了。
蒙特卡洛 Bootstrap 的做法:把回测期间的日超额收益序列拿出来,有放回地随机重采样 10000 次,每次构造一个等长的"伪收益序列",看有多少比例的伪序列比你真实的收益差。如果 >95% 的随机重排都不如你,说明你的收益不太可能纯靠运气得到。
这解决了一个根本问题:"你到底是厉害还是运气好"无法仅靠年化收益回答,需要统计检验。
实现思路
- 提取超额收益序列:每日组合收益 - 基准收益
- Bootstrap 重采样:从序列中有放回随机抽取 N 个(N=序列长度),构成一个 Bootstrap 样本
- 计算统计量:每个 Bootstrap 样本计算年化收益/夏普比率/最大回撤
- 构造置信区间:10000 次 Bootstrap 得到统计量的经验分布,取 2.5% 和 97.5% 分位数作为 95% 置信区间
- p 值:Bootstrap 分布中 0 的位置——如果 0 在置信区间外,说明策略超额收益统计显著
为什么不直接用 t 检验?Bootstrap 有什么优势?
t 检验假设收益服从正态分布,但金融收益分布通常是"肥尾"的(极端事件比正态分布预测的更频繁)。在肥尾分布下用 t 检验会低估 p 值,高估显著性。
Bootstrap 不需要正态分布假设——它直接从经验分布中采样,天然处理肥尾、偏态等非正态特征。对于金融数据,Bootstrap 比 t 检验更可靠。
另外,Bootstrap 还能计算复杂统计量的置信区间(如夏普比率、Calmar 比率、最大回撤),这些统计量没有解析公式做 t 检验,只能靠 Bootstrap。
5.6 Walk-Forward 验证
作用
模拟"用过去 N 个月的数据训练 → 在接下来的 M 个月实盘交易 → 滚动前进"的真实场景,检验策略的样本外表现。
为什么普通回测不够?Walk-Forward 解决什么问题?
普通回测最大的问题是未来信息泄露——你在回测前已经看到了全周期数据,参数优化会不自觉地"拟合"到这些数据上。即使你严格避免未来函数,参数选择本身就是在利用未来信息。
Walk-Forward 的核心思想:每个时间点只能用过去的数据做决策。具体来说:
- 用 2023-01 到 2023-12 的数据优化参数 → 得到参数集 A
- 用参数集 A 在 2024-01 到 2024-03 交易(样本外)→ 记录收益
- 窗口前进:用 2023-04 到 2024-03 的数据重新优化 → 得到参数集 B
- 用参数集 B 在 2024-04 到 2024-06 交易 → 记录收益
- ……持续滚动到回测结束
最终把所有"样本外"片段的收益拼接起来,得到 Walk-Forward 收益曲线。如果这条曲线和普通回测曲线接近,说明策略没有过拟合;如果差距巨大,说明你之前的回测收益主要是参数拟合的幻觉。
实现思路
- 训练窗口:默认 12 个月(可配置 6/9/18 月)
- 测试窗口:默认 3 个月(可配置 1/6 月)
- 参数优化:每个训练窗口内做参数网格搜索,选最优参数
- 滚动拼接:所有测试窗口收益首尾拼接,形成样本外收益序列
- 对比指标:Walk-Forward 夏普 vs 普通回测夏普,衰减率 <30% 认为无过拟合
为什么训练窗口选 12 个月、测试窗口选 3 个月?
训练窗口太短(如 3 个月):参数估计不稳定,容易被短期行情主导。在牛市期间训练,参数会偏激进;熊市训练,偏保守。3 个月只覆盖一个市场状态,不具备代表性。
训练窗口太长(如 36 个月):包含了太久远的数据,市场结构可能已经变化。A 股的风格轮动通常 1-2 年一个周期,3 年前的数据可能是完全不同的市场环境。
测试窗口 3 个月:和滚动 Alpha 的窗口一致,每季度重新校验一次。太短(1 个月)噪声太大;太长(6 个月)失去了 Walk-Forward "及时更新"的意义——如果参数在 4 个月后失效了,6 个月的测试窗口会拖累整条曲线。
5.7 Regime 回验
作用
按市场状态(Regime)分组回测推荐表现,验证策略在不同市场环境下是否有效,以及权重配置是否最优。
为什么需要按 Regime 分组?统一回测不够吗?
统一回测告诉你"过去一年策略赚了 15%",但不告诉你这 15% 是怎么赚的:
- 可能牛市赚了 30%、熊市亏了 15%——这种策略只在牛市有效,碰上熊市就是炸弹
- 可能趋势市赚了 25%、震荡市亏了 10%——这种策略依赖市场趋势性,碰上横盘就失效
Regime 回验把"过去一年"按市场状态切分——牛市 / 熊市 / 震荡市、高波 / 低波、放量 / 缩量——分别看策略在每种环境下的表现。这回答了两个关键问题:
- 策略在什么环境下有效——告诉你什么时候该用、什么时候不该用
- 策略在什么环境下会亏钱——告诉你什么时候需要止损或切换策略
不知道策略的环境偏好,就像不知道自己的车在雨天会打滑——晴天开得好好的,一碰雨天就出事。
实现思路
- Regime 识别:基于均线系统 + 波动率 + 成交量将每日市场分为 6 种状态(牛市趋势 / 熊市趋势 / 震荡 / 高波 / 低波 / 放量 / 缩量,正交组合)
- 分组回测:将推荐记录按当日 Regime 分组,分别计算各组超额收益
- 一致性检验:如果策略在 >4 种 Regime 下都能产生正超额收益,认为策略泛化能力强
- 权重最优性验证:检验实际配置的因子权重在各 Regime 下是否最优——如果某个因子在熊市 IC 为负,但权重仍然是正的,说明权重配置有问题
为什么不做 Regime 切换策略而是只做回验?
Regime 切换(在不同市场状态用不同策略权重)听起来很美——"牛市重动量、熊市重价值"。但实务中有三个陷阱:
- Regime 识别滞后:你确认进入熊市时,可能已经跌了 15%,切换已经晚了
- 切换成本:频繁切换策略意味着频繁调仓,交易成本和冲击成本会吃掉 Alpha
- 过拟合:为每种 Regime 单独优化权重,极易过拟合——你在拟合历史,不是预测未来
平台选择更稳健的方案:不做 Regime 切换,但做 Regime 回验——用统一的权重,但验证它在各种环境下的表现。如果某种环境下确实显著亏损,再针对性地加风控规则(如熊市降低仓位),而非改变策略逻辑。这比 Regime 切换更简单、更不容易过拟合。
5.8 模拟盘
作用
在真实市场环境中跟踪推荐信号的表现,但不实际下单——验证"回测到实盘"之间到底差多少。
为什么回测通过了还需要模拟盘?两者差在哪?
回测和实盘之间有 4 个系统性差异:
- 交易成本:回测通常只算佣金+印花税,忽略冲击成本。实盘中你买入 1000 万某只小盘股,你的买单本身就推高了价格——这叫冲击成本,回测里不会发生
- 执行延迟:回测假设你在收盘价精确成交。实盘从信号生成到下单执行有几分钟延迟,价格可能已经变了
- 数据差异:回测用的是清洗后的历史数据。实盘用的是实时数据,可能有异常值、延迟、缺失
- 行为偏差:回测里策略铁面无私。实盘里你可能看到跌了就恐慌性手动止损,或者在信号不明确时犹豫不决
模拟盘在真实市场数据下运行策略但不实际交易——它验证了前 3 个差异(交易成本可模拟、执行延迟可模拟、数据差异是真实的),只有行为偏差无法模拟。所以模拟盘通过后,你还需要克服自己的心理偏差,这只有真金白银的实盘才能训练。
实现思路
模拟盘系统包含 4 个子系统:
信号子系统
每日收盘后自动生成推荐信号,记录:推荐日期、股票代码、推荐理由、建议买入价、止损价、目标仓位。信号一旦生成不可修改。
执行子系统
模拟次日开盘成交:以次日开盘价 ± 滑点(默认 0.1%)模拟成交。记录实际成交价、成交数量、交易成本。滑点参数可配置。
风控子系统
模拟实盘风控规则:单只持仓 >10% 警告、行业集中度 >30% 警告、最大回撤 >15% 触发减仓。这些规则在回测中通常不开启(回测关注策略本身收益),在模拟盘和实盘中必须开启。
预警子系统
异常情况主动推送:持仓触及止损价、推荐信号与实际持仓偏离过大、市场出现极端波动。预警通过 SSE(Server-Sent Events)实时推送。
关键设计决策
- 信号不可篡改:推荐记录写入后不可修改,防止"看到亏了就偷偷改推荐"——保持记录的客观性
- 滑点可配置:大盘股默认 0.1%,小盘股默认 0.3%(流动性更差)
- 资金管理:初始资金 100 万,满仓限制 80%(留 20% 现金应对追加保证金等突发情况)
5 步使用流程
定时自动运行
- 每个交易日 15:30(收盘后)自动处理所有 RUNNING 模拟盘
- 执行顺序:分红结算 → 生成信号 → 批量执行
- Cron:
0 30 15 * * MON-FRI(北京时间) - 一键执行 API:
POST /api/paper-trading/{paperId}/execute-all-signals
状态机
| 状态 | 标签 | 说明 |
|---|---|---|
| RUNNING | 运行中 | 定时调度会处理;可手动生成信号和执行交易 |
| PAUSED | 已暂停 | 定时调度跳过;可查看历史,不能生成新信号 |
| STOPPED | 已停止 | 定时调度跳过;模拟盘归档,不再接受任何操作 |
风控配置
| 参数 | 默认值 | 说明 |
|---|---|---|
| 止损比例 | 5% | 单笔亏损超过此比例自动生成 SELL 信号 |
| 止盈比例 | 15% | 单笔盈利超过此比例自动生成 SELL 信号 |
| 最大行业暴露 | 35% | 单一行业仓位不超过总资产的比例 |
| 仓位集中度 | 20% | 单只股票仓位上限 |
| 回撤阈值 | 20% | 净值回撤超限生成 WARNING 预警 |
资金分配模式
| 模式 | 公式 | 优点 | 缺点 |
|---|---|---|---|
| equal 等权 | perStock = initialCapital / maxPosition | 简单公平 | 资金利用率低 |
| dynamic 动态 | perStock = minAlloc + (maxAlloc - minAlloc) × factorScore | 高分股多配 | 依赖因子准确性 |
| kelly 凯利 | f = (winRate×avgWin - avgLoss) / (avgWin×avgLoss),限制 5%~25% | 数学最优增长 | 需≥5笔历史交易;波动大 |
大盘择时信号
开启后,空头市场中自动暂停新开仓,避免系统性下跌中继续持仓。
| 市场状态 | 判定条件 | 行为 |
|---|---|---|
| 空头 | 均线温度="空头" 或 恐慌贪婪指数<29 | 暂停新 BUY(buySlots=0),已有持仓不强制卖出 |
| 多头 | 均线温度≠"空头" 且 恐慌贪婪指数≥29 | 正常生成 BUY,最多 10 只持仓 |
持仓预警触发表
| 预警类型 | 触发条件 | 严重级别 |
|---|---|---|
| MA_BREAK | 股价跌破 5 日均线 | WARNING |
| DROP | 单日跌幅 >5% | WARNING |
| NOTICE | 近 7 天有新公告 | INFO |
| REPORT | 近 7 天有研报覆盖 | INFO |
| RISK_CONCENTRATION | 单只仓位 >25% | WARNING |
| RISK_INDUSTRY | 单一行业仓位 >35% | WARNING |
| RISK_DRAWDOWN | 净值回撤 >20% | WARNING |
| EVENT_ADDITIONAL | 近 7 天有定增预案 | WARNING |
| EVENT_UNLOCK | 近 7 天有解禁事件 | WARNING |
POST /api/paper-trading/{paperId}/process-dividends。买入/卖出/分红后自动追加 NAV 记录。5.9 组合风控
作用
跨策略维度控制组合风险——多个策略同时推荐时,防止个股集中、行业集中、相关性过高导致的隐藏风险。
为什么单策略风控不够?跨策略风控解决什么问题?
假设你有 3 个策略,每个策略的风控都是"单只持仓 <10%"。看起来每只股票最多 10%,似乎很安全。但:
- 个股集中:策略 A 推荐 600519 占 10%,策略 B 也推荐 600519 占 8%,策略 C 推荐 600519 占 5%——实际合计 23%。三个策略各自看风控都没问题,但组合层面 600519 占了 23%,严重超限。
- 行业集中:策略 A 推荐了 3 只白酒股,策略 B 推荐了 2 只白酒股——组合里白酒行业占 35%。一旦白酒板块回调,整个组合跟着跌。
- 相关性陷阱:策略 A 推荐动量因子强的股,策略 B 推荐低 PE 的股。表面看是不同策略不同逻辑,但这些股票可能是同一批大盘股——因为低 PE 的往往也是大盘股。相关性极高,看似分散实际没分散。
组合风控解决的就是这个"各自合规但整体违规"的问题。它在策略推荐生成后、落盘前做一道交叉检查,把多个策略的推荐合并后做统一风控。
实现思路
- 跨策略个股去重:多个策略推荐同一只股票时,合并为一条记录,权重取加权平均
- 行业集中度检查:合并后的推荐列表按行业分组,任一行业占比 >30% 时自动缩减该行业推荐数
- 回撤监控:实时跟踪组合累计净值,当回撤超过阈值(默认 15%)时触发减仓信号
- 动态行业相关性:用 60 日滚动窗口计算行业间相关矩阵(基于 Union-Find 聚类),如果某两个行业相关性持续 >0.8,视为"等效行业",在集中度检查时合并计算
- 流动性上限检查:查询 ClickHouse 近 20 日日均成交额和换手率,日均成交额 <1000 万的股票直接剔除,<5000 万或换手率 <1% 的股票仓位上限降至 3%。当前为观察模式(只记 warning 不改仓位),确认预警名单合理后切换为生效模式
为什么行业相关性要动态计算?用静态分组不行吗?
行业之间的相关性不是固定的——牛市时各行业齐涨共跌,相关性很高;震荡市时行业分化明显,相关性较低。用静态分组意味着你假设"银行和地产永远相关性 0.7",但实际上这个相关性可能在 0.3 到 0.9 之间波动。
更关键的是行业之间的相关性会因市场结构性变化而改变——比如某条新政策让新能源和消费的相关性突然升高。静态分组无法捕捉这种变化,动态滚动相关性可以。
平台用 Union-Find 聚类算法把高相关性行业自动归并为"等效行业组"——比如银行+地产+非银金融经常聚为一组,在集中度检查时把它们的总权重合并计算。如果分散投资但实际都是"金融系",等于没分散。
5.10 多策略组合模式
作用
把多个独立策略按权重合并成一个「多策略模拟盘」(Combo),用一份资金、一条净值曲线统一管理,享受子策略低相关带来的波动平滑——而不是在三个模拟盘里各看各的盈亏。
为什么要用多策略组合?单个策略不够吗?
- 单策略有环境局限性:任何单一策略都有适应/失效的市场环境(比如动量策略在震荡市亏钱、价值策略在成长牛跑输)。多策略组合的核心是「分散」——只要子策略相关性低,组合波动会显著小于单策略,回撤更可控。
- 对比「分别建 3 个模拟盘」:分别建盘等于手管 3 份资金,无法统一看净值、无法做组合层面的再平衡和风控。多策略组合把它们合并为一份——资金、风控、再平衡统一管理。
- 效果可量化:组合详情用「平均相关性」和「分散化比率」两个指标客观衡量分散是否真的生效,而不是凭感觉。
入口与创建
多策略组合在「模拟盘交易」页创建:
[{strategyId, weight}]。创建后界面不提供增删子策略,需新建组合。组合详情(4 个 Tab)
组合总览
子策略权重条 + 策略间相关性矩阵 + 组合净值曲线(组合 vs 各子策略),一眼看出谁在贡献收益、谁在拖后腿。
子策略明细
每个子策略的权重 / 初始资金 / 当前资产 / 收益率 / 贡献 / 状态,可单独「暂停」「恢复」「调权重」。
再平衡历史
记录每次再平衡的触发类型(手动 / 周期 / 阈值)、最大偏离、调仓标的与备注。
信号流水
各子策略产生的买卖信号明细,含信号价、因子得分、命中原因与执行状态。
分散化监控(关键指标)
| 指标 | 含义 | 告警阈值 |
|---|---|---|
| 平均相关性 | 子策略两两相关性均值(基于各自净值) | ρ>0.7 → 组合分散失效 |
| 分散化比率 | 组合波动率 ÷ 子策略加权波动率,越>1 越好 | <1.15 → 策略同质化风险 |
权重调整与再平衡
- 调权重:在「子策略明细」点「调权重」,修改某子策略权重后保存 → 系统重新归一化组合配置,并触发该子策略再平衡。
- 手动再平衡:顶部「手动再平衡」按钮立即按当前配置对所有子策略做一次再平衡(触发类型
MANUAL)。 - 触发类型:MANUAL 手动 / SCHEDULE 周期 / THRESHOLD 阈值(某子策略实际权重偏离目标权重超阈时自动触发)。
子策略生命周期
| 状态 | 标签 | 说明 |
|---|---|---|
| RUNNING | 运行中 | 该子策略正常参与信号生成与执行 |
| PAUSED | 已暂停 | 该子账户暂停,不再生成新信号,不影响其它子策略 |
| STOPPED | 已停止 | 子账户归档,不再接受任何操作 |
六、数据管理
数据是量化平台的"原料"——因子计算、策略回测、推荐生成全部依赖底层数据质量。本模块管理行情数据、财务数据、研报数据、行业数据的采集、更新和质量监控,以及定时任务调度。核心原则:数据不可靠,以上所有模块的分析结果都不可信。
6.1 数据更新
作用
管理沪深+北交所全市场日线行情数据的每日采集和增量更新,是整个数据链路的起点。
为什么不用单一数据源?多数据源解决什么问题?
A 股数据源各有优劣,没有"万能源":
- Baostock:沪深 A 股免费、数据质量高、支持前复权,但不支持北交所、有限流(匿名登录按 IP 限流,高频请求会被封 IP)
- 腾讯接口:覆盖沪深+北交所、实时性好,但 PE/PB 只有快照值(非历史序列)、640 日数据上限
- akshare:备选情绪数据源,但 README 限制商用
平台采用"可插拔数据源"架构(data_provider 模块):Baostock 为主源(沪深),腾讯为辅源(北交所 + 沪深 fallback),akshare 为情绪数据补充。通过 DATA_SOURCE 环境变量切换。当 Baostock 被限流封 IP 时,自动 fallback 到腾讯接口,不中断数据采集。
实现思路
- 统一入口:
update_stock_daily.py通过工厂模式获取 provider(baostock/tencent),对调用方透明 - 增量更新:每次更新只拉取最新交易日数据,通过 ClickHouse 中已有的最大日期 +1 天确定起始日期
- 前复权刷新:除权除息日自动触发 QFQ 刷新——Baostock 重拉全历史 + force + refresh_version 写入,覆盖旧快照
- 分块拉取:Baostock 按年分块(3 年/块),单块超时 30s,超时自动 re-login 重试
- Volume 单位处理:科创板(688/689xxx)腾讯返回的是"股"而非"手",代码层自动识别不 ×100
为什么前复权数据需要"刷新"?不是一次性拉取就够吗?
前复权价格是时点快照——每次股票除权除息时,所有历史 qfq 因子都会被 retroactive 更新。假设:
- 2025-01-01 你拉取了 600519 的前复权数据,当时没有除权,qfq 因子=1.0
- 2025-06-01 600519 分红除权,qfq 因子变成 0.95
- 此时 CH 中 2025-01-01 之前的历史 qfq 数据已经过期了——它们用的是旧因子 1.0,但新因子是 0.95
如果不刷新,你在做回测时用的是过期的前复权价格——回测收益会有一致性偏差。平台通过 refresh_qfq_history.py 在每次除权后自动重拉全历史并覆盖旧数据(refresh_version=True 设置 update_time=now() 确保 ReplacingMergeTree 覆盖旧版本)。
各 Tab 操作说明
| Tab | 配置选项 | 说明 |
|---|---|---|
| 股票日线 | 市场(全部/沪/深/北交所) + 数据源(全部/Baostock/腾讯) + 股票池 + 断点续传 + 排除ST | 最常用,支持断点续传跳过已完成股票 |
| 指数日线 | 断点续传 | 更新沪深300/中证500/上证50等 10 个主要指数,后台自动 |
| 分红除权 | 跳过已有(默认勾选) | 巨潮→同花顺→东方财富三级回退,首次运行后无需再更新 |
| 财务数据 | 年份范围(默认近3年)+ 强制重新采集 | 同花顺→东方财富回退,年报/季报发布后运行 |
| 情绪数据 | 日期范围(默认近7天)+ 龙虎榜/融资融券/机构调研/大宗交易/涨跌停池 | 东方财富+同花顺,每日更新 |
数据源对照表
| 数据类型 | 主数据源 | 备用数据源 |
|---|---|---|
| 沪深日线 | Baostock | akshare / 腾讯 fallback |
| 北交所日线 | 腾讯证券 | -(仅腾讯覆盖北交所) |
| 指数日线 | Baostock | - |
| 分红除权 | 巨潮 | 同花顺 → 东方财富 |
| 财务数据 | 同花顺 | 东方财富 |
| 情绪数据 | 东方财富 | 同花顺 |
常见问题
- 网络中断后如何继续? 勾选「断点续传」重新开始,自动跳过已完成的股票
- 北交所数据没更新? 检查数据源是否选「全部」或「腾讯证券」,Baostock 不覆盖北交所
- 财务数据需要更新吗? 平时不需要,年报/季报发布季(4/8/10月)运行一次即可
6.2 财务数据
作用
采集上市公司财报数据(利润表、资产负债表、现金流量表),供财务因子(ROE、净利润增速、盈利质量等)计算使用。
为什么财务因子需要特殊的数据更新逻辑?
行情数据是日频的,每个交易日都更新。但财报数据是季频的,而且有一个特殊的"发布延迟"问题:
- 报告期 vs 发布日:一季报的报告期是 3-31,但实际发布日是 4-30。这意味着在 4-30 之前,你拿不到 Q1 数据,只能用去年 Q4 的
- 修正风险:初步发布的财报数据后续可能被修正。如果你在 4-30 用了初步数据,5-15 公司发布了修正版,你的因子计算结果会变
- TTM 计算:很多财务因子用 TTM(滚动 12 月)数据,需要把最近 4 个季度加起来。这需要确保你有连续 4 个季度的数据,缺一个就计算不了
平台通过季频前向填充解决"发布延迟"问题:在报告发布日前用上一季度的数据,发布后自动切换为新数据。这样因子计算永远有数据可用,不会因为财报未发布而出现空值。
实现思路
- 数据源:东方财富 + Baostock 财务接口
- 更新触发:每个季报发布季(4月/8月/10月)集中更新
- 前向填充:用最近一次已发布的财报数据填充后续日期,直到新财报发布
- TTM 计算:滚动取最近 4 个季度的财务数据求和/求均值
- P0-2 超额收益校准:用
trackRecommendationPerformance计算个股相对沪深 300 的超额收益,在财报发布季做季频校正(P4 步骤)
财务数据 5 步更新流程
核心数据表
| 表名 | 说明 | 主要字段 |
|---|---|---|
| stock_financial_indicator | 财务指标表 | roe, roa, revenue_yoy, net_profit_yoy, gross_profit_margin, debt_to_asset_ratio, free_cash_flow |
| stock_income | 利润表 | total_revenue, operating_profit, net_profit, eps_basic |
| stock_balance | 资产负债表 | total_assets, total_liabilities, total_equity, inventory, fixed_assets |
| stock_cashflow | 现金流量表 | net_operate_cf, net_invest_cf, net_finance_cf, free_cash_flow |
6.3 研报数据
作用
采集券商研究报告的核心数据(盈利预测、目标价、投资评级),作为情绪面和一致预期因子的数据源。
为什么需要研报数据?它和因子有什么关系?
研报数据提供两个独特的量化信号:
- 一致预期(Consensus Estimate):所有券商对某公司未来 EPS 的预测均值。如果实际 EPS 超过一致预期 → 正面惊喜 → 股价可能上涨。这叫 PEAD(Post-Earnings-Announcement Drift),是学术界证实的持续性异象。
- 分析师情绪:上调评级数量 vs 下调评级数量。如果某公司近期被多家券商上调评级,可能是积极信号。但注意——分析师评级有羊群效应(一个调了其他跟着调),不能简单看绝对数量。
平台将研报数据作为情绪面补充因子使用,不参与主推荐管线(因为数据覆盖不完整、发布日期不确定),但在个股深度分析中作为参考维度展示。
实现思路
- 采集源:东方财富研报接口
- 更新频率:默认关闭(前端 initialValues=false),需要时手动触发
- 数据范围:盈利预测(EPS/Revenue 预测值+预测日期)、目标价、投资评级(买入/增持/中性/减持/卖出)
- 一致预期计算:同一公司多个研报的预测值取加权平均(近期研报权重更高)
6.4 行业排行
作用
计算行业涨跌排行和资金流向,辅助判断市场风格轮动方向。
为什么需要行业排行?它和选股有什么关系?
行业排行本身不直接选股,但它提供了两个关键的上下文信息:
- 风格判断:如果近期涨幅前 5 的行业是"新能源/半导体/军工",说明市场偏好成长风格;如果是"银行/地产/煤炭",偏好价值风格。这帮助你判断因子权重是否需要微调——成长市里动量因子更有效,价值市里估值因子更有效。
- 行业拥挤度:如果某行业连续多日资金净流入且涨幅领先,可能是拥挤信号。在推荐管线 P3 步骤中,拥挤度检查会参考行业资金流数据。
行业排行是"宏观视角"——个股分析是"微观视角"。只有两者结合才能避免"选了好股票但选错了行业"的尴尬。
实现思路
- 数据源:腾讯行情 + 东方财富资金流
- 计算方式:按申万一级行业分类,计算各行业成分股的加权涨跌幅
- 资金流向:统计各行业的主力资金净流入(大单买入额 - 大单卖出额)
- 动态相关性:60 日滚动窗口计算行业间相关矩阵,Union-Find 聚类高相关行业
行业下钻功能
点击任意行业可下钻查看成分股列表,支持 4 种排序方式:
- 涨跌幅:快速找到行业内领涨/领跌个股
- PE(TTM):找行业内低估值个股
- PB:破净股筛选(PB<1)
- 换手率:找行业内活跃个股
统计指标
每个行业展示以下统计指标:平均涨跌幅、PE(TTM) 中位数、PB 中位数、成分股数量。通过 PE/PB 中位数可快速判断行业整体估值水平。
板块轮动判断规则
| 信号 | 判断规则 | 含义 |
|---|---|---|
| 强主题持续 | 连续 3 日排名前 5 | 板块处于主升浪,可关注龙头 |
| 板块走弱 | 连续 3 日排名后 5 | 板块资金流出,规避 |
| 风格切换 | 前 5 从成长切换为价值(或反向) | 市场风格可能转变,因子权重需调整 |
| 行业拥挤 | 连续 5 日资金净流入 + 涨幅前 3 | 拥挤度高,注意回调风险 |
6.5 定时任务
作用
自动化数据采集和因子计算的调度系统,按依赖链顺序执行每日任务,确保数据在需要时已经就绪。
为什么需要依赖链?各自独立执行不行吗?
数据采集和因子计算有严格的时序依赖关系:
- DAILY(日线采集):必须最先执行——没有行情数据,后续全部无法进行
- FACTOR_COMPUTE(因子计算):依赖 DAILY 完成——因子值需要当日行情数据
- RECOMMENDATION_TRACK(推荐追踪):依赖 DAILY——需要计算昨日推荐股票的今日收益
- BIDASK(盘口快照):可与 DAILY 并行
- DAILY_RECOMMENDATION(日推):依赖 FACTOR_COMPUTE——推荐需要当日因子值
- FACTOR_HEALTH_CHECK(因子健康检查):22:00 执行,依赖 DAILY_RECOMMENDATION——检查推荐表现是否异常
- QFQ_REFRESH(前复权刷新):除权日触发,依赖 DAILY——需要最新行情数据
如果各自独立执行,FACTOR_COMPUTE 可能在 DAILY 之前跑——此时还没有今天的行情数据,因子计算用的是昨天的数据,结果延迟一天。依赖链确保每个任务在前置任务完成后才执行,避免"垃圾进垃圾出"。
实现思路
- 任务定义:每个任务类型有前置依赖(如 FACTOR_COMPUTE 依赖 DAILY 完成)
- 调度引擎:每日定时触发 DAILY,后续任务通过事件总线级联触发
- 失败处理:前置任务失败时,后续任务不执行(而非用错误数据继续跑);支持手动重试
- 日期模式:支持 today(用今天日期)和 latest(用最新已交易日)两种模式
- 策略自动发现:DAILY_RECOMMENDATION 自动遍历所有 status=ACTIVE 的策略,无需手动配置
关键设计
- 除权→QFQ→因子:除权日触发 QFQ_REFRESH → 重新计算因子 → 刷新推荐,形成完整的依赖链
- 多策略并行:DAILY_RECOMMENDATION 为每个 ACTIVE 策略生成推荐,多策略时自动执行组合风控检查
- 推荐幂等:推荐写入前先
deleteByStrategyAndDateAndMode删除旧记录再 insert,防止重复 - 并发守卫:任务启动前先查同类型是否有
RUNNING记录,有则直接跳过本次,避免手动触发与定时触发撞车导致数据重复写入
常见排错清单
| 现象 | 原因与处理 |
|---|---|
| 某个因子的数据停在某一天不再更新(其他因子正常) | 最常见原因:该因子状态被置为 DEPRECATED(已废弃)。日增计算只遍历 factor_definition.status = 'ACTIVE' 的因子,废弃因子会被静默跳过,历史值仍保留在库里,于是 UI 上表现为"只算到 X 月 X 日"。处理:到因子管理确认状态;若确实还要用,改回 ACTIVE 后手动补算缺失区间(改状态本身不会回溯补数据) |
| 所有因子都没更新 | 看依赖链上游 DAILY 是否成功;DAILY 失败时后续任务按设计不执行 |
| 任务一直显示"运行中"不结束 | 进程可能已异常退出但状态未回写。确认后端进程存活(见 9.4),必要时手动将该任务记录状态改为失败再重跑;状态卡在 RUNNING 会让并发守卫持续跳过后续调度 |
| 手动触发无反应 | 八成是并发守卫命中——已有同类型任务处于 RUNNING |
| 因子值算出来但明显失真(如全为 0 / 极端值) | 先查当日行情是否完整(6.6 质量监控),再查是否处于除权日而 QFQ_REFRESH 未执行——未复权价格会让动量、波动类因子整体错位 |
| 日期用错(算了昨天的) | 确认任务的日期模式:today 用当天日期,latest 用最新已交易日;非交易日跑 today 会取不到数据 |
6.6 质量监控
作用
持续监控数据质量,发现数据缺失、异常值、跨源不一致等问题,确保下游分析基于可靠数据。
为什么需要专门的数据质量监控?数据采回来不就能用了吗?
数据采集成功 ≠ 数据正确。以下是实际遇到的典型问题:
- 静默缺失:某只股票停牌当天,数据源返回的是前一天的收盘价(而非空值)。程序看到"有数据"就以为正常,实际上这是无效数据
- 单位不一致:Baostock 返回 volume 单位是"股",腾讯返回的是"手"(非科创板)。如果不统一处理,因子计算中成交量相关因子的值会差 100 倍
- 前复权因子过期:除权后历史数据未刷新,导致跨期回测时前复权价格不一致——回测收益有偏差但不会报错
- 跨源不一致:同一只股票同一日期,Baostock 和腾讯的收盘价可能差 0.5%(前复权因子动态性导致)。这不是错误但需要知晓
这些问题不会导致程序崩溃——程序照常运行并产出结果,但结果可能是错的。质量监控的价值在于在错误传播到下游之前发现它。
实现思路
- 完整性检查:每只股票是否都有最新交易日的数据,缺失的标记并告警
- 异常值检测:涨跌幅 >20%(非涨跌停)、成交量突增 10 倍、PE 负值等异常情况标记
- 跨源对比:定期抽样对比 Baostock 和腾讯数据的一致性,差异超阈值(默认 0.5%)标记
- 因子覆盖检查:计算每个因子在股票池中的覆盖率,覆盖率 <50% 的因子标记为低覆盖
- 四源一致性验证:全量对比 CH/Baostock/腾讯/akshare 的价格数据,识别前复权因子动态变化导致的差异
6.7 缠论筛选
作用
基于缠论(缠中说禅技术分析理论)的形态识别,为 PATTERN 类型因子提供信号源。
为什么要把缠论放进量化平台?它和传统因子有什么不同?
缠论和传统因子的核心区别在于信号类型:
- 传统因子(BUILTIN/SCRIPTED):输出连续数值(如 ROE=15.2%),用于截面排序——谁高谁低
- 形态因子(PATTERN):输出离散事件(如"底分型成立"),用于时点判断——此刻是否触发信号
传统因子解决"买什么"的问题(选股),形态因子解决"何时买"的问题(择时)。两者互补:因子选股告诉你 600519 值得持有,缠论筛选告诉你当前是不是好的买入时机。
平台将缠论形态识别封装为 PATTERN 类型因子,融入因子引擎统一管理——这样可以和其他因子一起进入推荐管线,也可以在因子测试中评估其有效性。
实现思路
- 核心形态:顶分型 / 底分型 / 笔 / 线段 / 中枢——逐级递归识别
- 信号生成:底分型成立 → 买入信号;顶分型成立 → 卖出信号
- 因子封装:PATTERN 类型因子在
computeSingleValue中和 BUILTIN 一样走 builtinCalculators 查找 - ICW 默认权重:PATTERN 因子无 IC 历史数据时默认权重=0.05(非 1.0),防止新因子占过高权重
6.8 黑名单
作用
管理退市股票、ST 股票、长期停牌股票的排除列表,确保推荐系统不会推荐到不可交易的股票。
为什么需要黑名单机制?因子计算时过滤不行吗?
因子计算时过滤是"被动"的——每次计算都要检查每只股票是否可交易,性能开销大。黑名单是"主动"的——维护一个排除列表,在数据采集和推荐生成的最早阶段就过滤掉,后续流程完全不用处理这些股票。
更重要的是,黑名单不仅是"不可交易"的标记,还包含退市管理:
- 退市股票:从股票池移除,但保留历史数据用于回测(不能因为退市就删除历史——否则回测曲线会出现"幸存者偏差")
- ST 股票:不推荐但不移除——ST 摘帽后可以恢复推荐,需要保留数据
- 长期停牌:标记并暂停推荐——复牌后自动恢复
黑名单和退市管理的核心价值是避免幸存者偏差——如果你只保留当前还在上市的股票做回测,你的回测收益会系统性偏高(因为表现差的股票已经退市了,不在你的池子里)。保留退市股票的历史数据但排除推荐,是兼顾回测真实性和实盘可行性的最优方案。
实现思路
- 黑名单类型:DELISTED(退市)/ ST / SUSPENDED(长期停牌)/ MANUAL(手动加入)
- 自动检测:定期检查 stock_info 表的上市状态字段,自动更新黑名单
- 推荐过滤:推荐生成时先查黑名单,命中的股票直接跳过
- 历史保留:退市股票的 ClickHouse 行情数据不删除,用于回测
- 恢复机制:ST 摘帽或停牌复牌后,自动从黑名单移除
退市清理功能
退市清理是在黑名单基础上的延伸功能,定期检测已退市股票并清理相关数据。
- 检测原理:对比 Baostock 股票列表与本地 stock_info 表,Baostock 已移除但本地仍存在的即为退市股
- 清理范围:stock_info(标记退市状态)、stock_daily(保留历史数据用于回测,不删除)、factor_value(保留)、moneyflow(保留)
- 清理策略:仅标记不删除——退市股票的行情和因子数据保留在 ClickHouse 中,用于回测避免幸存者偏差
七、系统管理
系统管理是平台的治理层——它不参与因子计算、策略回测,却控制着"谁能访问哪些功能"。平台所有业务模块(因子、策略、回测、推荐、评估、数据)的访问入口都由它管控。它包含账号与权限(用户 / 角色 / 菜单)、可配置化基础数据(字典 / 配置中心)、组织与隔离(部门 / 数据权限)、以及密钥托管(凭证管理)共 8 类页面,配合 Sa-Token 权限框架实现菜单级 + 按钮级两级鉴权。
7.1 系统管理总览
作用与价值
把"平台账号"和"功能权限"解耦成三层结构,让权限分配可复用、可审计:用户被赋予角色,角色被赋予菜单/权限,最终用户的可访问范围 = 其所有角色权限的并集。
三层模型
- 用户(User):平台账号,可绑定多个角色。支持启用/禁用、重置密码。
- 角色(Role):权限的集合抽象。典型如
ADMIN(全权限)、TestRole(只读部分菜单)。一个角色可绑定多个菜单节点。 - 菜单/权限(Menu):左侧导航的载体,同时承载按钮级权限标识。分目录、菜单、按钮三类(详见 7.4)。
权限的两级
- 菜单级:能否看到某个页面(左侧导航是否出现该菜单)。
- 按钮级:页面内某个操作按钮是否可见/可用(如"新增用户""删除"),由按钮节点上的
permission标识控制。
为什么用"角色"做中间层,而不是给用户直接配权限?
直接给用户配权限,100 个用户就要配 100 遍,且无法保证一致性。引入角色后,权限只配一次(配给角色),再给用户指派角色即可。新增一类岗位(如"只读研究员"),只需新建一个角色并分配对应菜单,再把人加进去,运维成本从 O(用户数) 降到 O(角色数)。
7.2 用户管理
作用与价值
维护平台账号:创建运营/研究员账号、分配角色、重置密码、启用或禁用账号。这是权限落地的"最后一公里"——角色配得再好,最终也要赋给具体用户。
页面字段
| 字段 | 说明 |
|---|---|
| 账号(username) | 登录名,唯一,新增后不可修改 |
| 昵称 / 邮箱 / 手机 | 展示与联系方式,可选 |
| 状态 | 启用(1)/ 禁用(0)。禁用后该账号无法登录 |
| 角色 | 多选,决定该用户能访问哪些功能 |
操作(均受权限控制)
- 新增用户(
system:user:add) - 编辑(
system:user:edit):修改资料与角色 - 改密(
system:user:reset):单独的重置密码入口,无需知道原密码 - 删除(
system:user:delete):谨慎操作
实现思路
- 用户与角色是多对多关系,新增/编辑时在弹框用多选下拉一次性分配角色
- 密码以 BCrypt 加密存储(强度 10),数据库中不保存明文
- 重置密码走独立接口
POST /system/user/{id}/reset-password,管理员可强制改密
7.3 角色管理
作用与价值
把权限集合抽象成角色。核心操作是给角色分配菜单权限——通过一个树形勾选弹框完成。
核心功能:分配菜单权限(权限树)
- 打开某角色的"分配菜单"弹框,展示完整菜单树(目录 → 菜单 → 按钮 三层)。
- 所见即所得:勾了什么就存什么。UI 勾选状态 = 数据库存储 = 实际授权,三者严格一致。
- 级联规则:
- 勾选一个父节点(如"系统管理"目录,或"用户管理"菜单),其下所有子节点一并勾上;
- 取消一个父节点,其下所有勾选一并清除,绝不残留。
- 保存严格 1:1:不会自动"勾父=包含子"地展开存储。你看到勾了 3 个,数据库就只存 3 个,不会偷偷变成 16 个。
典型配置流程
- 角色管理页 → 新增角色,填角色编码(如 test_role)和角色名称
- 点该角色的"分配菜单" → 在权限树中勾选需要开放的菜单与按钮
- 保存 → 到用户管理把该角色赋给目标用户
- 用户重新登录后新权限生效(见 7.5)
7.4 菜单与权限管理
作用与价值
菜单树是权限的载体:它不仅决定左侧导航长什么样,还承载了所有按钮级权限标识。调整菜单结构,等于调整权限结构。
三类节点
| 类型 | 说明 | 必填项 |
|---|---|---|
| 目录(type=0) | 分组容器,无路由、无组件。如"系统管理" | 菜单名称 |
| 菜单(type=1) | 对应一个前端页面,左侧导航可点击进入 | 路由 path、前端组件 component |
| 按钮(type=2) | 页面内的操作权限,不绑定组件 | 权限标识 permission(如 system:user:add) |
关键字段
- 路由(path):菜单类型填写,如
/system/users - 组件(component):菜单类型填写,对应
src/pages下的相对路径 - 权限标识(permission):按钮类型必填,是后端
@SaCheckPermission注解校验的字符串,也是前端hasPermission判断的依据
实现思路
- 菜单以树形表格展示,支持按名称过滤(命中节点保留完整子树,方便查看结构)
- 新增菜单时可选择上级菜单、类型;目录/按钮无需填写组件
- 删除菜单会级联删除其下所有子项,操作前请确认
为什么按钮也要登记成"菜单"节点?
为了让权限可视化、可配置。把"新增用户"这种操作登记为一个 type=2 的按钮节点并挂在其父菜单下,管理员在角色权限树里就能直观勾选"要不要给这个角色开放新增用户按钮",而不用在代码里硬编码。权限标识(如 system:user:add)则是前后端统一的契约。
7.5 权限模型与常见问题
权限框架:Sa-Token
登录时签发 token,后续请求自动携带;后端用 @SaCheckPermission 做方法级鉴权,前端用 hasPermission 控制按钮显隐,按钮级鉴权是双保险:无权限既看不到按钮(前端),调接口也会返回 403(后端)。
登录态机制:httpOnly Cookie + 静默刷新
为什么 token 不再放在浏览器 localStorage?
早期实现把 token 存在 localStorage,由前端手动塞进请求头。问题是 localStorage 对页面上任意 JavaScript 都是可读的——一旦站内出现 XSS(哪怕是第三方依赖引入的),攻击者一行 localStorage.getItem('token') 就能把登录凭证发走,且窃取到的 token 在有效期内可任意冒用。
改造后 token 只经 httpOnly Cookie 下发与携带:浏览器自动带上,但 JS 读不到,XSS 窃取路径被直接掐断。同时登录、/auth/me 等接口的 响应体里 token 字段一律置 null,避免经日志、代理、埋点二次泄漏。
| 机制 | 说明 |
|---|---|
| 双 Cookie | satoken(访问令牌,30 分钟)+ satoken_refresh(刷新令牌,7 天),均为 httpOnly |
| Cookie 安全属性 | HttpOnly=true、SameSite=Lax(挡多数跨站 CSRF)、Secure 由环境变量 SA_TOKEN_COOKIE_SECURE 控制——生产 HTTPS 环境必须设为 true |
| 静默刷新 | 访问令牌过期时接口返回 401,前端拦截器自动调用匿名接口 POST /auth/refresh 换新令牌并重放原请求,用户无感知 |
| 刷新令牌轮换 | refresh token 一次性消费,每次刷新即作废并下发新的,降低泄露后的可用窗口 |
| 彻底失效 | refresh 也过期/无效时,清空所有 cookie 与前端用户态,跳登录页;原访问路径记入 sessionStorage,登录后自动回跳 |
| 小程序端 | backend-mp 仍走 请求头传 token(satoken 头,非 Bearer),与 Web 端 cookie 方案并存互不干扰 |
active-timeout 同为 30 分钟且持续活动会自动续期,加上前端静默刷新,只要 7 天内有使用就不会被要求重新登录。缩短访问令牌有效期只是为了缩小万一泄露时的可用窗口。null,用它判断会导致"提示登录成功却仍停在登录页"。登录安全加固
登录接口(/auth/login)内置三层防爆破,参数在 login-security 配置段可调:
| 机制 | 默认阈值 | 行为 |
|---|---|---|
| 账号维度锁定 | 同一账号+IP 连续失败 5 次 | 锁定 15 分钟,期间即使密码正确也拒绝登录 |
| IP 维度锁定 | 同一 IP 累计失败 20 次 | 锁定 15 分钟,防止针对大量不存在账号的撞库扫描 |
| 图形验证码 | 连续失败 3 次 | 登录响应返回 needCaptcha,前端自动拉取 /auth/captcha 显示验证码框,有效期 5 分钟 |
| 密码复杂度 | — | 修改密码时强制校验长度与字符组合,弱口令直接拒绝 |
失败计数落库到 sys_login_fail,登录成功后自动清零。校验顺序为:先查锁定 → 再验验证码 → 最后校验账号密码(验证码错误同样计入失败次数,避免绕过)。所有登录成败均记入登录日志(见 8.4)。
sys_login_fail 表删除对应记录立即解锁。连续失败次数达标却没弹验证码,通常是前端未读取响应中的 needCaptcha 标记,与后端无关。后端权限是从 Sa-Token 的 session 缓存 读取的,不是每次请求实时查库。因此:
- 你给某用户/角色调整了菜单权限后,该用户必须退出登录再重新登录,新权限才会生效。
- 否则会出现"数据库已更新,但页面仍看不到菜单 / 调接口仍 403"的假象——这不是 bug,是缓存未刷新。
常见排查清单
| 现象 | 排查方向 |
|---|---|
访问某页面/接口报 403 | 确认该用户所属角色是否勾选了对应菜单(菜单级)和按钮 permission(按钮级) |
| 页面上看不到某操作按钮 | 确认当前账号 hasPermission 是否包含该按钮标识(如 system:user:add) |
| 配置了权限但不生效 | 让用户重新登录(见上方说明),刷新 session 缓存 |
| 角色分配菜单弹框显示异常 | 确认菜单树数据完整(目录/菜单/按钮节点齐全),必要时到菜单管理检查节点 |
| 提示"登录成功"但仍停在登录页 | 前端登录态判断误用了 token 字段(恒为 null),应改判用户信息对象;见上方开发注意 |
| 频繁掉登录 / 刷新页面就要重登 | 检查浏览器是否拦截了 Cookie(隐私模式、跨站限制);生产 HTTPS 下确认 SA_TOKEN_COOKIE_SECURE=true,HTTP 下必须为 false,否则 cookie 根本种不上 |
| 登录时提示账号被锁定 | 连续失败触发保护,等 15 分钟或清 sys_login_fail 记录 |
| 换浏览器登录后原会话失效 | 正常现象:is-concurrent: false,同账号互斥登录,新会话踢掉旧会话 |
默认账号
系统初始化时内置 admin 账号(角色 ADMIN,拥有全部权限)。其他账号需由 admin 在用户管理页创建并赋角色。
7.6 字典管理
作用与价值
字典把"可变的下拉选项 / 枚举 / 标签"从代码里抽出来集中维护——例如 SLA 严重级别、信号类型、行业分类等,前端下拉直接读字典,改字典无需改代码发版。
页面结构
- 字典类型:类型编码(dictType,如
SLA_SEVERITY)+ 类型名称 + 描述 + 排序 + 状态;编码新增后不可改。 - 字典项:先选类型,再维护其下数据项——值(dictValue)、显示标签、颜色、扩展属性(JSON)、排序、状态;值新增后不可改。
- 新增 / 编辑类型与项均走独立弹窗(带颜色选择器)。
权限与说明
后端 DictController(/system/dict),权限 system:dict:list/add/edit/delete。字典是"数据而非代码"——字典缓存由 8.1 的"本地缓存状态"监控。
7.7 配置中心
作用与价值
把散落在配置文件 / 代码常量里的运行参数(如限流 QPS、调度开关、各类阈值)集中为可热改的键值对,多数修改无需重启即可生效。
页面字段
| 字段 | 说明 |
|---|---|
| 配置键(configKey) | 唯一键,如 rate_limit_qps,新增后不可改 |
| 显示标签 / 分组 | 分组如 SYSTEM / SCHEDULE,便于归类 |
| 值(configValue) | 按类型填写:文本 / 数字 / true|false / JSON |
| 类型 | STRING / NUMBER / BOOLEAN / JSON |
| 状态 | 启用 / 禁用 |
权限与说明
后端 ConfigController(/system/config),权限 system:config:list/add/edit/delete。配置启动时整体加载进缓存;命中率可在 8.1 查看。新增配置键后需同步前端使用方。
7.8 部门管理
作用与价值
维护组织树(部门),是"按部门做数据权限隔离"的基础。用户归属部门后,按部门授权的资源可"含子部门"向下可见。
页面操作
- 左侧部门树(默认展开),右侧表单维护部门名称、上级部门(TreeSelect)、排序、状态(启用 / 禁用)。
- 支持新增根部门 / 子部门、编辑、删除;存在子部门时拒绝删除。
权限与说明
后端 DepartmentController(/system/department),权限 system:dept:list/add/edit/delete。层级路径 dept_path = parentPath + "/" + id,数据权限拦截器据此做"本部门 + 子部门"匹配(见 7.9)。
7.9 数据权限管理
作用与价值
在菜单级权限之上做更细的行级数据隔离——决定某条策略 / 回测 / 因子 / 模拟盘记录对谁可见、谁可改。
两类控制
- 全局可见性(visibility):PRIVATE(仅自己)/ DEPT(部门可见,含子部门)/ PUBLIC(所有人)。
- 显式授权(shares):把资源额外授权给 用户(USER)/ 部门(DEPT,树选择)/ 角色(ROLE),权限分 VIEW(查看)/ EDIT(编辑)。
读写校验
仅资源 owner 与 ADMIN 可改;非 owner 写时后端 assertCanWrite 校验 EDIT 级授权,否则返回 400。读时后端拦截器按"资源 meta + share"注入 WHERE 条件做隔离,admin 跳过。
实现思路
resource_meta(owner / visibility)+ resource_share(grantee);InnerInterceptor 在 Mapper 层注入隔离条件。
7.10 凭证管理
作用与价值
集中存储第三方密钥(LLM API Key、微信、通知推送、数据库等)。明文输入保存后加密存储,列表只显示掩码;编辑时留空表示不改。
页面字段
| 字段 | 说明 |
|---|---|
| 凭证标识(credentialKey) | 如 DEEPSEEK_API_KEY,新增后不可改 |
| 名称 / 分类 | 分类:llm / wechat / notification / db |
| 密文掩码 | 列表展示脱敏值,详情接口才返回明文 |
| 状态 / 备注 / 创建时间 | — |
权限与说明
后端 CredentialController(/system/credential),权限 system:credential:list/add/edit/delete。业务代码(如 LLM 模块)按 credentialKey 读取解密后的值。
八、系统监控
系统监控是平台的可观测与安全层——它实时反映后端运行时健康、当前在线会话,以及"谁在何时做了什么"的操作留痕。运维与安全管理员借此快速定位异常、追溯问题、发现安全风险。
8.1 系统监控面板
作用与价值
一站式运行时健康看板,实时反映后端 JVM、HTTP、ClickHouse、调度任务、本地缓存与访问行为,便于快速定位异常。
四大核心卡片
- JVM 堆内存:使用率环图 + 堆使用趋势;提交内存、线程数、近 5 分钟 / 累计 GC 次数与耗时、CPU 核数。
- HTTP 流量:QPS(含趋势)、累计请求、近 1 分钟、错误数、平均耗时、P95 耗时;异常阈值黄色 / 红色提示。
- ClickHouse:启用 / 健康状态环、查询延迟(ms)、版本、表数量;未配置时显示"未配置"。
- 调度任务:运行中数量、今日总计 / 成功 / 失败、成功率进度条、最近执行记录。
本地缓存状态(表)
字典缓存(dict)与系统参数缓存(config)的 大小 / 命中 / 未命中 / 命中率(按 90/60/30% 分色)/ 最后加载时间 / 已加载状态——重启清零。
请求与行为
- 最近请求:时间 / 方法 / 路径 / 状态 / 耗时;状态码分布(2xx / 3xx / 4xx·5xx)水平条。
- 行为统计:页面访问分布(前端路由切换埋点实时上报,内存环形缓冲上限 500 条)、在线会话数、今日活跃任务、运行时长、堆峰值、CPU 核数。
自动刷新
默认 5s 轮询(可选 3 / 5 / 10 / 30s),可暂停或手动刷新。后端 PlatformMonitorController(/system/monitor),权限 system:monitor:list。
8.2 在线用户
作用与价值
查看当前所有登录会话,必要时强制某用户下线(如异常登录、账号回收)。
页面字段
| 字段 | 说明 |
|---|---|
| 用户 / 登录ID / 设备 | 当前会话标记"当前",不可踢 |
| 登录时间 | 由 Sa-Token 会话 lastActivityTime 解析 |
| 操作 | 下线(system:online:kick),每 8s 自动刷新 |
实现思路
后端读 Sa-Token 会话列表;kick 调 /system/online/kick?loginId=... 使该 token 失效。权限 system:online:list / system:online:kick。
8.3 审计日志
作用与价值
记录关键操作(增删改查)的"谁、什么时间、对什么、做了什么、结果如何",用于合规追溯与问题定位。
页面字段
操作人、IP、URL、模块、动作(add / edit / delete / query)、结果(成功 / 失败)、耗时(ms)、操作时间;支持按操作人 / 模块 / 动作 / 时间范围筛选;点"详情"看方法名、错误信息、请求参数(JSON)。可删除单条。
实现思路
通过 @OperationLog 注解 + AOP 切面自动落库(SysOperationLog),业务代码无需手工记。后端 AuditController(/system/audit),权限 system:audit:list / system:audit:delete。
8.4 登录日志
作用与价值
审计日志的"登录视角"筛选——专看登录成功 / 失败、来源 IP、User-Agent、失败原因,用于安全审计(如暴力破解、异地登录)。
页面字段
登录时间、账号、IP、User-Agent、结果(登录成功 / 登录失败)、失败原因;支持按账号 / 结果 / 时间范围筛选;详情同审计日志。
说明
复用审计日志接口(/system/audit,模块固定为 auth),权限同为 system:audit:list / system:audit:delete。
安全排查建议:同一账号短时间大量失败 → 针对性撞库,建议改强密码;同一 IP 失败但账号名五花八门 → 扫描器爆破,建议在网关层封禁该 IP;成功记录出现陌生 IP/UA → 立即到 8.2 在线用户强制下线并改密。
另注意:静默刷新(
/auth/refresh)不会产生登录日志——它只是续期,不是新登录;所以看到"用户在线一整天却只有早上一条登录记录"是正常的。九、部署与运维
前八章讲的是"平台能做什么",本章讲的是"怎么把它跑起来、跑稳定、出问题怎么救"。面向部署人员与日常运维,内容全部来自实际踩过的坑,按部署 → 启动 → 配置 → 探活 → 排错 → 数据陷阱的顺序展开。
9.1 部署架构
整体分层
| 层 | 组件 | 说明 |
|---|---|---|
| 接入层 | Nginx | 托管前端静态产物,反代 /api 到后端;配置见 deployment/nginx.conf |
| 前端 | React 18 + Vite + Ant Design 5 | 构建产物输出到 frontend/build;状态管理 zustand,图表 ECharts |
| 后端 | Spring Boot 3 + MyBatis-Plus(单 jar) | 端口 8080,context-path = /api(所有接口都带这个前缀,容易漏) |
| 计算 | Python 脚本 | 数据采集、因子计算、前复权刷新等由后端拉起子进程执行 |
| 数据层 | MySQL 8 + ClickHouse | MySQL 存元数据/配置/权限;ClickHouse 存海量行情与因子值。Docker Compose 编排还预置了 Redis,便于后续扩展 Sa-Token 分布式会话等场景 |
为什么要 MySQL 和 ClickHouse 两套库?
两类数据的读写特征完全相反:元数据(用户、角色、因子定义、策略配置)量小但要求事务与强一致,是典型 OLTP,MySQL 最合适;行情与因子值动辄数千万行、按列聚合扫描、几乎只追加不更新,是典型 OLAP——用 MySQL 存会让一次因子回测扫表扫到超时,而 ClickHouse 的列存 + 向量化执行能把同样的查询压到秒级。硬塞进一套库,必然在某一侧付出数量级的性能代价。
后端模块划分
common→platform-core→stock-service:依赖严格单向,反向依赖会直接导致构建循环platform-core:认证、权限、字典、配置中心、监控等平台通用能力stock-service:股票业务(因子/策略/回测/推荐),也是最终打包启动的模块backend-mp:小程序端,独立部署,与 Web 端认证方式不同(走请求头 token)
两种部署方式
| 方式 | 命令 | 适用 |
|---|---|---|
| Docker Compose(推荐) | docker compose up -d | 一键拉起 app + mysql + clickhouse + redis,依赖服务带 healthcheck 且 app 会等它们就绪;restart: unless-stopped 自动重启 |
| 裸机 / 手工 | 见 9.2 | 本地开发、或已有现成数据库实例 |
Dockerfile 采用多阶段构建:第一阶段用 maven:3.9-eclipse-temurin-21 编译(先复制 pom 再复制源码,利用层缓存加速依赖下载),第二阶段用 eclipse-temurin:21-jre 仅装运行时并以非 root 用户运行,缩小镜像体积与容器逃逸影响面。
9.2 启动与停止
后端构建
正确:
mvn -pl common,platform-core,stock-service -am clean package -DskipTests错误:
mvn -pl stock-service clean package —— 这样会去本地仓库读 旧版本的 common / platform-core jar,你刚改的公共模块代码根本没进包,表现为"改了代码却完全没生效",极难察觉。clean 全量重建只跑增量
package 时旧 class 可能残留,新增的 @GetMapping/@PostMapping 不会注册,请求会落到静态资源处理器并报 No static resource xxx。看到这个错误先怀疑构建,而不是怀疑路径写错。启动
java -jar stock-service/target/stock-service-1.0.0.jar --spring.profiles.active=prod
- 开发环境默认 profile 为
dev(SPRING_PROFILES_ACTIVE未设时) - 若运行环境注入了
SERVER__PORT之类的环境变量,需显式--server.port=8080覆盖,否则会监听到意外端口 - Windows 本地可用根目录的
start-stock-service.bat/start-frontend.bat
停止与重启
端口被旧进程占着时,新进程启动会失败或悄悄换端口,结果是"部署了新包但行为还是旧的"——这是最浪费时间的一类假象。
netstat -ano | findstr :8080 查占用 PID → taskkill /F /PID <pid>(Linux:lsof -i:8080 → kill -9)。构建前同样建议先停掉 java 进程,否则 jar 文件被占用导致打包失败。前端
| 场景 | 操作 |
|---|---|
| 开发 | npm start(Vite dev server),已配置 proxy 把 /api 与 WebSocket 转发到 localhost:8080,无需处理跨域 |
| 构建 | npm run build,产物输出到 frontend/build |
| 发布 | 将 build 目录交给 Nginx 托管;SPA 必须配置 history fallback(未匹配路径回退 index.html),否则刷新子路由页面会 404 |
9.3 配置项清单
Profile 策略
application.yml 放公共参数,application-dev.yml 提供本地默认值,application-prod.yml 不带任何默认值,敏感项全部要求环境变量注入——缺失即启动失败(fail-fast),避免生产误用开发口令。
生产必须注入的环境变量
| 变量 | 用途 | 不设的后果 |
|---|---|---|
MYSQL_HOST / MYSQL_PORT / MYSQL_DATABASE / MYSQL_USER / MYSQL_PASSWORD | MySQL 连接 | 启动失败 |
CLICKHOUSE_HOST / CLICKHOUSE_PASSWORD(端口默认 8123,库默认 stock) | ClickHouse 连接 | 启动失败 / 行情与因子查询不可用 |
CORS_ALLOWED_ORIGINS | 允许的前端域名(逗号分隔) | 生产强制要求;dev 未配置时放开所有来源 |
SA_TOKEN_COOKIE_SECURE | 登录 Cookie 是否仅 HTTPS 传输 | HTTPS 环境必须 true;但在纯 HTTP 环境设 true 会导致 cookie 种不上、永远登不进去 |
CREDENTIAL_AES_KEY | 凭证管理 AES-256 主密钥(建议 32 字节随机串) | prod 下未设直接 fail-fast;dev 用内置开发密钥(不安全) |
SPRING_PROFILES_ACTIVE | 激活的 profile | 默认 dev,生产忘记设会连到开发库 |
关键可调参数
| 配置段 | 项 | 默认值与含义 |
|---|---|---|
sa-token | timeout | 1800(访问令牌 30 分钟) |
active-timeout | 1800(30 分钟无活动则失效,有活动自动续期) | |
is-concurrent | false(同账号互斥登录,新登录踢旧会话) | |
cookie.same-site | Lax(挡多数跨站 CSRF) | |
auth | refresh-token-ttl-seconds | 604800(刷新令牌 7 天) |
login-security | account-lock-threshold / minutes | 5 次 / 15 分钟 |
ip-lock-threshold / minutes | 20 次 / 15 分钟 | |
captcha-threshold | 3 次后要求图形验证码 | |
slow-sql | threshold-ms | 1000(超过 1 秒的 SQL 记入 slow-sql.log) |
spring.datasource.hikari | maximum-pool-size | dev 30 / prod 50 |
mybatis-plus | logic-delete-field | deleted(全局逻辑删除,影响面很大,见 9.6) |
9.4 健康检查与探活
健康检查端点
GET /api/actuator/health → {"status":"UP"}
注意路径带 /api 前缀(context-path 所致);仅暴露 health 与 info,且 show-details: never,不泄漏内部组件细节。Docker HEALTHCHECK 与负载均衡探针都用它。
三级判活
| 层次 | 方法 | 结论 |
|---|---|---|
| 端口 | netstat -ano | findstr :8080 是否 LISTENING | 没有 → 进程没起来或端口被换 |
| 进程 | tasklist | findstr java(Linux:ps -ef | grep java) | 有进程无端口 → 启动过程中报错,看日志 |
| 业务 | 调 POST /api/auth/login 或健康端点 | 端口通但接口 500 → 多半是数据库连不上 |
重新部署时杀掉旧 jar,会让原来的后台任务被标记为失败,这是正常现象。判断服务死活一律以上表三级探活为准,不要看任务状态色块。
日常巡检去哪看
- 运行时健康(JVM 堆、GC、QPS、P95 耗时、ClickHouse 延迟、调度任务成功率)→ 8.1 系统监控面板,5 秒轮询
- 缓存是否正常加载(字典/配置命中率,重启后清零)→ 8.1 的"本地缓存状态"
- 谁在线、有无异常会话 → 8.2 在线用户
- 谁改了什么 → 8.3 审计日志;有无爆破迹象 → 8.4 登录日志
- 数据是否完整 → 6.6 质量监控;任务是否跑齐 → 6.5 定时任务
- 慢 SQL →
logs/slow-sql.log
9.5 故障排查手册
启动与部署类
| 现象 | 根因与处理 |
|---|---|
| 部署了新包但行为还是旧的 | ① 旧进程仍占 8080(新进程没起来);② 只构建了 stock-service 而没带上 common/platform-core。按 9.2 两条警告逐一排除 |
接口报 No static resource xxx | 新增的 Controller 端点未注册 → 做一次 clean package 全量重建 |
| 启动即退出,日志报数据源错误 | prod profile 下环境变量缺失(fail-fast 设计);对照 9.3 补齐 |
构建时报 Failed to delete target\... 或 Source must refer to an existing file | target 目录文件被占用:先 taskkill 掉残留的 java / python 进程(因子计算脚本可能持有 target/classes/scripts 句柄);Windows 上若系统回收站异常也会阻断删除,清空回收站可恢复 |
| 访问任意子路由刷新后 404 | Nginx 未配 SPA history fallback |
认证与权限类
| 现象 | 根因与处理 |
|---|---|
| 提示登录成功但停在登录页 | 前端用 token 字段判登录态(恒 null),应改判用户对象。见 7.5 |
| 一直登不进去 / cookie 种不上 | HTTP 环境却把 SA_TOKEN_COOKIE_SECURE 设成了 true |
| 改了权限不生效 | 权限取自会话缓存,需重新登录。见 7.5 |
| 接口 403 但角色明明有权限 | @SaCheckPermission 的类级与方法级注解是 AND 关系,只放宽方法级无效,须同时处理类级 |
| 登录本该 401 却返回 500 | 通用异常处理器抢先吞掉了未登录异常——认证异常处理器需要更高优先级 |
| 非管理员账号列表分页报 SQL 错误 | 数据权限拦截器注入的 SQL 括号不平衡(管理员跳过拦截器所以不复现) |
| 用户能改本不该改的资源 | 部门路径前缀匹配未带 / 分隔符,导致 /1/30 被误判属于 /1/3。读写两侧都必须带分隔符 |
数据与任务类
| 现象 | 根因与处理 |
|---|---|
| 某因子数据停更 | 因子状态为 DEPRECATED,日增只算 ACTIVE。见 6.5 排错清单 |
| 删除操作报 500 | 优先怀疑唯一键冲突:全局逻辑删除下软删记录仍占用唯一索引,同名重建会撞 DuplicateKeyException。见 9.6 |
ClickHouse 写入报 Max query size exceeded | 批量写入分块过大,需按实际字节数而非行数切分 |
ClickHouse 写入报 Syntax error at position 1 ({) | SQL 传递方式错误——必须放在 URL 的 ?query= 参数里,请求体只放数据。见 9.6 |
| 任务卡在"运行中" | 状态未回写,会让并发守卫持续跳过后续调度;确认进程状态后手工改为失败再重跑 |
9.6 数据层陷阱
以下几条是会导致结果错得"看起来很合理"的坑,比直接报错更危险,新接手的人务必先看。
① 全局逻辑删除
deleted 字段),意味着所有 deleteById 都是软删,即使实体没写 @TableLogic 注解也一样生效。两个后果:① 以为记录删掉了,其实只是被置了标记;② 数据库唯一索引不认软删——软删一个用户名/角色编码后再建同名的,会直接抛
DuplicateKeyException。所以删除接口报 500 时,第一反应应该是查唯一键冲突而不是查代码逻辑。另注意
sys_department 表没有 deleted 列,是真删。② 最后管理员保护
系统禁止删除 ADMIN 角色、禁止删除用户的最后一个角色、禁止删除自己、禁止删除最后一个管理员账号——避免把自己锁在系统外。前端对 ADMIN 相关操作有红色标记与二次确认。
③ ClickHouse 字段单位(最容易算错的地方)
| 字段 | 单位 | 注意 |
|---|---|---|
volume | 股(不是手) | 科创板 688/689 来自腾讯源的数据不做 ×100 换算 |
amount | 千元 | 回测引擎内部会 ×1000 转成元,自己写查询时别忘了 |
turnover_rate | 百分比数值(如 3.5 表示 3.5%) | 当成小数用会差 100 倍 |
code | 纯数字字符串 | 不带 sh/sz 前缀,跨库 join 时注意统一 |
④ ReplacingMergeTree 的"最终一致"
行情表用 ReplacingMergeTree,重复写入靠后台合并去重,合并不是实时的。刚写完立刻查可能看到重复行——查询时加 FINAL 或按主键聚合,不要假设写入即去重。
⑤ ClickHouse HTTP 写入的正确姿势
?query= 参数里(做 URL 编码),POST 请求体只放 FORMAT JSONEachRow 的数据行,Content-Type 用 text/plain。用
X-ClickHouse-Query 请求头传 SQL 在新版本 ClickHouse 上会被忽略,服务端转而把请求体当 SQL 解析,报出令人费解的 Syntax error: failed at position 1 ({)。另外分块要按真实字节数(约 1MB)切,不能按行数——宽表几十万行轻松突破
max_query_size(默认 256KB)。⑥ 前复权与除权日
除权日必须先跑前复权刷新再算因子,否则价格序列在除权点断裂,动量、波动、均线类因子会集体失真——数值仍然算得出来,只是全错,不会有任何报错提示。依赖链已内置这个顺序,手工补算时要自己保证。
⑦ 建表不会自动执行
系统不做自动 DDL,新增功能涉及的建表脚本在 stock-service/sql/ 下,升级时需手工执行。建表语句必须用 utf8mb4 字符集,否则中文与 emoji 会乱码或截断。
因子(选股能力的量化表达)→ 策略(因子组合+风控规则)→ 回测(策略在历史中的验证)→ 选股/推荐(策略在当下生成具体股票)→ 评估(验证收益的真实来源和持续性)→ 数据管理(支撑以上所有模块的底层数据质量)→ 系统管理(用户 / 角色 / 菜单 / 字典 / 配置中心 / 部门 / 数据权限 / 凭证,访问治理层)→ 系统监控(运行时健康、在线用户、审计与登录日志,安全与可观测层)→ 部署与运维(部署架构、配置、探活、排错与数据陷阱,工程支撑层)
每一步都不是孤立的——因子质量决定策略上限,回测质量决定策略可信度,推荐管线质量决定策略执行效果,评估质量决定策略迭代方向,数据质量决定以上一切的根基,系统管理决定了"谁能使用这一切",而部署与运维决定了"这一切能不能稳定地跑下去"。量化投资不是"找到一个好因子"那么简单,而是一个需要全链路质量保障的系统工程。