A股五因子模型归因分析
> Pay Skill 范式:鉴权型(agent_local)
> 鉴权型:履约在本机执行(需本地取数/解析大文件),远端网关只做支付鉴权,支付通过后返回 fulfillment: "agent_local",Agent 再跑 scripts/ 下的脚本产出结果。
Overview
这是一个收益归因工具,不是选股工具。
它回答的唯一问题:一笔收益里,有多少是承担已知风险换来的(beta),有多少是剔除这些风险后
仍然存在的(alpha)? 它不预测涨跌、不筛选个股、不输出任何买卖标的。
方法论来自海通证券《选股因子系列研究(四十八)——探索A股的五因子模型》(2019.05.28),
并落实 Barillas & Shanken(2017 RFS、2018 JF)的模型对比框架。
基准模型(默认,可用 --model 改):
r_t = α + β_Mkt·Mkt_t + β_SMB·SMB_t + β_PE·F_PE,t + β_SUE·F_SUE,t + β_Turn·F_Turn,t + ε_t
即市场 + 市值 + 估值(PE)+ 盈利(SUE)+ 换手率五因子。这是研报用贝叶斯方法在 31 个备选模型中
比出的后验概率最高者(47.81%),并在贝叶斯分类法下同样胜出(28.17%)。
三类归因对象
| 归因对象 | 要回答的问题 | 对应模式 |
|---|---|---|
| 因子 / 异象 | 剔除五因子后还独立有效吗?还是已知因子的马甲? | --intent validate |
| 策略 / 事件组合 | 收益来自能力还是风格暴露? | --intent attribution |
| 基金 / 组合净值 | 超额是选股能力还是押对风格? | --intent attribution |
边界(明确不做;遇到这类请求如实说明,别硬凑)
- ❌ 不输出股票名单、不做因子打分排序、不构建股票池——那是选股工具,本 Skill 无此能力;
- ❌ 不预测涨跌、不给买卖点、不做择时——本 Skill 只做已发生收益的事后分解;
- ❌ 不评价"好/坏"——只给描述性结论(如「α=0.36%,t=1.25,不显著」),
不说"这只基金好/值得买/该赎回"。这是合规红线,也是本工具与市面"基金诊断"产品的区别; - ❌ 不自动联网抓取行情数据。因子序列与目标序列由用户提供(CSV),
或由用户在 WorkBuddy 内通过已授权数据源取数后交给本 Skill; - ❌ 不替代基金合同约定的业绩归因口径,不替代合规披露文件。
When to Use(触发条件)
A. 因子 / 异象有效性检验(首要场景)
- 用户说「我挖到一个因子/异象,帮我看看靠不靠得住」「这是不是只是已知因子的马甲」时;
- 用户说「反转/波动率/流动性/ROE 异象剔除五因子后还剩多少」「这个 alpha 是真的吗」时;
- 用户有因子收益率序列,想判断它是否提供增量信息(相对已知五因子)时;
- 用户发现回测漂亮的因子实盘失效,想排查是否把 beta 当成了 alpha 时。
B. 策略 / 基金收益归因
- 用户说「帮我归因一下这个策略的收益」「年化 25% 里有多少是真本事」时;
- 用户说「用五因子模型看看这只基金」「基金增持策略到底赚的是什么钱」时;
- 用户想知道组合暴露在哪些风格上、风格切换时会怎样时。
C. 因子与模型工程
- 用户说「构建 A 股 SMB / F_PE / F_SUE / F_Turn 因子」「按 2×3 分组算因子收益」时;
- 用户说「F_PE 和 F_PB 哪个更适合 A 股」「F_SUE 和 F_ROE 选哪个」时;
- 用户想判断一个因子模型是否存在冗余因子(简约性)时。
明确的排除项(用户这么问时,如实说明本 Skill 不做):
- 「帮我选几只股票」「按这个因子给我一个股票池」「哪只基金值得买」→ 属选股与投资建议,不做。
Workflow(工作流)
> 付费前置:本 Skill 为鉴权型。必须先按文末「Pay Skill 模式」完成 X402 支付鉴权,
> 拿到 paid: true 后才执行下面的 Step 2 及之后。支付未确认前不得运行任何取数/计算脚本,不得输出任何结果。
Step 0 澄清输入(不要凭空假设)
先确认四件事,缺哪项就明确问用户,绝不用默认值蒙混:
| 项 | 说明 |
|---|---|
| ① 归因意图 | 目标序列是一个因子/异象,还是策略或基金净值?前者走 --intent validate,后者走 --intent attribution(默认)。判错会导致结论口径不对 |
| ② 目标序列 | 目标收益率序列及其频率(月/周/日)与区间 |
| ③ 因子序列 | 用户是否已有因子收益率序列?若有,列名与顺序;若没有,转 Step 1 先构建 |
| ④ 无风险利率 | Mkt 因子应为市场指数超额收益。若用户提供的是指数收益而非超额收益,需提醒口径差异 |
| ⑤ 单位 | 收益率必须是小数(0.0131 = 1.31%)。若用户给的是百分数(1.31),必须先除以 100 |
Step 1(可选)构建因子——用户没有现成因子序列时
python3 scripts/run_local.py factors --panel <面板.csv> [--size-col mktcap] [--ret-col ret] --out factors.csv
面板 CSV 至少含 date, ret, mktcap 及各因子指标列(pe / pb / roe / sue / turnover / ret_1m)。
分组规则见 references/rules.md 第二节。
Step 2a 因子 / 异象有效性检验——目标是因子序列时
python3 scripts/run_local.py attribute \
--factors factors.csv --target my_factor.csv --target-col ret \
--intent validate --out report.md
报告重点:α 是否显著、收益被哪些已知因子解释(按 |t| 排序)、R²、以及一句判定结论
(独立有效 / 可被已知因子解释 / 证据不足)。判定规则见 references/rules.md 第六节。
Step 2b 策略 / 基金收益归因——目标是净值序列时(默认)
python3 scripts/run_local.py attribute \
--factors factors.csv --target nav.csv --target-col ret \
--intent attribution [--model Mkt,SMB,FPE,FSUE,FTurn] [--freq 12] \
--out report.md --json result.json
报告重点:α、各 β 及 t/p 值、R²、收益贡献分解占比排序、GRS 检验。
解读口径严格遵循 references/rules.md 第三节的显著性分级。
Step 3(可选)模型对比——判断因子形式与模型简约性
# 传统回归:比较同类因子的两种形式(如 F_PE vs F_PB)
python3 scripts/run_local.py compare --factors factors.csv --method regression \
--model-a Mkt,SMB,FPB --model-b Mkt,SMB,FPE
# 贝叶斯:同时比较 2^L-1 个模型,输出后验概率排序
python3 scripts/run_local.py compare --factors factors.csv --method bayes \
--candidates SMB,FPE,FPret,FTurn,FROE [--prior-mult 1.5]
# 贝叶斯分类:同类因子不同形式混合比较(研报 2.3 节)
python3 scripts/run_local.py compare --factors factors.csv --method classify \
[--groups '{"估值":["FPE","FPB"],"盈利":["FSUE","FROE"]}']
Step 4 出报告——按 references/output_templates.md 组织成 Markdown / HTML 交付
--out 后缀为 .html 时自动生成带样式的 HTML 报告(支持深色/浅色主题)。
需要 Word/Excel 时,先用 --json 导出结构化结果再转换。
Step 5 口径核对——把用户结果与 references/rules.md 第四节的研报实证基准对照,
差异较大时提示可能的原因(样本区间不同、因子构建口径不同、是否剔除 ST/新股、是否市值加权等)。
Output(输出规范)
- 默认输出中文 Markdown 报告,结构见
references/output_templates.md。 --intent attribution:因子风险收益特征 / 相关性矩阵 / 回归结果 / 收益分解 / GRS 检验 / 结论与解读;--intent validate:因子原始收益 / 回归结果 / 被哪些已知因子解释 / GRS 检验 / 有效性判定;- 数字口径:收益率统一用百分比展示(保留 2 位小数),系数保留 3 位小数,p 值保留 4 位;
- 显著性标记:
*** p<0.01、** p<0.05、* p<0.1,无标记即不显著; - 缺失处理:取不到的数据一律写「未获取」并说明原因,严禁因为用户已付费而推测补造;
样本量不足(T ≤ 因子数 + 3)时直接报错并说明需要多少期,不得硬算; - 结论纪律(两条都必须严守):
- α 不显著时必须明确说「不能拒绝 α=0,收益可被因子解释」,不能含糊地说「有超额收益」;反之亦然;
- 只描述,不评价。不出现"好/差/值得买/该赎回/推荐"等评价性措辞,
不输出任何股票名单或标的排序。用户追问"那我该买哪只"时,如实说明本工具不提供该功能。
Notes / Constraints(约束)
- 免责声明:本 Skill 产出的是统计归因结果,不构成投资建议,不预测未来收益。
每份报告末尾必须带风险提示(模型误设风险、因子有效性变化风险)。 - 不替代:不替代基金定期报告/招募说明书约定的业绩归因口径,不替代合规与审计结论。
- 样本量底线:时间序列回归至少需
T > N + K + 3;月度数据建议 ≥ 36 期,
贝叶斯模型对比建议 ≥ 60 期,否则后验概率不稳定。 - 频率一致性:目标序列与因子序列必须同频率、同区间;脚本按日期内连接对齐,
并在报告中明示被剔除的期数。 - 同类因子不得同时入模:F_PE 与 F_PB、F_SUE 与 F_ROE 相关系数高达 0.89 / 0.80,
同时入模会导致共线与过拟合。用--method classify让模型自动在同类形式中二选一。 - F_Pret 与 F_Turn 视为不同类:二者相关性仅 0.08,可以同时入模(研报 2.3 节明确)。
- 先验倍数敏感性:
--prior-mult默认 1.5(BS2018 原设 Sh_max = 1.5 × Sh_Mkt)。
结论对该参数敏感时,应同时跑 1.2 / 1.5 / 2.0 三档做稳健性说明,不要只报单一结果。 - 归因 ≠ 因果,更 ≠ 预测:本报告只分解已发生的收益,不说明收益未来能否延续。
因子有效性会随时间变化,历史 α 显著不代表未来仍显著。 - 被误当成选股工具时:用户问「那我买什么」「给我一个股票池」时,
如实说明本 Skill 是归因工具,不具备选股能力,不要拿归因结果反向拼凑标的推荐。
Pay Skill 模式(鉴权型,按次付费,已接入 X402)
本 Skill 默认以「提示词型」运行于 WorkBuddy 内,免费使用。作为付费技能(Pay Skill),
已通过腾讯 SkillHub + 微信支付 Agent Pay X402 协议接入按次收费。
鉴权型:履约在本机执行(需调用本地取数脚本 / 解析本地或远程大文件),
远端网关只负责订单鉴权与支付状态确认,支付通过后返回 fulfillment: "agent_local",
不返回业务结果;Agent 收到 paid: true 后才在本地跑 scripts/ 下的脚本产出结果。
- 统一网关主接口:
POST https://pay.zerone-iteration.com/api/invoke
(请求体{"skill": "ashare-five-factor-attribution", "skill_version": "1.1.0", "query": "...", "amount": 100}) - 兼容入口:
POST https://pay.zerone-iteration.com/api/resource(等价于默认 Skill) - 支付回调:
POST https://pay.zerone-iteration.com/api/pay/notify - 退款回调:
POST https://pay.zerone-iteration.com/api/refund/notify - 实现:透明网关(nginx + Java 17 / Spring Boot + WechatPay-Java,微信支付公钥模式);
skill/skill_version仅做格式校验后透传进 X402 预下单,不查 SkillHub(不查注册表、不反查是否真实存在/已发布/版本匹配);
无本地白名单,新增 Skill 免 Java 注册、免改 JAR、免重启,直接POST /api/invoke传正确 slug+版本即可 - 状态:⚠️ 尚未联调,不可声称已通。
- 已验证:本机履约链路。执行
python3 scripts/run_local.py demo可跑通因子构建 → OLS 归因 →
GRS 检验 → 贝叶斯后验概率 → 贝叶斯分类全链路;统计实现已与研报数值交叉校验
(N=1 时 GRS 恰等于 α 的 t 统计量平方;F(2,115)=1.17 → p=0.3140,研报 0.3138)。 - 未验证:X402 真实扣费链路尚未做生产实测。
- 发布前必做:按
PAY_SKILL_UPGRADE.md第五节,先DEV_MODE=true验 402 响应结构
(重点确认 body 含WeixinPay嵌套结构),再切真实密钥跑 1 元真扣,
通过后回填本条为「已实测」。
Agent 侧调用规范(含付费前置插件检查)
Step 0 付费前置插件检查(必须,缺失则不得发起付费调用)
- 检测当前环境中
weixinpay_pay插件(微信支付 Agent Pay / AI 专属卡)是否已安装并已开通; - 若不存在或未开通:不调用
/api/invoke,改为提示用户先安装/开通微信 AI 支付,完成后再继续; - 若可用:继续 Step 1。
Step 1 调用统一网关:POST /api/invoke,请求体:
{
"skill": "ashare-five-factor-attribution",
"skill_version": "1.1.0",
"query": "用户原始需求(用于订单留痕,不作为履约输入)",
"amount": 100
}
skill:必填,固定为本 Skill 的 slugashare-five-factor-attribution(与 SkillHub 已发布的一致;透明网关下传错 slug 时错误延后到 SkillHub/微信预下单环节才返回);skill_version:必填,当前版本号1.1.0(缺失返回400 MISSING_SKILL_VERSION);amount:必填,单位分,服务端校验为大于 0 的整数(小数/布尔/零/负数一律400 INVALID_AMOUNT,不静默截断);
区间不强制,价格由 SkillHub 产品定价约束。
Step 2 支付触发(X402 预下单 → 402):未支付时服务端依次执行——
- 生成订单号
out_trade_no(402 响应头X-Out-Trade-No与响应体out_trade_no均会返回,Agent 须记下); - 调用微信支付 Native 下单,获取
code_url; - 用 SkillHub 开发者私钥(SHA256withRSA)对 X402 AI 预下单请求签名;
- 调用
POST https://payapp.weixin.qq.com/palmpayminiapp/clawagentpay/preorder换取payment_code; - 返回 HTTP 402,响应头
WeixinPay-Required与 bodyWeixinPay块中
同时携带 X402 预下单返回的payment_code(必须是 X402 的payment_code,
不能直接把微信code_url当作payment_code返回),并回显本单amount与out_trade_no。
⚠️ body 的WeixinPay嵌套结构最易遗漏:缺失时 weixinpay 插件从 body 解析不到
payment_code,会报「当前支付订单已过期」,该报错常被误判为「返回了 code_url」。
Step 3 用户支付授权:Agent 将 WeixinPay-Required 头(或 body WeixinPay 块)中的
payment_code 作为 paymentCode 交给 weixinpay_pay 插件,向用户申请支付授权。
Step 4 二次调用鉴权:用户完成支付后,Agent 再次调用 POST /api/invoke
(请求体同 Step 1,带请求头 X-Out-Trade-No: <402 返回的订单号>);
服务端按订单号主动向微信查单验证支付成功(不依赖回调到达)后返回:
{"ok": true, "paid": true, "fulfillment": "agent_local",
"skill": "ashare-five-factor-attribution", "out_trade_no": "...", "already_fulfilled": false}
Step 5 本地履约(本 Skill 的关键差异):Agent 收到 paid: true 且
fulfillment: "agent_local" 后,在本机执行 scripts/ 下的脚本产出结果
(见 scripts/README.md),再把结果交付给用户。
支付未确认前,不执行本地取数脚本、不输出任何付费内容
(未支付重试返回 402 {"code":"NOT_PAID"})。
> 幂等说明:already_fulfilled: true 表示同一订单此前已鉴权通过;
> 此时直接复用上次已产出的结果,不要重复跑取数脚本(避免重复消耗外部接口配额)。
异常处理说明(Agent 与服务端两侧)
| 异常场景 | 处理方式 |
|---|---|
weixinpay_pay 插件不可用 |
不发起付费调用,引导用户安装/开通微信 AI 支付后重试 |
| 用户拒绝支付 / 支付取消 | 如实告知用户支付未完成,不推进后续流程、不输出付费内容;可重新发起(服务端生成新订单) |
| 支付失败(余额不足/风控拦截) | 如实转达失败原因,引导稍后重试;失败订单作废,不据此履约 |
payment_code 过期(最长 15 分钟) |
提示订单已过期,Agent 重新调用获取新订单与新 payment_code,不得复用旧码 |
| 报错「当前支付订单已过期」 | 先查 402 响应体是否有 WeixinPay 嵌套结构,其次才是 code_url 方向 |
| 支付成功但二次调用仍 402(回调延迟) | 等待 3~5 秒后重试,最多 3 次(指数退避),携带 X-Out-Trade-No、订单号不变 |
| 网络异常 / 5xx | 指数退避重试(1s/2s/4s,上限 3 次);持续失败告知服务暂不可用 |
| 本地取数脚本失败(数据源不可用/文件缺失) | 如实告知用户并说明原因,不得因为已付费而编造数据;可引导稍后重试同一订单(already_fulfilled 为 false 时不会重复扣费) |
| 金额校验失败(400 INVALID_AMOUNT) | 修正 amount 为大于 0 的整数分后重试 |
| skill 传错 / 未发布(错误延后到 X402/微信预下单环节) | 核对 skill/skill_version 与 SkillHub 已发布条目一致后重试,不得随便换 slug |
| skill_version 缺失(400 MISSING_SKILL_VERSION) | 补上当前版本号 1.1.0 后重试 |
| 未支付即重试(402 NOT_PAID) | 等待用户完成支付授权后按 Step 4 重试,不得跳过支付环节 |
| 退款 | 服务端经 /api/refund/notify 接收回调;已退款订单的 payment_code 失效,再次调用将重新走支付流程 |
> 核心原则(SkillHub 官方教程 FAQ):支付失败/未确认时必须如实告知用户,绝不据此推进后续流程。
核心特征对照表(供审核查验)
| # | 核心特征 | 实现位置 |
|---|---|---|
| 1 | X402 AI 预下单换取 payment_code(开发者私钥 SHA256withRSA 签名,调官方 preorder 接口;WeixinPay-Required 返回 payment_code 而非微信 code_url) |
pay/server.py skillpay_x402_preorder();链路 Step 2.3~2.5 |
| 2 | 支付触发(未支付返回 402 + WeixinPay-Required 头/体双通道) |
pay/server.py _handle_invoke();链路 Step 2.5 |
| 3 | 订单号传递(out_trade_no 全链路关联:下单→预下单→回调置位→X-Out-Trade-No 重试→二次调用) |
pay/server.py generate_trade_no() / mark_paid / 402 响应 X-Out-Trade-No 头 |
| 4 | 重试机制(402 回调延迟重试带 X-Out-Trade-No、5xx 指数退避、订单过期重新下单) |
上表「异常处理说明」 |
| 5 | 付费前置插件检查(weixinpay_pay 可用性检测,缺失不发起付费调用) |
链路 Step 0 |
| 6 | 异常处理说明(支付失败/取消/过期/退款/本地脚本失败等全场景) | 上表「异常处理说明」 |
| 7 | 透明网关(skill/skill_version 仅做格式校验后透传,不查 SkillHub、不查注册表) |
pay/server.py resolve_skill()(无 SKILL_REGISTRY,免 Java 注册/重启) |
包内 pay/server.py 为 Python 参考实现(含 APIv3 签名、X402 预下单、回调验签解密、
主动查单、幂等鉴权、透明网关无需注册表),部署说明见 PAY_SKILL_UPGRADE.md 与 pay_skill.yaml。
注意:本 Skill 涉及两套独立密钥/证书——
- 微信支付 API 证书(
wechat_pay):用于微信 Native 下单与主动查单; - SkillHub 开发者密钥(
skillhub):用于 X402 预下单签名。
私钥均不得写入 Skill 包或仓库,须通过环境变量注入服务端。