做债券数据活的人,应该都有过这段经历:月初拿到一张几百行的持债清单,要求补全中债估值收益率、修正久期、票面利率、待偿期限,还得按主体评级筛一遍。最早我靠Wind终端一只一只点开,复制粘贴到Excel,做完差不多小半天,眼睛都是花的。后来把Wind给的另外两条路摸熟了——Excel插件和Python的WindPy接口——同一张表压缩到五分钟。这篇文章没有官方文档那么死板,就是我在债券分析里被这些工具救过、也坑过之后,整理出来的完整玩法。
不管你是债券交易员、信评、风控还是量化开发,只要日常工作离不开Wind数据和Excel,这篇文章都值得收藏。我会先把Excel插件端的函数用法讲透,再一步步把Python交互跑通,最后给出债券场景下的完整脚本和踩坑记录。
1. 债券工作里的两个“Wind入口”:Excel插件和Python各管哪一段
1.1 Excel插件解决“轻量查询”,Python解决“批量与自动化”
很多刚接触Wind的人有个误区,以为Excel插件和Python接口只能选一个。实际这两套东西在债券工作流里分工完全是互补的。
Excel插件最大的优势是“所见即所得”。你在单元格里敲一行=WSS("210205.IB","中债估值收益率",""),马上看到结果,鼠标下拉还能批量填充。对于临时查一两只券、核对一个数、做张给领导看的交互报表,这个效率是Python比不了的。它适合的场景是:量不大、逻辑不复杂、需要人工干预和确认的数据操作。
Python接口(WindPy)则适合重活累活。几百只债券一起取数,Excel公式往下拖的时候每一格都在发请求,文件卡得鼠标都挪不动;这时候用Python一次w.wss调用把几百个代码的数据全部拉下来,转成DataFrame清洗、筛选、计算,再回填Excel,速度和稳定性完全不在一个量级。
债券分析和股票不太一样,你可能需要每天跟估值报表、收益率曲线、利差数据打交道,这些要么是批量截面,要么是长时间序列,天生适合脚本处理。所以我给你的第一个建议:查数、核数用Excel插件;跑表、建模型用Python,两条腿走路。
1.2 同一个Wind账号,数据口径是否一致
实务中大家最担心的问题就是:Excel插件里取出来的数和Python取出来的是不是一样?我的经验是,底层数据源同一套,绝大多数情况下口径一致。但有一个隐藏风险:某些字段在Excel端有默认参数,在Python端可能也需要显式传参数值,你不传的时候可能会得到不太一样的值。
举个例子,收益率类字段经常涉及“精度”“计算方式”“是否含税”等参数;评级字段可能涉及“债项评级”和“主体评级”。如果你在Excel里用的字段名,到Python里原封不动照搬,有时候会踩到参数默认值的坑。
所以我做关键报表时有一个固定习惯:重要数据先用终端手工核一次,再分别用Excel插件和Python各取一次,三者对上之后,再把这个字段名单固化在脚本里。不要盲目信任某个字段名,Wind的指标显示名在不同版本里偶尔会调整,脚里最好加一步数据校验。
2. 先用Excel插件把估值表拉直:债券场景最常用的W函数
2.1 WSS、WSD、WDS、WSQ选哪个
Wind Excel插件对外暴露了一组W开头的函数。很多新手拿到就乱用,拉出来的数据张冠李戴。其实选函数很简单,核心就是判断你要的是“截面”还是“时序”。
| 函数 | 用途 | 返回形态 | 债券场景 |
|---|---|---|---|
| WSS | 多个代码的某一时点指标 | 一行一个代码 | 取最新估值收益率、久期、评级 |
| WSD | 单代码的历史时间序列 | 一行一个日期 | 取某只债或某条曲线的历史收益率 |
| WDS | 多代码在历史某一天的截面数据 | 一行一个代码 | 取历史估值日所有券的估值 |
| WSQ | 实时行情快照 | 一行一个代码 | 银行间/交易所行情盯盘 |
| WST | 日内分钟/ tick数据 | 一行一个时间点 | 基本不用于债券日常 |
债券分析里最常碰到的还是WSS和WSD。比如我要知道“210205.IB今天的中债估值收益率和修正久期”,用WSS;我要看“过去一年这只券的估值收益率走势”,用WSD。WDS常用于做历史截面回看,比如“6月30日这天这批债券分别估值多少”。
2.2 一个债券估值模板的搭建过程
假设你手里有一批债券代码,要在Excel里快速建一张估值表。打开Wind插件后,可以先在“公式生成器”里搜索字段名,把标准字段名复制出来,避免手打错误。然后在单元格里这样写:
A2: 210205.IB B2: =WSS(A2,"中债估值收益率","") C2: =WSS(A2,"中债估值修正久期","") D2: =WSS(A2,"票面利率","") E2: =WSS(A2,"待偿期限","")如果有多只券,就把A列往下拉成代码列表,B到E列的公式一并下拉填充。这里有个实操细节:下拉之前一定要把公式里的代码引用改成绝对引用,比如=WSS($A2,"中债估值收益率",""),或者直接用Excel表格对象(Table)的结构化引用,否则下拉后行号会错位,轻则取错券,重则整列都是#N/A。
我建模板的时候,还会顺手在最上面加一行“数据日期”,然后用WDS来做历史切换。只要改日期,整张表自动回到那一天,这个在月底回溯、历史估值对比时特别好用。
2.3 Excel插件的两个“反直觉”坑
第一个坑:WSS返回的很多数值在Excel里会被当成文本。你用SUM、AVERAGE统计时会发现结果不对,单元格左上角可能有个绿色三角。处理办法是给公式包一层数学运算,比如=WSS($A2,"中债估值修正久期","")*1,或者在外面套VALUE()。不建议只靠“分列”功能去改,因为公式刷新后又会变回文本。
第二个坑:整张表全是WSS公式时,只要改一个无关单元格,Excel都可能在后台重新请求一遍Wind数据,文件会越用越卡。更麻烦的是,如果你把工作簿发给没有Wind终端的人,对方打开后单元格全变成#NAME?或#VALUE!。所以给外部人员发报表之前,一定要把含公式的区域复制粘贴成数值。如果是自己日常用,建议加一个“刷新开关”:让公式前面用一个单元格布尔值控制,比如=IF($G$1,WSS($A2,"中债估值收益率",""),—),只有需要刷新时才打开开关,否则填—占位。这招能让大表格流畅很多。
3. Python接管数据:WindPy的安装、连接与三板斧
3.1 安装WindPy并完成第一次连接
WindPy是Wind官方提供的Python接口,不是第三方爬虫,数据合法性和稳定性都有保障。安装方式现在很省事,打开Wind终端,在菜单里找到“帮助”或“API接口”区域,下载对应Python版本的安装包,或者直接在终端里搜索“WindPy”,按提示pip安装。安装完成后,代码里这样启动:
from WindPy import w import pandas as pd w.start() print(w.isconnected())如果输出True,说明已经连上终端了。有几个前提条件:
- 必须是Windows系统,且本机装了Wind终端。
- 连接前Wind终端要保持登录状态,不一定要有行情界面,但后台进程得活着。
- Python版本最好在Wind官方支持矩阵内,别一上来装最新版Python,有的版本WindPy还没适配。
第一次跑通我用了五分钟,大部分时间都花在确认Python版本和Wind终端版本是不是匹配上。如果你import WindPy就报错,先去检查这个版本匹配关系,比反复重装库有效得多。
3.2 wsd、wss、wsq返回对象的解析套路
WindPy里最常用的三个接口,对应Excel端的WSS、WSD、WSQ:
w.wss(codes, fields, options):取截面,多代码多字段一次返回。w.wsd(code, field, beginTime, endTime, options):取单代码单字段的历史序列。w.wsq(codes, fields, options):取实时行情。
每个接口返回的都是一个对象,关键属性就这几个:ErrorCode(0表示成功)、Codes(代码列表)、Fields(字段列表)、Times(日期列表)、Data(数据矩阵)。新手最容易犯的错是拿到返回对象后,盯着它看半天不知道下一步怎么处理。其实把它转成pandas结构就行了。
from WindPy import w import pandas as pd w.start() # 截面数据:多只债券、多个字段 codes = ["210205.IB", "220001.IB", "019742.SH"] fields = "中债估值收益率,中债估值修正久期,票面利率,待偿期限" res = w.wss(codes, fields, "tradeDate=20240920") if res.ErrorCode == 0: df = pd.DataFrame(res.Data, index=res.Codes, columns=res.Fields) print(df) else: print("ErrorCode:", res.ErrorCode, res.Data)这段代码是债券估值表最核心的骨架。res.Data是一个二维列表,行为代码、列为字段,所以直接用它构造DataFrame,索引设成债券代码,列名设成字段名就行。
历史序列的转法也很固定:
res = w.wsd("210205.IB", "中债估值收益率", "2024-01-01", "2024-12-31", "fill=Previous") if res.ErrorCode == 0: s = pd.Series(res.Data[0], index=pd.to_datetime(res.Times), name=res.Fields[0]) print(s.head())fill=Previous这个参数在债券历史序列里很实用,因为非交易日太多,不填的话序列会有大量空值,画图不好看,计算也会出问题。
3.3 把Wind数据转成Pandas DataFrame的两个实用函数
为了避免每次写代码都重复处理返回对象,我把这几个接口封装成了小函数,放在本地工具模块里。你完全可以直接抄走用:
from WindPy import w import pandas as pd w.start() def wss_to_df(codes, fields, options=""): """多代码、多字段截面数据,返回DataFrame""" res = w.wss(codes, fields, options) if res.ErrorCode != 0: raise RuntimeError(f"Wind error: {res.ErrorCode}") return pd.DataFrame(res.Data, index=res.Codes, columns=res.Fields) def wsd_to_series(code, field, start, end, options=""): """单代码、单字段历史序列,返回Series""" res = w.wsd(code, field, start, end, options) if res.ErrorCode != 0: raise RuntimeError(f"Wind error: {res.ErrorCode}") return pd.Series(res.Data[0], index=pd.to_datetime(res.Times), name=res.Fields[0]) def wsd_to_df(codes, field, start, end, options=""): """多代码、单字段历史序列,返回宽表DataFrame,行为日期,列为代码""" res = w.wsd(codes, field, start, end, options) if res.ErrorCode != 0: raise RuntimeError(f"Wind error: {res.ErrorCode}") df = pd.DataFrame(res.Data).T df.index = pd.to_datetime(res.Times) df.columns = res.Codes return df注意w.wsd其实也支持一次传多个代码,返回的res.Data里每个元素是对应一只券的序列。wsd_to_df就是利用这个特性,把多只券的同字段历史数据拼成宽表,做利差分析和曲线走势对比很顺手。
4. 债券实战:批量拉取估值指标并回填Excel的完整脚本
4.1 案例需求:筛选“久期在2到4年、主体评级较好”的信用债
光讲函数太抽象,我拿一个真实场景串一遍。假设你手上有一堆信用债代码,领导想要一张表:只要待偿期限在2到4年、中债估值修正久期在2到4年、主体评级在AA+及以上的券,同时把估值收益率、净价、票面利率全列出来。
放到以前,我会在Excel里拉几百行WSS公式,然后手动筛。现在Python几十行搞定。顺便说一句,这个选券逻辑看着简单,却是组合免疫、久期匹配这些策略里最常做的基础动作。
4.2 脚本分步拆解:代码清单、参数说明、输出效果
from WindPy import w import pandas as pd w.start() codes = [ "210205.IB", "220001.IB", "220005.IB", "019742.SH", "143295.SH", "102280023.IB", "102280156.IB", ] fields = "中债估值收益率,中债估值修正久期,中债估值净价,票面利率,待偿期限,主体评级" res = w.wss(codes, fields, "tradeDate=20240920") if res.ErrorCode != 0: print("取数失败,ErrorCode:", res.ErrorCode) else: df = pd.DataFrame(res.Data, index=res.Codes, columns=res.Fields) print("原始数据:") print(df) # 数值型字段转成数字,避免后面比较时报错 for col in ["中债估值收益率", "中债估值修正久期", "中债估值净价", "票面利率", "待偿期限"]: df[col] = pd.to_numeric(df[col], errors="coerce") # 评级字段先看看实际返回了什么,别急着筛 print("评级字段枚举值:", df["主体评级"].unique()) rating_mask = df["主体评级"].astype(str).str.contains("AA\\+", regex=True, na=False) maturity_mask = (df["待偿期限"] >= 2) & (df["待偿期限"] <= 4) duration_mask = (df["中债估值修正久期"] >= 2) & (df["中债估值修正久期"] <= 4) result = df[rating_mask & maturity_mask & duration_mask] print("筛选结果:") print(result)有几处需要特别说明:
代码后缀不能省。债券在Wind里的代码体系比股票复杂,银行间用
.IB,上交所用.SH,深交所用.SZ。同一个券在不同市场可能对应不同代码,写错后缀取不到数或者取到另一只券。pd.to_numeric(errors="coerce")是必须的。Wind返回的有些数值列可能是对象类型,直接和数字比较会报错,强制转成数字后,转不了的会自动变成NaN,后面再做缺失处理。主体评级字段我习惯先打印
unique()看一次。因为现实中可能是“AAA”“AA+”“AA+ 中债”,也可能带了空格或后缀。用str.contains("AA\\+", regex=True)匹配能避开“AAA被AA误匹配”的问题,因为正则里的\\+表示匹配加号本身,不会把AAA匹配进去。
4.3 用Python拼接Excel报告:把结果回填到模板表
数据算完,最自然的需求是落成Excel报表。如果你只要一个简单的结果文件,pandas一行就行:
result.to_excel("bond_screen_result.xlsx", sheet_name="筛选结果")但实际工作中,领导可能已经给了你一个固定格式的Excel模板,里面有表头、配色、批注。这时候别让pandas直接生成新文件,更好的做法是用openpyxl读取模板,把结果填进去。
from openpyxl import load_workbook wb = load_workbook("模板.xlsx") ws = wb["估值表"] # 从模板第2行开始写,1表头 for i, (code, row) in enumerate(result.iterrows(), start=2): ws.cell(row=i, column=1, value=code) ws.cell(row=i, column=2, value=row["中债估值收益率"]) ws.cell(row=i, column=3, value=row["中债估值修正久期"]) ws.cell(row=i, column=4, value=row["中债估值净价"]) ws.cell(row=i, column=5, value=row["票面利率"]) ws.cell(row=i, column=6, value=row["待偿期限"]) wb.save("估值表_已更新.xlsx")这样保留模板格式,只更新业务数据,领导收到后也挑不出毛病。如果你做的是一份周报,建议把这段代码放进一个main()函数,每周五下午跑一次,自动生成带日期的文件名,比如bond_report_20240920.xlsx,省得手动改名。
5. 高频踩坑记录:从返回码到缺失值,从性能到权限
5.1 先检查ErrorCode,再谈数据准确性
WindPy里所有接口的返回对象都有一个ErrorCode。我见过很多同事写脚本时不看这个值,直接拿res.Data去做计算,结果数据是空的或错的,找了半天才发现是权限或参数问题。我的习惯是:每一个接口调用后,第一时间判断ErrorCode != 0就抛异常或记录日志。
负数错误码的排查顺序也有讲究:先看w.isconnected()是不是True,不是就重启连接;再看债券代码后缀是否正确,比如.IB写成了.SH;然后看字段名是否被Wind改名了;最后才怀疑是权限不足。按这个方法排查,90%的问题能在两分钟内定位。
5.2 缺失值、停牌、NaN:债券数据和股票数据不太一样
股票数据有停牌、涨跌停、不交易的日子,缺失值特别多。债券虽然也有非交易日,但估值数据通常是中债登或中证估值机构提供的,相对连续。不过这不代表不会出现NaN,比如新债还没上市、老债已经摘牌、某只券当天没有估值,Wind返回的值可能是None、nan或者空字符串。
我的处理原则是:数值型字段统一走pd.to_numeric(errors="coerce");字符串型字段缺失时用fillna("");筛选逻辑里有NaN的券直接剔除,绝不拿NaN去比大小。另外一个细节:如果是从wsd拿历史收益率序列,缺数据的非交易日不要用dropna直接删,会让时间轴变得支离破碎,建议用fill=Previous参数,或者先把时间序列重采样到标准交易日历上。
5.3 批量请求性能优化:一次API调用代替几百次循环
新手最容易写出的低效代码,是在循环里一次次调wss:
# 千万别这么写 for code in codes: res = w.wss(code, "中债估值收益率", "") # ...几百只债券就发几百次请求,速度慢不说,还容易触发Wind终端的风控或超时。正确做法是把所有代码一次性传给w.wss,一次调用出全部结果。如果你确实要分段拉取(比如代码量太大),也尽量控制在每批200到300只,批与批之间sleep(0.5),给终端一点喘息时间。
我实测下来,几百只债券的截面数据,一次调用几秒钟就能返回,而循环方式可能要好几分钟,差距非常明显。
5.4 权限与登录态:脚本半夜跑挂了的真实案例
之前帮同事搭过一个定时任务,每天晚上十一点自动拉第二天的估值数据,方便早上开盘前看。脚本逻辑没问题,但连着跑了两天都挂。排查发现,问题出在Wind终端到了晚上自动进入锁屏状态,或者被安全策略踢出了登录态,导致WindPy连不上。
这个坑很隐蔽,因为人坐在工位上时终端是正常登录的,压根想不到后台进程会掉线。后来我在脚本开头加了两道保险:第一步调用w.start()后立即检查w.isconnected(),连不上就直接发邮件告警;第二步在拉数失败时重试三次,仍然失败就保留上次数据文件,并标记数据日期为“前值”。另外,给跑定时任务的那台电脑设置好电源计划,别让系统自动休眠,终端进程也要在任务计划里做“保持运行”的配置。
还有一类权限问题:某些字段需要特定模块权限,比如中债估值全价、银行间质押式回购利率等。用普通账号拉这些字段,返回码会直接报权限类错误。处理办法是找负责Wind账号管理的同事申请对应权限,别自己折腾。
6. 我的工作流建议:哪种场景该用Excel,哪种该切Python
6.1 一张表说清楚分工
| 使用场景 | 推荐工具 | 原因 |
|---|---|---|
| 临时查一只债的估值和久期 | Excel插件WSS | 快、直观,改个代码立刻刷新 |
| 做几十只以内的小范围分析 | Excel插件 | 公式+透视表交互方便 |
| 周报/月报,几百只债券批量估值 | Python WindPy | 一次拉取、清洗、回填,稳定高效 |
| 构建收益率曲线、利差时序 | Python wsd | pandas处理时间序列最顺手 |
| 给领导演示的交互式报表 | Excel插件+透视表 | 非技术同事也能自己筛选 |
结论很简单:凡是会重复执行的、数据量大的、要拼接多张表的,都往Python迁移;凡是临时的、交互的、需要人反复看的,留在Excel里。
6.2 从Excel迁移到Python的最小成本路径
很多债券研究员不是不会Python,而是担心迁移过程太折腾。我建议不要想着一周内把所有报表全切过去,而是找一个频率最高、最耗时的表先试点。比如你现在每周都要做“信用债估值跟踪表”,那就把它的取数逻辑抽出来,用Python跑通,输出结果和原来的Excel手动表对比两周,确认无误后再替换。替换时也保留原来的Excel模板,只把数据源从公式换成Python回填的数值。
这样迁移的好处是,业务侧的人看到的还是同一个文件,只是背后的数据生产流程变了。等你在一个场景上积累了信心,再慢慢把其他重复性报表迁过来,压力会小很多。
6.3 一点个人经验:工具是手段,不是目的
最后说点真话。工具再顺,也只是把脏活累活自动化,并不意味着数据直接可用。我在实际项目里踩过更隐蔽的坑:Excel模板里公式套公式,某天字段改名导致整列偏差;Python脚本因为评级字段返回了一种我没见过的新写法,筛选条件悄悄出了问题。所以我现在养成了两个习惯,分享给你。
一是每版数据出来,抽三到五只券和Wind终端人工核对一遍,尤其是第一次使用新字段名的时候。二是把取数日期、字段版本、筛选条件都记录在最终报表的备注里,这样过两个月回看,还能知道当时这张表是怎么算出来的。
工具的组合拳打好了,能帮你省出大量时间;但省出来的时间不该只用来做更多表,而是用来理解数字背后业务在发生什么。毕竟在债券这个市场里,数据和风险的距离,往往比你想象的要近。