当一家公司的利好被分析师 写进研报、被市场定价之后,这份信息还会顺着"关联网络"流向哪些尚未反应的股票?
股票对共同基本面冲击的价格反应并不同步,关联股票可以成为目标股票的前置信息源 。但"关联股票涨了"本身几乎没有预测力(RankIC ≈ 0),真正有效的是关联股票出现了正向基本面惊喜 这一信号。基于"股票池筛选 + 扩散因子排序"的两阶段框架,在完全一致的回测口径下,Top10 组合的年化净收益从 12.90% 提升到 22.80% ,Sharpe 从 0.61 提升到 0.92。
这篇文章假设你已经熟悉多因子选股的基本流程(因子暴露、中性化、RankIC 检验、分组回测)。如果你刚接触量化,建议先补一下经典动量和分析师预期因子的内容,再回来读这篇------因为我们要讨论的,恰好是这些经典因子覆盖不到的那部分信息。
一、问题:动量溢出存在,但"关联上涨"没有用
1.1 底层逻辑:价格发现不是同步完成的
同一条产业链上的公司,会同时暴露在相同的需求、成本、政策与景气冲击之下。但它们的研究关注度、投资者结构和信息处理速度各不相同------于是总有那么几只股票先完成定价,另一些随后跟随。这就是经典的"价格发现不同步"。
顺着这个思路,关联动量(cross-firm momentum)不是把目标股票自身动量换个名字,而是:用关联股票已经反映的信息,去识别尚未被充分定价的目标股票。学术上已有不少证据(如基于共同分析师覆盖的跨公司动量研究)。

要让这个机制成立,需要回答两个问题:
- 哪些股票之间存在稳定的信息联系?------关联网络的构建方式;
- 谁更可能先反映信息?------信息领先者的识别方式。
1.2 两类关联网络:分析师共同覆盖 vs 研报主题相似
(1)分析师共同覆盖网络。 过去 12 个月内,同一分析师同时覆盖两只股票,两者之间就形成一条关联边;共同覆盖的分析师数量越多,边强度越高。每只股票与全部关联股票的边权之和定义为强度中心度(SC)------高中心度的股票研究连接更广,更可能率先反映共同信息,信息从高 SC 股票流向低 SC 关联股票。
(2)研报标题主题网络。 把每只股票过去 6 个月的研报标题收集起来,剔除日期、代码、目标价和"公司点评""首次覆盖"这类模板词,用字符片段 TF-IDF 把标题文本转成主题向量,再计算股票两两之间的余弦相似度。每只股票保留相似度 ≥ 0.08 的前 30 只作为主题关联股票。注意这是有向的:A 把 B 视为关联,不代表 B 也把 A 视为关联。
两类网络的结构差异很大,这直接决定了它们能捕捉什么信息:

