3. 数据准备与清洗:交易数据、头寸数据与市场数据
各位同学,咱们直接进入正题。
做压力测试,最怕什么?不是模型不够复杂,也不是场景不够极端。我最怕的是——数据是脏的。你想想看,辛辛苦苦搭了个漂亮的模型,跑出来结果一看,完全不符合常识。查了半天,发现是某个交易日期字段解析成了空值。这种坑,我踩过不止一次。
所以今天这一章,咱们就聊聊数据准备与清洗。说白了,就是把你手头的原始数据,变成能直接喂给压力测试模型的干净数据。
3.1 三类核心数据:交易、头寸、市场
做流动性风险压力测试,你需要三类数据。缺一不可。
- 交易数据:每一笔交易的明细。谁买的、谁卖的、什么产品、多少钱、什么时候到期。这是最细粒度的数据。
- 头寸数据:某个时间点上的持仓汇总。比如今天收盘后,你手里有多少国债、多少存单、多少回购。这是快照数据。
- 市场数据:利率、汇率、信用利差、波动率。这些是外部环境变量,用来驱动压力场景。
我个人习惯,先把这三类数据分别拉出来,各自做一遍清洗,然后再做关联校验。为什么?因为它们的来源不同,脏数据的模式也不同。
核心原则:交易数据要全,头寸数据要准,市场数据要稳。
3.2 交易数据清洗:那些年我踩过的坑
交易数据通常来自核心交易系统。字段很多,但真正关键的没几个:交易ID、产品代码、交易方向、名义本金、起息日、到期日、对手方。
我遇到过最典型的问题是什么?重复交易。有一次,某个交易员误操作,同一笔交易在系统里录了两次。如果不做去重,压力测试的现金流会直接翻倍,结果完全失真。
去重逻辑其实很简单:
# 按交易ID去重,保留最早的一条
df_trades = df_trades.drop_duplicates(subset='trade_id', keep='first')
但这里有个坑——有些系统的交易ID是自增的,重复交易会有不同的ID。这时候就得用更复杂的逻辑:按产品、金额、对手方、日期做组合去重。
注意:不要盲目去重。有些产品(比如利率互换)确实会有多笔相同金额的交易,那是正常的对冲操作。去重前一定要跟业务确认。
还有一个常见问题:日期格式不统一。有的系统用YYYY-MM-DD,有的用YYYY/MM/DD,还有的用MM/DD/YYYY。我建议统一转成datetime类型,方便后续计算剩余期限。
# 统一日期格式
df_trades['maturity_date'] = pd.to_datetime(df_trades['maturity_date'], errors='coerce')
嗯,这里要注意。errors='coerce'这个参数,会把无法解析的日期变成NaT。跑完之后一定要检查NaT的数量,如果太多,说明数据源有问题。
3.3 头寸数据清洗:快照数据的陷阱
头寸数据是某个时间点的快照。比如每天收盘后的持仓汇总。
头寸数据最大的问题是什么?数据滞后。我记得有一次做压力测试,用的头寸数据是T-1日的,但当天市场发生了剧烈波动,头寸已经变了。结果压力测试的结果跟实际情况差了十万八千里。
所以,我建议:
- 明确头寸数据的截止时间点(比如每天下午5点)
- 检查头寸汇总是否与总账一致(这叫勾稽关系校验)
- 对于衍生品头寸,注意名义本金与市值的区别
头寸数据还有一个容易忽略的点:空头头寸。有些产品允许做空,头寸是负的。如果你的清洗逻辑把负数当成异常值删掉了,那压力测试的结果就完全不对了。
小技巧:头寸数据清洗后,做一个简单的汇总校验。把所有头寸的名义本金加起来,跟资产负债表上的总资产/总负债对一下。对不上?那就得回去查数据源了。
3.4 市场数据清洗:时间序列的脏活
市场数据是时间序列数据。利率、汇率、信用利差、波动率,这些数据通常来自外部数据供应商(比如Bloomberg、Reuters)。
市场数据清洗,我个人觉得最头疼的是缺失值。节假日没有数据,周末没有数据,有些小众产品的数据甚至断断续续。
处理缺失值,我一般分三步:
- 识别缺失模式:是随机缺失,还是整段缺失?
- 填充方法:短期缺失用前向填充(ffill),长期缺失用插值或模型预测
- 标记填充数据:填充过的数据要打上标记,方便后续分析
# 前向填充短期缺失
df_market['rate'] = df_market['rate'].fillna(method='ffill', limit=5)
# 标记填充过的数据
df_market['is_filled'] = df_market['rate'].isna().astype(int)
还有一个问题:异常值。市场数据偶尔会出现极端值,比如利率突然跳到100%。这可能是数据录入错误,也可能是真实的极端事件。怎么区分?我一般用滚动窗口的Z-score方法:
# 滚动窗口Z-score检测异常值
window = 20
mean = df_market['rate'].rolling(window=window).mean()
std = df_market['rate'].rolling(window=window).std()
z_score = (df_market['rate'] - mean) / std
outliers = z_score.abs() > 3
但注意,压力测试本身就是研究极端事件的。如果直接把异常值删掉,可能会丢失重要的尾部风险信息。我建议:异常值先标记,不急着删。等压力场景设计时再决定是否保留。
3.5 数据关联与一致性校验
三类数据都清洗完了,最后一步是关联校验。说白了,就是检查它们之间能不能对得上。
举个例子:交易数据里有一笔1000万的国债买入,那头寸数据里应该能看到对应的国债持仓增加。市场数据里应该有对应的国债收益率曲线。
如果对不上,问题可能出在:
- 交易数据的时间戳和头寸数据的快照时间不一致
- 产品代码在不同系统里不统一(比如交易系统用ISIN,头寸系统用内部代码)
- 市场数据的报价时间与交易时间不匹配
我曾经遇到过一个案例:交易数据和头寸数据差了整整一天,原因是两个系统的结算周期不同。查了三天才找到原因。从那以后,我每次做数据清洗,都会先画一张数据流图,把每个数据源的时间戳、更新频率、业务含义都标清楚。
一句话总结:数据清洗不是一次性工作,而是一个迭代过程。每发现一个新问题,就加一条校验规则。慢慢地,你的数据质量就会越来越高。
3.6 本章知识体系图
下面这张图,是我自己总结的数据准备与清洗的完整流程。你可以把它当成一个检查清单,每次做压力测试前都过一遍。
这张图把整个流程串起来了。从左到右是三个数据源,中间是各自的清洗步骤,最后汇聚到关联校验。你每次做数据准备,都可以拿这张图对照着来,不容易漏掉步骤。
我的建议:把这张图打印出来贴在工位上。数据清洗是个细致活,容易忘步骤。有张图在眼前,心里踏实。
好了,数据准备与清洗就讲到这里。记住一句话:干净的数据是压力测试的基石。数据没准备好,后面的一切都是空中楼阁。
公众号:蓝海资料掘金营,微信deep3321