简介:面向债券市场风险管理与量化研究者,这套221页的PDF资料以DeepSeek-R1为技术主线,系统讲解基于市场深度指标计算的流动性风险评估与危机早期预警模型构建。内容覆盖Tick级行情数据采集预处理、订单簿结构化存储与索引设计、盘口深度与深度斜率算法、流动性价差动态加权、大单冲击下深度恢复能力指标,以及LSTM/Transformer和注意力机制在风险因子识别中的应用,包含从数据工程、特征工程到模型选型的完整链条。压缩包为单个PDF文件,大小约11.48MB,支持目录章节跳转、书签大纲定位和章节快速查阅,文字图表完整无异常。目前已有68人浏览学习。文档以50个大章节组织,数学推导与实现逻辑并重,适合需要落地债券流动性风险量化方案的机构研究员、量化分析师和Python开发者参考。
1. 债券流动性风险为什么必须看市场深度:光看成交量和价差远远不够
2022年四季度的信用债调整行情里,我见过最典型的场景:一只AA+级城投债的成交价连续一周没有大幅波动,但风险管理部挂出5000万面值的减持单后,连续三天才出完,每次一挂单就把报价砸出三五个BP。价格稳定不代表流动性好,这个浅显的道理在债券市场几乎每个月都要重新验证一遍。市场深度指标计算,正是把「一只债券能承受多大的卖单而不砸穿价格」量化出来的核心手段,也是证券债券流动性风险评估方案的地基。
DeepSeek在这套方案里的角色经常被误解:它不是用来直接预测违约的模型,而是贯穿「市场深度指标计算 → 特征工程 → 模型构建」这条链路的生产力工具。用自然语言描述特征逻辑、让DeepSeek生成pandas代码、再人工复核合入,比纯手写快一个数量级。这份221页的方案,厚度本身说明问题:流动性风险项目真正难的不是训练模型,而是把每一个深度指标的口径敲定、跑通、留下验证记录。
这篇笔记写给固收风险分析师、量化开发和应用运维。目标是让你看完之后,能自己动手把市场深度指标算出来、把预警阈值立起来、把评分模型跑起来,顺便避开我在生产环境里踩过的坑。新手可以直接抄代码,熟手可以参考参数边界和排错思路。
2. 市场深度指标计算:从报价快照到五个核心指标
2.1 为什么债券必须用订单簿深度,而不是成交量做刻度
股票市场看流动性,第一反应是换手率和成交量,因为股票参与者足够多、交易足够连续。债券完全不是这个逻辑:交易所债券虽然有连续竞价盘口,但大量信用债一天只成交三五笔;银行间债券更是做市商报价驱动,很多券的成交回报稀疏得没法做统计。成交量天然不连续,拿日成交额去算标准差,结果忽高忽低,根本没法用。
市场深度指标计算的核心是回答三个问题:当前挂单量能吃下多大的卖单不砸穿价格(深度);买卖双方最优报价差多远(价差宽度);深度被消耗之后多久能恢复(弹性)。这三个维度合在一起,才能描述「能不能卖、以什么成本卖」。
具体到落地,我一般用下面这几个指标做基础池:
| 指标 | 计算口径 | 风险含义 |
|---|---|---|
| Quoted Spread | (卖一价-买一价)/中间价,转BP | 价差越宽,交易成本越高 |
| Top-N Depth | 买/卖前N档挂单量之和(万元面值) | 深度越小,承接大单能力越弱 |
| Order Book Imbalance | (买深-卖深)/(买深+卖深) | 偏离0越远,单向压单越明显 |
| Amihud非流动性 | 收益率绝对值/成交金额 | 单位成交额引发的价格波动 |
| 深度衰减率 | 当前深度/过去K个快照深度中位数 | 深度快速萎缩是危机前兆 |
提示:做市商报价和真实成交深度经常不一致,落地时建议同时保留「报价深度」和「成交深度」两套口径,预警触发时对比两套口径的差异,能过滤掉不少假信号。
2.2 用Python从订单簿快照计算双边深度和价格冲击
先贴最核心的函数。交易所债券Level-2快照给的是逐档委托数据,下面这段代码是单帧快照的深度计算逻辑:
import pandas as pd import numpy as np def compute_depth_metrics(snapshot_df: pd.DataFrame, levels: int = 5) -> dict: """ 从一帧订单簿快照计算市场深度指标。 输入字段约定: side : 'bid' 表示买方挂单,'ask' 表示卖方挂单 price : 申报价,单位:元 volume : 挂单量,单位:万元面值 ts : 快照时间戳 返回指标: spread_bp : 最优买卖价差(BP) bid_depth : 买方前N档累计挂单量(万元) ask_depth : 卖方前N档累计挂单量(万元) depth_imbalance : 订单簿不平衡度,取值[-1, 1] conservative_depth : 双边深度取小值,偏保守口径 """ bid = snapshot_df[snapshot_df['side'] == 'bid'].nlargest(levels, 'price') ask = snapshot_df[snapshot_df['side'] == 'ask'].nsmallest(levels, 'price') if bid.empty or ask.empty: return None # 单边挂单缺失时不计算,避免指标失真 best_bid = bid['price'].iloc[0] best_ask = ask['price'].iloc[0] mid_price = (best_bid + best_ask) / 2.0 spread_bp = (best_ask - best_bid) / mid_price * 10000 # BP bid_depth = bid['volume'].sum() ask_depth = ask['volume'].sum() total_depth = bid_depth + ask_depth + 1e-9 depth_imbalance = (bid_depth - ask_depth) / total_depth return { 'mid': mid_price, 'spread_bp': spread_bp, 'bid_depth': bid_depth, 'ask_depth': ask_depth, 'depth_imbalance': depth_imbalance, 'conservative_depth': min(bid_depth, ask_depth) }这里几个参数的用意说明一下。levels=5是取前五档深度,对交易所信用债来说,第五档之外基本没有连续挂单量,取到十档只会把空档期算成零值、制造虚假的深度衰减信号。nlargest(levels, 'price')取买盘最高五个价位,nsmallest(levels, 'price')取卖盘最低五个价位,这两个方向不能反,反了算出来的价差就是负数。单边挂单缺失时返回None而不是填零,是为了在聚合层做前向填充,而不是伪造一个深度为零的假快照。
然后是时序聚合。快照是逐笔推送的,但预警模型要的是稳定刻度,所以要按固定频率聚合:
def aggregate_depth_snapshots(snapshot_df: pd.DataFrame, freq: str = '15min') -> pd.DataFrame: """ 把逐笔快照按固定频率聚合为时序指标。 为什么用中位数而不是均值: 债券盘口经常出现瞬时大额挂单然后撤单,均值会被单帧异常值拉高, 中位数对做市商的试探性挂单更稳健。 """ snapshot_df = snapshot_df.sort_values('ts').set_index('ts') row_list = [] for ts, frame in snapshot_df.groupby(pd.Grouper(freq=freq)): metrics = compute_depth_metrics(frame) if metrics is not None: metrics['ts'] = ts row_list.append(metrics) result = pd.DataFrame(row_list).set_index('ts') # 空档期前向填充:债券盘口稀疏,空档不代表流动性归零 result = result.resample(freq).ffill().dropna(subset=['mid']) # 深度衰减率:当前保守深度 / 过去5根K线的中位数 result['depth_decay'] = ( result['conservative_depth'] / result['conservative_depth'].rolling(5, min_periods=1).median() ) return resultresample(freq).ffill()是这里的关键取巧:债券盘口经常在某个15分钟窗口里一单都没有,如果直接填零,深度衰减率会瞬间掉到接近于零,制造大量假预警。前向填充意味着「没有成交或挂单变化,就认为上一帧的流动性状态还在延续」,这符合做市商市场的实际机制。depth_decay里的rolling(5)是指5根15分钟K线(75分钟),不是5分钟,命名和文档里要写清楚,否则团队其他成员很容易读成5分钟衰减率。
2.3 指标参数怎么调:档位、窗口和异常值处理
参数这块我踩过不少坑,直接给结论。freq建议活跃券用5分钟、非活跃券用15到30分钟、本来就没成交的券用1小时,但同时要在特征里加一个num_quotes列,记录每个窗口内实际有挂单的快照数,模型可以用它来区分「流动性差」和「流动性未知」——这两个状态在业务上含义完全不同。
异常值处理统一走99%分位截尾(Winsorize),不要在聚合前用3σ剔除。债券盘口的天量挂单经常是单笔报错或者机构对倒,3σ会把这种异常当成信息保留下来,而99%分位截尾只砍极端、保留尾部形状。价差序列同理,报价瞬间拉出几百BP的情况在真实数据里很常见,不截尾的话,滚动分位数阈值会被个别极端值顶上去,预警信号整段失效。
最后提醒一句单位。债券报价用「元」、挂单量用「万元面值」、价差用「BP」,三个单位必须在进库之前统一好。我见过不止一个项目因为成交额用了「亿元」、深度用了「万元」导致量纲差一万倍,模型训练时某个特征系数被撑到天文数字,评分卡完全没法解释。
3. 流动性危机早期预警:把深度衰减变成可执行的警报
3.1 预警阈值设计:滚动分位数和双条件触发
指标算出来只是第一步,怎么从指标到警报才是方案的核心。我一般不做单指标触发,原因是债券市场噪音大:某只券一天没成交,深度衰减率自然难看,但这不代表马上要出流动性危机。单指标触发的误报率在真实盘面上能到60%以上,运维团队很快会对警报脱敏——这是做风控最怕的结局,警报喊多了,真出事的时候反而没人看。
推荐做法是滚动分位数加双条件触发:
def build_liquidity_warning(df: pd.DataFrame, lookback: int = 252, q_low: float = 0.05) -> pd.DataFrame: """ 用滚动分位数构造预警阈值。 lookback=252 表示用过去252个交易日(约一年)的分位数做基准。 q_low=0.05 表示深度指标跌破过去一年5%分位时进入预警区。 价差是反向指标,取高分位阈值(1 - q_low)。 触发逻辑: 保守深度 < 5%分位阈值,且价差 > 95%分位阈值,两个条件同时满足才告警。 """ out = pd.DataFrame(index=df.index) out['bid_depth_th'] = df['bid_depth'].rolling(lookback, min_periods=60).quantile(q_low) out['ask_depth_th'] = df['ask_depth'].rolling(lookback, min_periods=60).quantile(q_low) out['depth_decay_th'] = df['depth_decay'].rolling(lookback, min_periods=60).quantile(q_low) out['spread_th'] = df['spread_bp'].rolling(lookback, min_periods=60).quantile(1 - q_low) out['alert'] = ( (df['conservative_depth'] < out['bid_depth_th']) & (df['spread_bp'] > out['spread_th']) ).astype(int) return out说明几个参数。min_periods=60的意思是滚动窗口里至少要有60个有效观测值才计算分位数,刚上市的新券和历史数据不足60天的券,预警直接不启用,避免用一段残缺历史算出离谱的阈值。lookback=252取一年交易日,匹配债券市场的季节性——年底流动性普遍收紧、季末资金面波动,一年窗口能把周期性拉平。
为什么不用固定阈值?我在2021年做过一次回测,用当年上半年的固定深度阈值去跑下半年,误报率翻了一倍。原因是债券市场流动性处在牛熊切换中:牛市深度普遍充裕、价差普遍偏窄,固定阈值在牛市里几乎不触发,一到熊市就全线告警。滚动分位数是跟随市场的自适应刻度,虽然有一定滞后,但至少不会让整套阈值在三个月后集体失效。
3.2 DeepSeek在预警链路里的两个用途:特征代码生成和规则复核
DeepSeek在这条链路里到底怎么用?我看到很多团队把它当对话机器人,问「流动性风险怎么建模」,得到的答案只能用来科普,不能落地。我的用法很具体,是两个能验收、能上线的环节。
第一个用途是特征代码生成。我把特征需求写成自然语言规格,比如「计算过去20个快照的深度中位数衰减率,空档期前向填充,输出序列」,然后让DeepSeek生成pandas函数。省掉的是从想法到代码的翻译时间,不是代码审查。生成之后我做三件事:跑单测验证输入输出形状,拿一段已知答案的样本数据对结果,最后逐行检查有没有用到未来数据。DeepSeek生成的代码最常见的毛病就是无意识引入shift(-k)或者rolling(k).mean()里掺进下一根K线的数据,这类逻辑错误人眼扫一遍就能发现,但直接上线就是事故。
from openai import OpenAI # 本地部署的DeepSeek服务,兼容OpenAI协议,行情数据不出内网 client = OpenAI( base_url="http://10.20.30.40:11434/v1", api_key="local-only" ) def gen_feature_code(requirement: str) -> str: """把自然语言特征需求转成候选代码。仅作草稿,必须人工复核后合入。""" resp = client.chat.completions.create( model="deepseek-r1:14b", messages=[{ "role": "user", "content": f"你是量化风控工程师。用pandas实现以下流动性特征指标函数," f"输出完整代码并加中文注释:{requirement}" }], temperature=0.2 ) return resp.choices[0].message.contentbase_url指向内网部署的推理服务,deepseek-r1:14b是中等尺寸的蒸馏模型,跑特征代码生成足够用。temperature=0.2压到很低,让输出尽量稳定、少一点自由发挥。关于本地部署的这个选择,我从一开始就没考虑外部API——债券行情数据属于敏感数据,任何情况下都不能出内网。
第二个用途是规则复核。监管对证券公司流动性风险有数量化要求,我把方案里的预警规则描述文段丢给DeepSeek,让它逐条列出对应的指标口径、触发条件和覆盖范围,用来查漏。它经常能找出我漏掉的细节,比如「连续三个交易日触发关注级别预警后,应自动升级为正式预警」。这个用法不需要代码,只需要把规则文档拆成小段喂进去,输出对照表人工签字确认。
注意:DeepSeek的输出永远只是草稿。特征代码必须过单元测试,规则对照必须有人工复核,没有任何商量余地——模型幻觉在这里不是小概率事件,而是每次都要防的默认情况。
3.3 警报复核闭环:从触发到处置
预警触发后不能直接发邮件,必须有一个复核闭环。我一般分三步走。第一步是数据质量检查,看触发时段内有没有快照缺失、报价跳变、复权价格异常;第二步是市场环境确认,检查当日同评级债券的整体成交状况,如果整个板块都在缩量,单券预警的权重就要下调;第三步是处置动作分级——关注级别只通知交易台加强盯市,预警级别则要限制新增买入、对存量持仓做折价出售模拟。
折价出售模拟是流动性风险处置里最实用的工具:把当前深度和价差代入,模拟卖出不同面值所需的时间跨度和折扣成本。实现上直接用compute_depth_metrics的bid_depth反推——卖出量除以买一档挂单量,得到「吃穿第几档」的结果,再累加每一档的价格偏移,就是抛售成本。这个模拟的输出我会让交易台签字确认,作为压力测试的留痕依据。
4. 模型构建:从深度指标到流动性风险评分卡
4.1 特征工程:哪些深度指标进模型,哪些只会添乱
预警规则能覆盖80%的明显场景,但真正的黑匣子在于「指标都在正常范围内,然而一个月后这只券就卖不出了」。这需要模型把多个深度指标的边际变化组合起来,提前识别出即将恶化的券。特征工程这一块,我踩过最大的坑是把所有算出来的指标一股脑丢进模型,结果特征相关性高得离谱:bid_depth、ask_depth、conservative_depth两两相关系数都在0.9以上,模型权重分配毫无意义。
我的特征池是这样收敛的。基础特征只保留4个:spread_bp、conservative_depth、depth_imbalance、depth_decay。衍生特征加3个:volume_ma5(过去5个窗口成交额均值)、amihud_iliq(Amihud非流动性)、depth_volatility(深度序列标准差)。交叉特征做1个:spread_depth_ratio = spread_bp / conservative_depth,这个比值含义是「单位深度对应的价差成本」,比单独看价差或单独看深度都更能刻画流动性恶化的拐点。
有一个明确的禁忌:不要引入num_quotes < 3的样本进训练集。这些样本代表「这段时间根本没有对手方报价」,信息量极低且极端值集中,会让模型学会一个粗暴规则——盘中无报价就是危机——看起来AUC很高,实际上对任何连续交易场景都没有泛化能力。
4.2 样本不均衡下的模型训练:为什么选逻辑回归和XGBoost搭配
流动性危机是典型的小概率事件。我做过统计,正样本(未来5天深度萎缩超30%且价差翻倍)占比通常不到3%,极端情况下只有1%。这种不均衡下,直接上深度神经网络是浪费——正样本太少,模型学不到稳定的结构。我一般用逻辑回归加XGBoost的搭配:逻辑回归产出可解释的评分卡给风控审批看,XGBoost做黑匣子兜底给量化团队自己用。
训练代码:
import xgboost as xgb from sklearn.model_selection import TimeSeriesSplit FEATURES = [ 'spread_bp', 'conservative_depth', 'depth_imbalance', 'depth_decay', 'volume_ma5', 'amihud_iliq', 'depth_volatility', 'spread_depth_ratio' ] def train_liquidity_model(df: pd.DataFrame): """ 训练流动性危机预警模型。 label 定义(用shift构造未来标签,仅训练用,不上线): 未来5个交易日 conservative_depth 相对当前萎缩超过30%, 且同期 spread_bp 扩张超过2倍,记为正样本1。 """ df = df.sort_index() df['future_depth'] = df['conservative_depth'].shift(-5) df['future_spread'] = df['spread_bp'].shift(-5) df['label'] = ( (df['future_depth'] < df['conservative_depth'] * 0.7) & (df['future_spread'] > df['spread_bp'] * 2.0) ).astype(int) # 剔除未来列,防止特征泄漏;NaN标签一并丢弃 train_df = df.dropna(subset=['label']).copy() X = train_df[FEATURES] y = train_df['label'] # 时间序列切分,禁止随机K折 tscv = TimeSeriesSplit(n_splits=5) for fold, (tr_idx, te_idx) in enumerate(tscv.split(X)): X_tr, X_te = X.iloc[tr_idx], X.iloc[te_idx] y_tr, y_te = y.iloc[tr_idx], y.iloc[te_idx] # 正负样本权重均衡:正样本越少,权重越大 scale_pos_weight = (y_tr == 0).sum() / max((y_tr == 1).sum(), 1) model = xgb.XGBClassifier( n_estimators=300, max_depth=4, learning_rate=0.03, scale_pos_weight=scale_pos_weight, eval_metric='auc', early_stopping_rounds=30, subsample=0.8, colsample_bytree=0.8 ) model.fit(X_tr, y_tr, eval_set=[(X_te, y_te)], verbose=False) print(f'fold {fold + 1} 最佳AUC: {model.best_score:.4f}') return model三个参数要重点说明。scale_pos_weight是正负样本比值的倒数,我的样本里正样本占比2%左右,这个值通常在30到50之间,它让模型在预测正样本时错误代价更高,直接解决不均衡问题,比SMOTE过采样稳定得多。early_stopping_rounds=30用测试折的AUC做早停,防止模型在尾部过拟合到交易噪音上。max_depth=4是刻意限制树深——深度指标之间的关系相对线性,树太深只会记住单只券的路径,换一只券就失效。
逻辑回归这边我不展开代码,但强调两个点:特征标准化必须只用训练集的均值方差,不能整段数据一起标准化,否则等于用了未来信息;评分卡的分箱建议在模型训练前就定好,不要训练后按系数硬凑,风控审核时要解释每一档的合理性。
4.3 模型验证:AUC不够,要看召回率和标签质量
流动性风险模型的验证比普通风控模型更苛刻。AUC在0.8以上只能代表排序能力,不代表能提前预警,因为正样本实在太小,AUC对「提前几天预警」这个诉求完全不敏感。我一般加两个指标:一是召回率,把阈值设在覆盖20%样本的位置,看正样本能召回多少;二是提前预警天数分布,把「模型首次告警距危机爆发日的间隔」画出来,如果大部分集中在0到1天,说明模型只是同步指标而不是领先指标,没多大用。
标签质量是另一个大坑。我用shift(-5)构造未来标签,这要求历史数据必须是日频对齐的连续序列。若某只券停牌一周,shift(-5)会跨过停牌期,把复牌后的深度和停牌前比较,制造出完全失真的标签。我现在的做法是先用交易日历做对齐,剔除停牌期间生成的所有样本,宁少勿假。
还有一个验证细节:提前预警天数分布应该按券汇总,而不是按样本点汇总。同一只券在危机前的每一天都会产生正样本,模型每天都在命中同一个事件,AUC和召回率都会被这只券拉高;按「券-事件」维度去重后重新计算,才是真实的样本外表现。
5. DeepSeek落地中的常见问题与排查:五个血泪坑
下面五条是这套方案从开发到上线的过程中最典型的坑,按踩坑频率排序。每一条都按「现象 → 原因 → 解决」写,方便对照排查。
5.1 订单簿快照缺失导致深度指标跳变
现象:某个交易日盘中,conservative_depth突然从5000万掉到0,触发预警,但当时盘面其实没有异常。
原因:数据源在09:30左右丢了连续三帧快照,compute_depth_metrics收到的快照只剩单边挂单,函数返回None;聚合层ffill最多只能前向填充连续缺失值,超过窗口就直接留下了上一帧的残留值,看起来像是深度瞬间清零。
解决:在聚合层增加快照覆盖度统计,记录每个窗口实际收到的快照帧数和双边挂单齐全的帧数。实时预警和离线回测都要求覆盖度大于90%才参与计算,低于这个阈值的一律标记为DATA_QUALITY_LOW,跳过预警触发。这个治理是我在踩过一次「假危机」之后才加的,现在数据质量检查和预警是同一条链路,不会漏判。
5.2 DeepSeek生成的插值代码引入前视偏差
现象:回测AUC高达0.92,但实时上线后预警效果极差,几乎每天都是假警报。
原因:DeepSeek生成的填充代码用了df['depth'].interpolate(method='linear'),而插值序列的构造包含了整段数据。回测时整段数据都在内存里,插值自然使用了未来信息;上线时逐日滚动,没有未来数据,填充行为完全不同。这是典型的「回测天堂、实盘地狱」。
解决:给生成代码加一条铁律——任何填充、去极值、标准化操作都不能跨越时间切分点。用groupby('ts_date')限定在日内填充,或者用expanding窗口逐步计算。我在复核DeepSeek输出的检查清单里把这一项列为第一优先,每个候选函数都会被追问一句:这一行有没有用到未来信息?没有明确答出来就不准合入。
5.3 阈值分位数漂移:牛熊切换后旧阈值全部失效
现象:2022年10月之前,预警阈值基本每周触发0到1次;11月理财赎回冲击之后,同一套阈值每周触发20多次,交易台直接屏蔽了告警邮件。
原因:市况急转直下,整个板块的深度中枢下移、价差中枢上移,用过去一年5%分位算出的固定阈值已经被新的常态远远甩开。阈值跟随市场变化的速度远慢于市场本身变化。
解决:改用两段式阈值——慢阈值用252天分位数识别周期性恶化,快阈值额外加一条20天分位数,任何指标突破20天极端值立即进入关注级。另外补一个阈值漂移监控:每周统计实际触发率,若连续两周触发率超过5%,自动冻结该券的预警输出并重新校准阈值。校准窗口拉长到三年,让牛熊完整周期都进基准。
5.4 模型对「永远卖不出」的债券失效
现象:某些低评级私募债,盘口常年只有做市商挂单,spread_bp常年高于300BP、深度常年接近0。模型对这些券的评分全是「高危机」,预警每天触发,和没有预警一样。
原因:模型学到的是「低流动性等于高风险」,而这些券的流动性差是常态,不是危机。它们真正的风险是信用事件,不是流动性恶化,用市场深度指标去预测信用风险属于工具用错了对象。
解决:在入模前做样本分层,把「长期低流动性的存量券」和「流动性由正常转恶化的券」分开建模。前者改用信用价差和舆情特征,后者才用市场深度指标。判断标准很简单:样本区间内深度指标的标准差小于某个阈值就归入低流动性常态组,不再参与流动性危机预警模型的训练。
5.5 回测撞上休市日历:节假日数据把指标拉出假警报
现象:春节前最后一个交易日,回测中大量预警触发,细查发现当天深度萎缩、价差扩大非常明显,但真实市场上那年春节前流动性本来就季节性收紧。
原因:交易日历没对齐。shift(-5)在节假日附近会跨过休市日,把节前和节后的数据错位比较;滚动窗口的分位数在节后重新计算时,节前极值会被算两次,造成阈值短暂失真。
解决:把所有时间序列操作统一挂到交易所交易日历上,禁止直接裸用shift(5);用CustomBusinessDay或者自维护的交易日表做偏移。回测脚本里加一个断言:每一个样本的标签时间窗口与交易日历对齐,节假日前后各剔除一个样本,让「节前效应」和「节后效应」不会串进相邻样本。
6. 把这套方案搬进生产:最小可行架构与压测验证
单机脚本跑通只是第一步,真正让预警有价值的是把它放进一个能每天运行、能追溯、能压测的生产链路。我的做法是「收盘后再推理」:交易时段内只做行情采集和特征计算,模型推理放在收盘后批量执行,所有预警结果落库供次日晨会核查。不做盘中实时推理有两个原因:一是债券流动性风险本身是日频级别的事件,分钟级预警没有增量价值;二是盘中实时推理出问题时会直接影响交易系统,风险收益比不划算。
最小可行架构分四层:行情接入层用Kafka收交易所Level-2快照和银行间报价,落ClickHouse做明细存储;特征服务层每天收盘后用aggregate_depth_snapshots和build_liquidity_warning批量计算指标和阈值;模型服务层加载训练好的XGBoost,输出每只券的危机概率和触发条件明细;告警层把结果按券种排序分级推送。DeepSeek不在在线推理链路上,它只存在于离线开发期的三件事:写特征代码初稿、复核规则覆盖度、把新券种的需求转成特征规格。
| 组件 | 选型建议 | 职责 |
|---|---|---|
| 行情接入 | Kafka + 自研解码器 | 接收快照、补齐缺失、单位统一 |
| 存储 | ClickHouse | 明细查询、回测数据源 |
| 特征计算 | Python定时任务 | 聚合快照、算深度指标 |
| 模型服务 | XGBoost + MLflow | 评分、版本管理、特征重要性输出 |
| 告警 | 企业微信/邮件 | 分级推送、附数据质量标记 |
压测我做了两轮。第一轮用2022年四季度的信用债调整行情做全量回放,把每天的指标和模型输出过一遍,判定标准是「模型能否在危机被公开讨论之前提前3个交易日给出预警」,结果是有明确流动性恶化路径的券命中率在70%左右。第二轮是灾难演练:人为把当天所有快照丢弃50%,验证预警链路是否会被数据缺失打崩,结果触发了一串DATA_QUALITY_LOW标记但链路没有误报——这正是我加了覆盖度检查之后想看到的行为。
我个人的一个习惯是:每次换数据供应商或者改解析代码,第一件事不是跑单券测试,而是把过去三年的历史数据完整回放一遍,对比新旧数据源算出的depth_decay和spread_bp序列,任何一只券出现超过5%的指标偏差都要查清原因。市场深度指标计算这件事,数据口径的一致性比模型精度更重要——口径一换,历史基准全部作废,这个教训我付过真金白银。希望这套方案和这些参数经验,能帮你在债券流动性风险评估这条路上少走一段弯路。
本文还有配套的精品资源,点击获取