1.3 冷水:原始关联动量因子基本无效
直觉很美好,实测很骨感。把两类网络直接加权汇总成"关联收益因子"做检验(统计期 2017-01 至 2026-06,十分组):
| 因子 | RankIC 均值 | 月度 RankICIR | 备注 |
|---|---|---|---|
| 纯反转 | 3.74% | --- | 基准对照 |
| Gap(SC 关联收益 − 自身收益) | 3.90% | --- | 机械包含反转贡献,≠ 独立增量 |
| SC 加权关联收益 | −0.55% | −0.034 | 无效 |
| 主题网络关联收益 | 0.03% | 0.003 | 无效 |
分组收益上,两个因子的高低组之间确实存在差异(多空月均约 0.86%--0.92%),但组间不单调、Newey-West t 值只有 1.4--1.7,多头组合最大回撤也很高。结论:不能当独立选股信号用。
为什么?因为关联股票的上涨可能只是行业行情、主题拥挤或短期波动的产物------这类上涨没有"可传导的内容"。真正有溢出价值的,不是价格本身,而是价格背后的基本面变化。
二、改进:识别"具有外溢能力的信息"
2.1 用基本面惊喜替代股价上涨
"基本面惊喜"从三个维度取并集判定,任一维度为正即记为正向惊喜:
- 研报标题正向惊喜 :标题中出现"超预期""好于预期""业绩高增"等表述记正,"不及预期""低于预期""下修"记负,"符合预期"不计;正负同时出现时优先按负向处理(保守原则);
- 分析师评级上调:评级映射为有序阶梯,过去 3 个月机构平均评级变化 > 0;
- 盈利预测正向修订 :过去 3 个月 FY1 盈利预测修订幅度横截面中位数 > 0 且 净上调广度((上调家数 − 下调家数) / 有效修订家数)> 0,两个条件必须同时满足。
在此基础上定义网络传播变量:自身超预期 (OwnPos)、关联公司超预期 (以关联股票的 SC 为权重加权汇总其惊喜标记)、隐含关联公司超预期(自身尚无惊喜、但关联公司已经出现惊喜------这正是"信息还没传导到我"的形态),以及自身---关联联合超预期。
**覆盖率是个隐藏的瓶颈:**仅靠目标股票自身的基本面惊喜信号,月均只能覆盖严格筛选股票池的约 25%;把研报标题主题网络和共同分析师覆盖网络纳入后,月均覆盖率可以提升到约 76%。网络的价值之一,就是把"看不见的信息"变成"看得见的信号"。
2.2 三个层次的信号构造
- 信息暴露类:目标股票周边有多少关联公司已经出现正向惊喜(信息量本身);
- 价格确认类 :进一步要求关联公司在释放正向信息的同时取得了正收益(信息已被市场初步验证);
- 异常收益确认 :剔除行业与规模共同波动后,正向惊喜是否伴随更具公司特征的价格反应(确认这不是随行业beta起舞)。
2.3 两阶段框架:股票池 × 排序因子
这是整套方法的骨架。与其把所有信号塞进一个因子里硬拼权重,不如拆成两阶段------股票池回答"哪里值得看",排序因子回答"看谁":

2.4 核心对比:原始信号 vs 改进信号的预测力
改进是否真的有效?把同一统计期内各类信号的 RankIC 放在一起看(所有信号均控制对数市值与一级行业):

