尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

用Python分析SEC 13F机构持仓:从爬虫到调仓地图全攻略

用Python分析SEC 13F机构持仓:从爬虫到调仓地图全攻略 近半年聊 AI 产业链的人越来越多但观点也明显分成两派一边是模型能力还在迭代一边是部分资金开始从“无差别买入”转向“结构性调仓”。与其听各种二手观点不如直接看一手数据——美国 SEC 要求符合条件的大型机构每个季度提交 Form 13F公开披露自己的部分持仓。这份文件既是金融研究的重要素材也是 Python 数据分析、爬虫和可视化练手的好案例。本文不预测行情而是把 13F 当作一份公开数据源带你从零完成一条完整链路SEC EDGAR 数据获取 → XML 解析 → 季度持仓对比 → 调仓地图可视化。学完之后你可以把同一套代码扩展到任意一家披露 13F 的机构甚至做成定时任务持续监控。1. 背景什么是 13F为什么值得分析1.1 13F 文件是什么13F 全称是 Form 13F由美国证券交易委员会SEC要求机构投资经理定期提交的报告。只要机构的管理资产规模超过 1 亿美元并且持有 13(f) 证券就需要在每个季度结束后的 45 天内向 SEC 提交上一季度末的持仓明细。这里说的 13(f) 证券通常覆盖在美国交易所上市或有一定规模以上的股票、期权、可转换债券等资产。表中最重要的几项信息包括证券名称name of issuer证券类别title of classCUSIP 编码证券唯一标识市值value单位是千美元持股数量sshPrnamt数量类型SH 表示股数PRN 表示本金金额换句话说13F 是一份“季度末快照”。它告诉我们在每一个季度结束时头部机构手里拿着哪些证券、每只证券值多少钱、持有多少数量。1.2 为什么 13F 是观察机构资金的窗口普通投资者很难看到头部机构的实时成交记录但 13F 提供了目前最接近“透明化”的定期披露窗口。它的优势非常明显数据标准化所有机构使用同一套表格和字段。强制披露满足条件的机构必须按时提交。跨季度可比同一家机构的历史文件可以纵向对比。覆盖范围广几乎所有大型机构都会出现在 EDGAR 系统里。比如市场讨论比较多的 AI 产业链涉及芯片、云计算、应用软件等多个环节。通过对比不同季度的 13F 数据可以观察到头部机构是在加仓某只芯片股还是把仓位挪到了软件方向这种“用脚投票”的过程非常值得用数据分析去还原。1.3 13F 数据的边界13F 虽然公开但并不是一份“完美持仓清单”。使用之前必须清楚它的限制存在滞后性报告在季度结束 45 天之内提交你看到的数据往往已经是 45 天前的旧信息。不是全部仓位部分证券类型不受 13F 约束例如美国国债、某些外国资产等。存在隐藏空间机构可以向 SEC 申请 confidential treatment暂时隐藏某些持仓。存在口径差异同一家公司如果有多个投资经理可能拆成多份文件数据合并时要注意重复计算。单位容易看错value 字段单位是“千美元”不是美元初学者经常在这里踩坑。理解这些边界才能在后续做分析时不把 13F 数据当成“实时买卖信号”来用。2. 环境准备与整体流程2.1 环境依赖本文以 Python 3 为例建议使用 Python 3.9 及以上版本。核心依赖只有四个requests发送 HTTP 请求从 SEC 下载数据pandas解析 XML做季度数据对比lxmlpandas 读取 XML 时依赖的解析器matplotlib绘制调仓地图建议先创建一个独立的虚拟环境mkdir 13f-analysis cd 13f-analysis python -m venv venv source venv/bin/activate # Windows 下使用 venv\Scripts\activate pip install requests pandas lxml matplotlib版本不需要刻意锁死以当前 pip 能安装到的最新稳定版为准。安装完成之后项目结构建议这样规划13f-analysis/ ├── config.py # 请求头与路径配置 ├── fetch_13f.py # 下载 13F 数据 ├── analyze_13f.py # 解析与对比 ├── visualize_13f.py # 可视化 └── data/ # 存放下载的 XML2.2 整体流程整个分析过程可以拆成五步根据机构名称或股票代码在 EDGAR 系统里找到对应的 CIK 编号。通过 CIK 查询最近的 13F-HR 提交记录。下载 13F 信息表 XML 文件。用 pandas 解析 XML获得结构化持仓表。合并两个季度的持仓数据计算调仓变化并可视化。每一步都不复杂但每一步都有容易忽略的细节。下面按顺序展开。3. 从 SEC EDGAR 获取 13F 数据3.1 SEC 对请求的要求SEC EDGAR 系统对爬虫是比较友好的官方提供了结构化 JSON 接口但同时也要求请求方遵守公平访问原则。最重要的一点是请求头里必须带上 User-Agent并且这个 User-Agent 要能联系到你。建议的格式是公司名 联系邮箱比如User-Agent: MyResearchTeam adminmycompany.com如果没有规范的 User-Agent或者请求频率过高服务器会直接返回 403。另外官方建议请求频率控制在每秒 10 次以内日常分析完全不需要这么快加一个 sleep 反而更稳妥。3.2 根据 Ticker 查询 CIKEDGAR 系统内部使用 CIKCentral Index Key来标识每一家机构。CIK 是一串数字需要通过查询接口把机构名称或代码转换成 CIK。先用一个配置文件保存请求头# config.py SEC_HEADERS { User-Agent: YourCompanyName adminyourcompany.com, Accept-Encoding: gzip, deflate, }再编写一个通过股票代码查询 CIK 的函数。SEC 提供了一个公开的映射文件company_tickers.json里面是所有上市公司的代码和 CIK 对应关系# fetch_13f.py import requests from config import SEC_HEADERS def get_cik_by_ticker(ticker: str) - str: url https://www.sec.gov/files/company_tickers.json resp requests.get(url, headersSEC_HEADERS) resp.raise_for_status() data resp.json() for item in data.values(): if item[ticker] ticker.upper(): return str(item[cik_str]).zfill(10) raise ValueError(f未找到 ticker: {ticker})这里有两个细节需要注意ticker统一转成大写再匹配避免大小写不一致。cik_str本身是数字需要用zfill(10)补足到 10 位后面拼接 URL 时才是规范格式。3.3 查询最近的 13F-HR 提交记录拿到 CIK 之后可以访问 SEC 的 Submission 接口获取该机构所有的历史提交记录def get_recent_13f_meta(cik: str) - dict: url fhttps://data.sec.gov/submissions/CIK{cik}.json resp requests.get(url, headersSEC_HEADERS) resp.raise_for_status() data resp.json() recent data[filings][recent] for i, form in enumerate(recent[form]): if form in (13F-HR, 13F-HR/A): accession recent[accessionNumber][i].replace(-, ) return { accession: accession, primary_doc: recent[primaryDocument][i], report_date: recent[reportDate][i], filing_date: recent[filingDate][i], } raise ValueError(未找到 13F 记录)filings[recent]是一个字典里面按时间倒序存放了表名、受理号、报告日期、原始文件等信息。我们只关心13F-HR定期报告和13F-HR/A修正报告。受理号里的横线要移除因为拼接下载地址时使用的是纯数字格式。3.4 下载信息表 XML13F 提交文件的核心内容是信息表Information Table通常是一个 XML 文件文件名一般是InfoTable.xml。拼接下载地址的规则是https://www.sec.gov/Archives/edgar/data/{CIK数字部分}/{受理号}/{主文件}对应代码如下from pathlib import Path def download_infotable(cik: str, meta: dict, save_path: str) - str: cik_num int(cik) # 去掉前导 0 base https://www.sec.gov/Archives/edgar/data url f{base}/{cik_num}/{meta[accession]}/{meta[primary_doc]} headers {**SEC_HEADERS, Accept: application/xml} resp requests.get(url, headersheaders) resp.raise_for_status() Path(save_path).parent.mkdir(parentsTrue, exist_okTrue) with open(save_path, wb) as f: f.write(resp.content) return save_path这里要注意int(cik)把 CIK 字符串转成数字因为 EDGAR 的文件目录路径里不希望出现前导 0。保存文件时用二进制模式写入避免文本编码问题。有的 13F-HR 主文件可能不是InfoTable.xml而是一个完整的提交文档.txt。遇到这种情况可以先把主文件下载下来再在里面定位informationTable部分或者去该 accession 对应的目录下找InfoTable.xml。本文先假设主文件就是信息表后面会在常见问题里给排查方案。4. 解析 13F 信息表4.1 信息表字段说明打开 13F 的 XML 信息表核心内容是大量infoTable节点。每个节点代表一只证券常用字段如下字段含义注意事项nameOfIssuer证券发行方名称字符串titleOfClass证券类别名称例如 Common StockcusipCUSIP 编码证券唯一标识合并的关键字段value市值单位是千美元sshPrnamt持股数量也可能是本金金额sshPrnamtType数量类型SH 是股数PRN 是本金putCall期权标识空值表示股票PUT/CALL 表示期权investmentDiscretion投资决定权SOLE / SHARED / DEFINEDvotingAuthoritySole独立投票权数量通常与持仓数量一致4.2 用 pandas 解析 XMLpandas 提供了read_xml方法可以直接把 XML 节点解析成 DataFrame。关键是要传对命名空间13F 信息表的命名空间是http://www.sec.gov/edgar/document/thirteenf/informationtable解析函数如下# analyze_13f.py import pandas as pd THIRTEEN_F_NS { ns: http://www.sec.gov/edgar/document/thirteenf/informationtable } def parse_13f(xml_path: str) - pd.DataFrame: df pd.read_xml( xml_path, xpath.//ns:infoTable, namespacesTHIRTEEN_F_NS, ) return df如果不传命名空间xpath经常匹配不到任何节点返回值是空表。这也是初学者最常见的解析失败原因之一。另外read_xml依赖lxml如果之前没有安装会直接报 ImportError。用本文开头的 pip 命令安装即可解决。4.3 数据清洗拿到原始 DataFrame 后需要做几个基础清洗动作把value转成数值类型方便后续计算。把sshPrnamt转成数值类型。把cusip统一转成字符串并去掉空格。过滤掉明显没有意义的空行。def clean_13f(df: pd.DataFrame) - pd.DataFrame: df df.copy() df[cusip] df[cusip].astype(str).str.strip() df[value] pd.to_numeric(df[value], errorscoerce) df[sshPrnamt] pd.to_numeric(df[sshPrnamt], errorscoerce) df df.dropna(subset[cusip, value]) return df清洗之后字段就是干净、可计算的格式了。5. 构建两个季度的调仓分析5.1 季度对比思路13F 是季度末快照所以“调仓”本质上是两个快照之间的差值。思路很简单下载最近两个季度的 13F 信息表。以 CUSIP 为 key做一次 outer join 合并。当前季度持仓减去上季度持仓得到变动值。判断哪些是新建仓、清仓、继续加仓或减仓。为什么用 outer join因为一个季度内会出现“新买入了上季度没有的股票”和“清仓了上季度持有的股票”inner join 会把这两种情况直接丢掉只有 outer join 能保留完整变化。5.2 计算持仓变动合并和计算函数如下def compare_quarters(cur: pd.DataFrame, prev: pd.DataFrame) - pd.DataFrame: cols [ nameOfIssuer, cusip, titleOfClass, value, sshPrnamt, sshPrnamtType, putCall, ] cur cur[cols].copy() prev prev[cols].copy() for df in (cur, prev): df[value] pd.to_numeric(df[value], errorscoerce) df[sshPrnamt] pd.to_numeric(df[sshPrnamt], errorscoerce) df[cusip] df[cusip].astype(str).str.strip() merged cur.merge( prev, oncusip, howouter, suffixes(_now, _prev), ) # 统一名称 merged[issuer] merged[nameOfIssuer_now].fillna(merged[nameOfIssuer_prev]) merged[title] merged[titleOfClass_now].fillna(merged[titleOfClass_prev]) # 空值补 0便于计算差值 value_now merged[value_now].fillna(0) value_prev merged[value_prev].fillna(0) shares_now merged[sshPrnamt_now].fillna(0) shares_prev merged[sshPrnamt_prev].fillna(0) merged[delta_value] value_now - value_prev merged[delta_shares] shares_now - shares_prev # 标记持仓状态 merged[position_type] 持有/调整 merged.loc[(value_prev 0) (value_now 0), position_type] 新建仓 merged.loc[(value_now 0) (value_prev 0), position_type] 清仓 # 按市值变动绝对值排序 merged[abs_delta_value] merged[delta_value].abs() merged merged.sort_values(abs_delta_value, ascendingFalse) return merged.reset_index(dropTrue)这个函数会输出一张完整的调仓明细表。delta_value为正代表市值增加为负代表市值减少。position_type可以直接告诉我们哪些是新买的、哪些是被清掉的。5.3 运行与结果说明假设我们已经把最近两个季度分别保存为cur.xml和prev.xml运行分析cur_df clean_13f(parse_13f(data/current.xml)) prev_df clean_13f(parse_13f(data/previous.xml)) result compare_quarters(cur_df, prev_df) print(result[[issuer, cusip, delta_value, delta_shares, position_type]].head(10))预期输出会是一个表格第一行是市值变动绝对值最大的证券。需要注意这个“变动”既包含真实买卖也包含价格波动引起的市值变化。如果想区分两者需要把sshPrnamt数量变化和value市值变化放在一起看数量增加说明真实加仓数量不变但市值增加则主要是价格上涨。6. 可视化调仓地图6.1 加仓与减仓 Top N 柱状图调仓分析最直观的图表是“加仓/减仓 Top N”的横向柱状图import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Arial Unicode MS, WenQuanYi Zen Hei] plt.rcParams[axes.unicode_minus] False def plot_top_changes(merged: pd.DataFrame, top_n: int 15): top merged.head(top_n)[[issuer, delta_value, position_type]].copy() colors [#d62728 if v 0 else #2ca02c for v in top[delta_value]] fig, ax plt.subplots(figsize(10, 8)) ax.barh(top[issuer], top[delta_value], colorcolors) ax.set_xlabel(市值变化千美元) ax.set_title(f13F 调仓 Top {top_n}) ax.invert_yaxis() return fig红色代表加仓绿色代表减仓。invert_yaxis()可以让变动最大的显示在最上方。6.2 四象限散点图真正的“调仓地图”标题里提到“调仓地图”更合适的可视化是散点图。横轴放持股数量变化纵轴放市值变化点的大小代表当前市值颜色代表加减仓方向。这样一张图可以同时表达三层信息def plot_adjust_map(merged: pd.DataFrame): fig, ax plt.subplots(figsize(12, 8)) x merged[delta_shares] / 1e4 # 股数转万股 y merged[delta_value] size (merged[value_now].fillna(0).abs() ** 0.5) / 20 colors [#d62728 if v 0 else #2ca02c for v in merged[delta_value]] ax.scatter(x, y, ssize, ccolors, alpha0.55) ax.axhline(0, colorgray, linestyle--, linewidth0.8) ax.axvline(0, colorgray, linestyle--, linewidth0.8) ax.set_xlabel(持股数量变化万股) ax.set_ylabel(市值变化千美元) ax.set_title(机构调仓地图四象限视图) return fig四个象限的含义非常清晰右上角数量增加市值增加属于明显加仓。左上角数量增加但市值下降通常是价格下跌导致也可能是加仓幅度没有抵消价格跌幅。右下角数量减少但市值上升一般是减仓后价格大涨。左下角数量减少市值下降属于明显减仓。这张图很适合用来快速定位“机构最剧烈变动的仓位”。6.3 保存图片生成图表之后直接保存到本地fig1 plot_top_changes(result, top_n15) fig1.savefig(output/top_changes.png, dpi150, bbox_inchestight) fig2 plot_adjust_map(result) fig2.savefig(output/adjust_map.png, dpi150, bbox_inchestight)bbox_inchestight可以避免横轴标签被截断。7. 常见问题与排查思路实操过程中最容易遇到的问题我整理成了一张排查表问题现象常见原因解决思路请求返回 403User-Agent 不规范或请求太频繁按“机构名 邮箱”格式设置 UA并控制请求间隔read_xml 报 ImportError缺少 lxml执行 pip install lxml解析结果是空表XPath 命名空间写错确认 namespaces 参数为 thirteenf/informationtable同一个 CUSIP 出现多行多家子机构分别提交结合 nameOfIssuer 或 filer 字段做聚合value 数值看起来偏小单位是千美元需要时乘以 1000 转成美元找不到 13F-HR 记录机构通过多个主体提交尝试查询母公司或子公司的 CIK主文件不是 XMLprimaryDocument 是完整提交文档去 accession 目录下找 InfoTable.xml7.1 403 问题403 是最高频的问题。绝大多数情况是 User-Agent 没有包含可联系信息或者单位时间内请求次数过多。建议把请求间隔设置成 0.1 到 0.2 秒分析几十家机构也不慢但不会触发限流。7.2 空表问题read_xml返回空 DataFrame优先检查命名空间。13F 信息表的命名空间是固定的不要自己去猜。如果不想处理命名空间也可以先读 XML 字符串把informationTable里的内容提取出来再用read_xml解析。7.3 数据重复问题同一只股票在表格里出现多次可能是同一个机构里有多个投资经理也可能是期权和正股同时存在。分析前最好确认是否需要按putCall过滤把普通股票和期权分开统计。8. 最佳实践与工程建议写到这里代码链路已经完整了。但作为一个长期维护的数据分析项目还有几个工程层面的建议值得注意。8.1 数据工程层面原始数据必须缓存每次运行都重新下载既浪费流量又容易触发限流。建议把下载的 XML 按“CIK 报告期”命名存到本地 data 目录只有不存在时才重新下载。解析结果落盘把清洗后的 DataFrame 保存为 Parquet 或 CSV后续做多机构对比时就不用重新解析 XML。写一个重试机制网络请求偶尔会失败可以用tenacity库或简单循环做指数退避重试。保留报告期字段每条记录都应该带上报告期否则多个季度数据叠在一起会混乱。8.2 合规与风险层面遵守 SEC EDGAR 的服务条款设置合法 User-Agent控制请求频率。13F 数据是滞后数据只能用于研究和验证思路不能当作实时买卖信号。本文所有代码与结论仅用于技术学习和数据分析演示不构成任何投资建议。真实投资决策需要结合更多信息源。8.3 分析与扩展层面可以按行业把 CUSIP 映射到 GICS 行业分类观察资金在 AI 芯片、云计算、软件应用之间的流向。可以同时拉取多家机构的 13F做机构之间的横向对比找出“多家机构同时加仓”的共识方向。如果想长期跟踪可以把这个脚本部署成定时任务每季度自动更新一次数据再生成调仓地图。9. 总结与拓展方向这篇教程完成了一条从原始数据到可视化图表的完整分析链路用 SEC EDGAR 接口获取 13F 文件用 pandas 解析 XML 信息表用 merge 计算季度持仓变化最后用 matplotlib 画出调仓地图。整个过程不依赖任何付费数据源纯 Python 就能实现。下一步可以继续深入的方向有两个。一是把单机构分析扩展成多机构聚合分析做机构资金流向的横向对比二是把 CUSIP 映射成行业标签这样就能回答“头部机构的钱到底在产业链哪个环节进出”这类更有分析价值的问题。如果你准备拿真实数据动手试一下可以先从一家你感兴趣的机构开始跑通完整流程后再逐步增加机构数量和季度数量。13F 数据本身是公开的分析代码也是可以复用的这种“公开数据 编程能力”的组合正是数据分析和量化研究最好的练习场。
返回列表