用关联图谱做量化选股:让基本面惊喜沿着网络扩散

当一家公司的利好被分析师 写进研报、被市场定价之后,这份信息还会顺着"关联网络"流向哪些尚未反应的股票?

股票对共同基本面冲击的价格反应并不同步,关联股票可以成为目标股票的前置信息源 。但"关联股票涨了"本身几乎没有预测力(RankIC ≈ 0),真正有效的是关联股票出现了正向基本面惊喜 这一信号。基于"股票池筛选 + 扩散因子排序"的两阶段框架,在完全一致的回测口径下,Top10 组合的年化净收益从 12.90% 提升到 22.80% ,Sharpe 从 0.61 提升到 0.92

这篇文章假设你已经熟悉多因子选股的基本流程(因子暴露、中性化、RankIC 检验、分组回测)。如果你刚接触量化,建议先补一下经典动量和分析师预期因子的内容,再回来读这篇------因为我们要讨论的,恰好是这些经典因子覆盖不到的那部分信息

一、问题:动量溢出存在,但"关联上涨"没有用

1.1 底层逻辑:价格发现不是同步完成的

同一条产业链上的公司,会同时暴露在相同的需求、成本、政策与景气冲击之下。但它们的研究关注度、投资者结构和信息处理速度各不相同------于是总有那么几只股票先完成定价,另一些随后跟随。这就是经典的"价格发现不同步"。

顺着这个思路,关联动量(cross-firm momentum)不是把目标股票自身动量换个名字,而是:用关联股票已经反映的信息,去识别尚未被充分定价的目标股票。学术上已有不少证据(如基于共同分析师覆盖的跨公司动量研究)。

要让这个机制成立,需要回答两个问题:

  1. 哪些股票之间存在稳定的信息联系?------关联网络的构建方式;
  2. 谁更可能先反映信息?------信息领先者的识别方式。

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 用基本面惊喜替代股价上涨

"基本面惊喜"从三个维度取并集判定,任一维度为正即记为正向惊喜:

  1. 研报标题正向惊喜 :标题中出现"超预期""好于预期""业绩高增"等表述记正,"不及预期""低于预期""下修"记负,"符合预期"不计;正负同时出现时优先按负向处理(保守原则);
  2. 分析师评级上调:评级映射为有序阶梯,过去 3 个月机构平均评级变化 > 0;
  3. 盈利预测正向修订 :过去 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-learnpandas

复制代码
"""关联图谱选股 · 教学级最小实现(思路示意)
环境: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)中的关联图谱选股框架相关,文中数据引自该研究的公开摘要口径,仅作教学讨论用途;所有图表均为本文自行绘制,代码为教学级思路复现,与任何机构的原始工程实现无关。不构成投资建议。

相关推荐
Lonely 净土7 小时前
Linux 运维文件写入操作
linux·运维·服务器·文件管理
城管不管7 小时前
重生——第十一次面试之挖财一面2026.8.19已OC
java·服务器·jvm·数据库·spring·面试·职场和发展
liuyicenysabel9 小时前
多服务上线日记三:
运维·服务器·笔记·学习
吠品10 小时前
Wine 在 Linux 上运行 Windows 软件完整指南
java·linux·服务器
AuTumn-L12 小时前
使用 SSH 密钥认证实现安全免密登录
服务器·安全·ssh
2601_9657984712 小时前
Salient Theme Setup Guide for Fast Creative WordPress Sites
数据库·web3·php·wordpress
深维AI随笔13 小时前
《构建之法》| 第二章个人技术和流程:单元测试不是“可选“,而是“必需“
单元测试·log4j·apache
知无不研13 小时前
Linux I/O复用之epoll
linux·服务器·epoll·socket编程
dogstarhuang14 小时前
OpenAI GPT-5.6 降价后如何重算 API 账单?多模型路由与成本治理实战
服务器·网络·人工智能·大模型·api·ai应用开发·接口管理