几个值得注意的细节:
- 主题网络的有效信息主要来自基本面惊喜本身,而非关联股票的一般收益联动;
- 共同覆盖网络的单纯惊喜暴露几乎无效(RankIC 仅 0.09%),但叠加异常收益确认后提升到 0.84%------价格确认对这类网络更关键;
- 未经基本面筛选的原始关联收益在复合因子中被用作负向控制项:如果一只股票的关联涨幅完全没有基本面支撑,反而应该扣分。
2.5 一个可以跑起来的最小实现
整套流程里最"可代码化"的两块是主题网络构建和惊喜信号判定。下面是一个教学级的最小实现(思路复现,非原始工程代码),依赖 scikit-learn 与 pandas:
"""关联图谱选股 · 教学级最小实现(思路示意)
环境:python 3.11 / pandas 2.2 / scikit-learn 1.5
数据:需要一张宽表 report_df,字段 = [日期, 股票代码, 研报标题, 评级, FY1盈利预测]
"""
import re
import pandas as pd
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
# ---------- 1. 研报标题清洗:剔除模板化信息 ----------
TEMPLATE_WORDS = r"(公司点评|首次覆盖|年报点评|季报点评|中报点评|目标价|\d{4}[-/年]\d{1,2}|\d{6}|维持|下调|上调评级)"
def clean_title(title: str) -> str:
return re.sub(TEMPLATE_WORDS, "", title).strip()
# ---------- 2. 主题相似度网络:字符片段 TF-IDF + 余弦相似度 ----------
def build_topic_network(titles: pd.Series, top_n: int = 30,
sim_threshold: float = 0.08) -> dict[str, dict[str, float]]:
"""titles: index=股票代码, value=过去6个月研报标题拼接文本
返回 {股票A: {关联股票B: 相似度, ...}, ...}(有向网络)"""
corpus = titles.map(clean_title)
vec = TfidfVectorizer(analyzer="char_wb", ngram_range=(2, 3), # 字符2-3元片段
max_features=5000)
X = vec.fit_transform(corpus)
sim = cosine_similarity(X) # N x N 相似度矩阵
codes = corpus.index.tolist()
net = {}
for i, code in enumerate(codes):
sims = [(codes[j], sim[i, j]) for j in range(len(codes)) if j != i]
sims = [(c, s) for c, s in sims if s >= sim_threshold]
net[code] = dict(sorted(sims, key=lambda x: -x[1])[:top_n]) # 保留前30只
return net
# ---------- 3. 基本面惊喜:三维度并集 ----------
POS_KEYWORDS = ["超预期", "好于预期", "业绩高增", "业绩亮眼", "超市场预期"]
NEG_KEYWORDS = ["不及预期", "低于预期", "下修", "业绩承压"]
def title_surprise(title: str) -> int:
"""研报标题惊喜:+1 正向 / -1 负向 / 0 中性。正负并存时保守取负。"""
has_pos = any(k in title for k in POS_KEYWORDS)
has_neg = any(k in title for k in NEG_KEYWORDS)
if has_neg: return -1 # 保守原则:负向优先
if has_pos: return 1
return 0
def fundamental_surprise(df: pd.DataFrame) -> pd.Series:
"""df: 单只股票过去3个月的研报明细。返回该股惊喜标记(1/0)。"""
title_pos = any(title_surprise(t) > 0 for t in df["研报标题"])
rating_up = (df["评级阶梯"].astype(float).diff().dropna() > 0).any()
# 盈利修订:幅度中位数 > 0 且 净上调广度 > 0,二者缺一不可
revisions = df.groupby("机构")["FY1盈利预测"].last().sub(
df.groupby("机构")["FY1盈利预测"].first(), fill_value=0)
up, down = (revisions > 0).sum(), (revisions < 0).sum()
valid = up + down
eps_rev = (revisions.median() > 0) and (valid > 0 and (up - down) / valid > 0)
# 三维度并集
return pd.Series(int(title_pos or rating_up or eps_rev))
# ---------- 4. 关联惊喜暴露(阶段一信号之一) ----------
def linked_surprise_exposure(net: dict, own: pd.Series,
sc: pd.Series) -> pd.Series:
"""以关联股票自身 SC 为权重,加权其惊喜标记,在有效关联范围内归一化。"""
expo = {}
for code, neighbors in net.items():
w = {nb: sc.get(nb, 0.0) for nb in neighbors if nb in own.index}
total_w = sum(w.values())
if total_w > 0:
expo[code] = sum(w[nb] * own[nb] for nb in w) / total_w
else:
expo[code] = 0.0
return pd.Series(expo)
# 之后:横截面百分位排名 → ≥2项进前30% → 入池 → 扩散因子排序
# 完整回测还需要:严格股票池过滤、行业市值中性化、月度调仓与交易成本
工程上容易踩的坑:
- 关联网络是有向的,且必须每月末用滚动窗口重建(12 个月共同覆盖 / 6 个月标题),防止未来函数;
- TF-IDF 一定要用字符片段(char n-gram)而不是分词------研报标题是短文本,分词在噪声标题上很不稳定;
- 评级阶梯映射要先对齐不同券商的评级体系(如 5 档制 vs 7 档制),否则"上调"会被体系差异污染。
三、效果:加进多因子策略之后发生了什么
3.1 与常见因子几乎不相关
增量信息的价值取决于它和你已有的东西重合多少。在关联暴露股票池内,对扩散因子和 64 个常见中性化因子统一剔除行业与市值影响后计算月均秩相关:

3.2 策略回测:增量集中在头部
把扩散因子加入一个覆盖质量、股东、估值、动量、量价相关性、资金流、分析师预期七个维度的等权基础策略(所有口径完全一致:同股票池、同执行日历、同交易成本,基准为中证500价格指数):

3.3 分年度:10 个区间里 8 个为正
2017 年至 2026 年上半年共 10 个年度区间中,Top10 增强策略在 8 个区间跑赢基础策略(Top30 为 6 个)。尤其是 2024 年以来增量显著放大:

3.4 组合特征:因子改的是排序,不是风格
加入关联图谱因子后,组合几乎没变"形状":
- 月均覆盖 7.54 个一级行业,第一大行业平均权重 25.31%,前三大 54.25%;
- 持仓平均市值 218 亿元(中位数 121 亿元),仅略低于基础策略;
- 月均完整 L1 换手率约 173%,与上月持仓平均重合率约 14%;
- 两策略同月 Top10 持仓平均重合率 68.23%。
也就是说,这个因子主要在调整头部个股的相对排序,而不是把组合切换到另一种行业或市值风格------这是判断"增量信息"而非"风格漂移"的重要证据。
四、参数敏感性与稳健性
股票池筛选阈值(单项信号进入当月前 30% 视为有效确认,基准 0.70)从 0.60 到 0.80 扫一遍:
| 阈值 | 关联暴露池月均股票数 | 关联暴露池 RankIC | 双网络确认池 RankIC |
|---|---|---|---|
| 0.60 | 1,012.5 | 2.19% | 2.08% |
| 0.65 | --- | 2.24% | 1.93% |
| 0.70(基准) | 723.8 | 2.36% | 1.69% |
| 0.75 | 581.0 | 2.60% | 1.83% |
| 0.80 | 439.9 | 2.37% | 1.38% |
结果不是"阈值越高越好"的单调关系------0.70 作为预先设定的基准,是在覆盖范围、确认强度和稳健性之间的折中。这种非单调性本身是好事:说明结果不是过拟合调出来的尖峰。
五、Trade-offs 与局限
- **增量集中在头部。**持仓从 Top10 扩到 Top50,增量从 +9.90 pct 衰减到 +0.35 pct。如果你的组合持仓很分散,这个因子帮不了你多少;
- **依赖卖方研报生态。**标题惊喜、评级和盈利预测全部来自分析师数据------覆盖稀疏的小票信号天然更弱,且研报文本风格的变化(如模板化加剧)会侵蚀标题信号质量;
- **网络构建成本不低。**每月滚动重建两张网络(TF-IDF + 相似度矩阵 + 共同覆盖统计),全市场口径下是一笔不小的计算开销;
- **Top10 结果的统计含义 > 实盘含义。**10 个持仓的组合方差很大,年化 22.80% 更多说明"因子在头部排序上有辨识力",而不是"你应该只买 10 只股票";
- 月度换手 173% 不低,对交易成本和冲击成本敏感,回测口径中已计成本,但实盘容量仍需评估。
想自己跑一遍回测?数据从哪来
文中这套框架落地时,最大的工程量其实在数据侧:研报标题、分析师评级、盈利预测修订、月度行情与停牌状态,每一项都是长期滚动维护的负担。如果不想从零对接各个数据源,可以试试 StockAPI------一个面向量化开发者的股票数据接口平台:
StockAPI(官网:www.stockapi.com.cn)
- 实时行情接口:毫秒级低延迟推送,99.99% 接口高可用,覆盖全市场核心品种,还支持 Level-2 深度数据(逐笔成交、10 档行情);
- 策略回测:内置回测系统,可以在统一口径下验证多因子策略,省掉自建回测引擎的初期成本;
- 资金流与游资监控:主力净流入、资金流向分析等数据,正好覆盖本文"七因子基础策略"里资金流维度的数据需求;
- AI 智能选股:结合行业数据与实时行情的选股服务,可以作为自研因子之外的参照基准;
- 接入友好 :标准 REST 接口,几行 Python 就能拉到数据,也支持导入 Excel。

以日线行情为例,注册拿到 token 之后:
import requests
url = "https://www.stockapi.com.cn/v1/base/day"
params = {"token": "YOUR_TOKEN", "code": "600519"}
response = requests.get(url, params=params)
data = response.json()
print(f"当前价格: {data['price']}")
print(f"主力净流入: {data['main_net_inflow']}")
注册即有免费测试额度,适合先跑通本文的最小实现,再决定是否接入正式数据源。
📌 本文为独立撰写的技术方法论文章:思路与公开量化研究(2026)中的关联图谱选股框架相关,文中数据引自该研究的公开摘要口径,仅作教学讨论用途;所有图表均为本文自行绘制,代码为教学级思路复现,与任何机构的原始工程实现无关。不构成投资建议。