尧图精选

债券市场流动性风险预警:基于市场深度指标与DeepSeek特征工程

🕒 发布时间:2026/9/19 6:04:34 📁 来源:尧图网络
简介这是一份面向债券市场流动性风险量化评估与危机预警的深度学习方案文档以DeepSeek-R1架构为技术主线适合证券风控、量化研究及相关从业者系统掌握市场深度指标的计算逻辑与预警模型构建方法。文档共221页、50大章节压缩包内为单个PDF文件大小11.48MB支持目录章节跳转和阅读器书签大纲快速定位。内容从债券流动性风险痛点切入详细展开Tick级行情数据采集与预处理、订单簿结构化存储与索引设计、买卖盘口深度实时计算、深度斜率与动态加权价差算法、大单冲击深度恢复能力指标以及历史危机事件特征标注、LSTM与Transformer时序预警模型适配改造、注意力机制风险因子识别等完整链路。又从市场深度数学模型到张量计算加速再到跨市场关联特征融合与非线性特征降维均配有推导过程和代码实现思路。目前已有68人浏览学习适合具备Python与机器学习基础、希望将深度学习落地到债券市场风险分析场景的读者。1. 债券市场的流动性风险往往先死在订单簿里债券市场的流动性风险有个麻烦的特征它不怎么出现在收益率曲线上而是先在订单簿里一点点消失。2020 年 3 月美债抛售是典型例子——价格还没大幅跳空买卖价差先拉宽一档挂单深度被连续成交撕掉之后才看到价格缺口。债券是报价驱动市场不是竞价交易流动性分层极为严重单看成交量或 N 笔成交效率只能得到滞后信号。市场深度指标计算解决的是“提前量”把盘口挂单量、相对价差、订单簿斜率这些微观结构数据变成可监控的时序特征再在此基础上做流动性危机早期预警。本文顺着“市场深度指标计算 → DeepSeek 辅助特征工程 → 预警模型构建 → 阈值验证”这条链走一遍。模型本身不复杂真正的难点在数据口径、特征一致性、以及如何用 DeepSeek 这类大模型把反复试错的过程压缩掉。2. 市场深度指标计算从逐笔盘口到日频特征2.1 指标选型价差、深度与订单簿斜率各管什么流动性风险评估方案里市场深度指标不能只算一个“挂单总量”要拆成几个互相独立的维度价格维度看买卖价差数量维度看累计深度结构维度看订单簿斜率。三者缺一个都容易被单一市场状态骗过去。指标口径流动性恶化的信号计算成本相对买卖价差(ask - bid) / mid价差持续走阔低最优价累计深度一档/三档/五档买量卖量深度腰斩低深度衰减率从一档到 N 档深度的递减速度浅层挂单消失中订单簿斜率价格偏离度对挂单量的回归斜率斜率变平中Amihud 非流动性收益率绝对值 / 成交量单位成交量推动价格波动的成本上升低这里要注意一个常见误用把交易所 Level2 快照里的“五档量”直接当成深度。债券 OTC 市场没有连续订单簿很多债券一天只有几条报价五档深度是造市商挂出来的“影子深度”真正可成交深度要打折扣。因此指标设计上我倾向于把“深度衰减率”单独列出来观察从一档到三档的深度差。如果一档深度骤降、三档深度没动说明造市商在撤浅层流动性这是早期预警里最有信息量的信号之一。2.2 用 Python 计算五档盘口深度与斜率拿到逐笔盘口快照后第一步是清洗剔除时间戳乱序、买卖价小于等于 0、卖价低于买价的脏数据。第二步才是计算指标。import numpy as np import pandas as pd def compute_book_features(ob: pd.DataFrame) - pd.DataFrame: 输入为盘口快照字段含 bid_price_1..5, bid_size_1..5, ask_price_1..5, ask_size_1..5 df ob.copy() # 中间价与相对价差mid 为 0 时价差返回 NaN避免除零 df[mid] (df[bid_price_1] df[ask_price_1]) / 2 df[rel_spread] (df[ask_price_1] - df[bid_price_1]) / df[mid].replace(0, np.nan) # 一档和三档累计深度单位按面值折算 df[depth_1] df[bid_size_1] df[ask_size_1] df[depth_3] ( df[bid_size_1] df[ask_size_1] df[bid_size_2] df[ask_size_2] df[bid_size_3] df[ask_size_3] ) df[depth_decay] df[depth_1] / df[depth_3].replace(0, np.nan) # 订单簿斜率用 5 档价格和挂单量做一次线性回归 slope_list [] for _, row in df.iterrows(): prices, sizes [], [] for i in range(1, 6): b_p, b_s row[fbid_price_{i}], row[fbid_size_{i}] a_p, a_s row[fask_price_{i}], row[fask_size_{i}] if b_s 0 and not np.isnan(b_p): prices.append(b_p); sizes.append(b_s) if a_s 0 and not np.isnan(a_p): prices.append(a_p); sizes.append(a_s) if len(prices) 3: slope_list.append(np.nan) else: slope_list.append(np.polyfit(prices, sizes, 1)[0]) df[book_slope] slope_list return df代码里有两个关键参数值得展开。rel_spread用的是中间价作为分母而不是最新成交价因为快照时刻的最新成交价可能滞后用中间价能消除成交延迟带来的分母抖动。depth_decay作为一档深度占三档深度的比例正常状态接近 1/3如果这个比值快速掉到 0.1 以下说明一档挂单被大额成交或撤单清空。订单簿斜率的计算用了np.polyfit(..., 1)一阶系数代表单位价格偏离对应的挂单量变化斜率变平意味着造市商不愿在偏离中间价的方向加厚挂单。逐笔计算完成后还要聚合成日频特征。债券的盘口快照频率不高单券一天可能只有几百条快照直接取日内均值会把开收盘的异常段混进去。我一般只取“连续有报价的时段”用日内中位数做主指标用 90 分位数记录压力时刻。daily df.groupby(ts_date).agg( spread_med(rel_spread, median), spread_p90(rel_spread, lambda x: x.quantile(0.9)), depth1_med(depth_1, median), decay_med(depth_decay, median), slope_med(book_slope, median), ).reset_index()为什么用中位数而不用均值债券快照里经常出现“垃圾报价”例如机构为满足监管要求挂出的大单成交概率极低均值会被这类大单拉高中位数更贴近可成交深度的典型水平。90 分位价差则保留了一天里最紧张时刻的信息这部分对早期预警比中枢更敏感。2.3 算出来的指标要先做稳定性检查指标算完后不要急着进模型。先用一个简单规则过滤异常交易日如果当日有效快照数量不足 30 条或者中间价波动超过 5%当日所有深度指标标记为不可用。原因是债券报价稀疏样本量太少时中位数本身就不稳定把它当成特征喂给模型只会让模型学到“数据稀疏”的假信号而不是真实的流动性恶化。3. 用 DeepSeek 加速指标计算API 调用、上下文工程与本地部署3.1 DeepSeek API 怎么调用一个最小可跑通的示例市场深度指标计算写完之后你会发现大量时间花在重复代码上换一只债券、换一个数据源、换一个字段命名清洗逻辑就要重写一遍。这时候 DeepSeek 的定位不是“替代模型”而是“特征工程的加速器”。最常见的做法是用 DeepSeek API 把自然语言描述直接转成可运行的 Python 代码。from openai import OpenAI client OpenAI( api_keyyour-api-key, base_urlhttps://api.deepseek.com ) completion client.chat.completions.create( modeldeepseek-chat, messages[ {role: system, content: 你是一名固收量化研究员擅长用 Python 做市场微观结构分析。}, {role: user, content: 给定五档盘口数据实现一个函数计算相对买卖价差、一档累计深度、深度衰减率并在除数为零时返回 NaN。} ], temperature0.2, max_tokens1024 ) print(completion.choices[0].message.content)temperature0.2是这类代码生成任务里最值得调的参数。写特征工程代码要求确定性高、可复现temperature 调太高会让模型输出不同的代码风格和变量命名调到 0.2 左右能保留少量多样性但又不会每次生成天差地别的实现。max_tokens1024够生成一个小函数如果要求生成完整脚本建议改成 4096否则输出会被截断在 import 语句附近。注意这段代码使用的 OpenAI SDK 兼容接口chat.completions.create的返回结构里choices[0].message.content就是模型输出。如果你的环境网络策略不允许访问外部 API可以把base_url换成内部网关通常不需要改代码逻辑。3.2 上下文工程把口径写进 Prompt而不是等它猜调用 DeepSeek API 最常见的失败不是代码报错而是它不知道你的数据长什么样。给一个光秃秃的任务描述它会默认你在处理股票订单簿生成一堆跟债券无关的逻辑。正确做法是把列名、数据样例、口径约束都塞进 prompt。prompt f 请实现一个 Python 函数输入 pandas DataFrame包含以下列 bid_price_1..5, bid_size_1..5, ask_price_1..5, ask_size_1..5, ts_date, isin 要求 1. 计算相对价差 (ask_price_1 - bid_price_1) / mid 2. 计算一档深度 bid_size_1 ask_size_1单位视为万元面值 3. 深度衰减率 一档深度 / 三档深度 4. 按 isin 和 ts_date 分组返回每日中位数 5. 任何除数为 0 的字段置为 NaN不要用 0 填充 输出可直接运行的 Python 代码不要输出解释。 这里有一个容易被忽略的参数isin一定要出现在分组键里。债券量化里经常犯的错误是只按日期分组忘记同一日期下有多个券。另一条约束是“除数为 0 置为 NaN 不要用 0 填充”因为 0 和缺失在风控系统里含义完全不同0 会被当成“没有流动性”而 NaN 会被下游模型跳过或插值。Prompt 里不写这条DeepSeek 很可能顺手用fillna(0)。3.3 数据不出域时的本地部署路径如果债券交易数据受合规约束不能出域本地部署是常见做法。把 DeepSeek 系列开源模型用 vLLM 或 Ollama 拉起来暴露一个 OpenAI 兼容接口客户端代码只需要改base_url连接本地服务。这样做的代价是代码生成速度下降但换来的是数据安全边界不动。本地部署后还可以顺手接入命令行编码代理比如你日常用的 Codex 类工具或 VS Code 插件把模型入口从云端切到本地就能在写指标计算脚本时直接让模型补全。部署时有个细节量化团队服务器通常没有 GPUCPU 推理虽然慢但对“生成一段特征工程代码”这种非实时任务完全够用真正要实时计算深度指标的在线预警模块走的还是 Python 脚本不经过大模型。Prompt 要素作用反面例子字段清单约束模型生成的代码不会臆造列名只说“计算深度”输出格式减少解析成本让模型自由发挥边界条件除零、空值、稀疏报价处理只要求“算出来”单位说明面值、手数、万元口径统一单位混用4. 构建流动性危机早期预警模型标签构造、特征融合与分级阈值4.1 标签怎么定义用“未来坏状态”而不是“未来收益率”流动性危机早期预警模型的难点不在算法在标签。债券流动性恶化是一个状态不是一两个大单就能定义的。我常用的做法是把标签定义为“未来 N 个交易日相对价差是否跳升超过 1.5 倍同时一档深度是否下降超过 50%”。两个条件同时满足才算危机信号避免把个别大额成交导致的瞬时波动当预警。import pandas as pd def build_label(daily: pd.DataFrame, horizon: int 5) - pd.DataFrame: df daily.sort_values([isin, ts_date]).copy() # 未来 horizon 天的最大价差 / 当天价差 df[future_spread_max] ( df.groupby(isin)[spread_med] .shift(-1) .rolling(horizon, min_periods1) .max() .shift(-(horizon - 1)) ) df[future_depth_min] ( df.groupby(isin)[depth1_med] .shift(-1) .rolling(horizon, min_periods1) .min() .shift(-(horizon - 1)) ) df[label_risk] ( (df[future_spread_max] / df[spread_med] 1.5) (df[future_depth_min] / df[depth1_med] 0.5) ).astype(int) return df这段代码里的shift和rolling组合是许多人写错的地方。shift(-1)先对齐到下一天rolling(horizon).max()再取未来窗口的最大值最后再shift(-(horizon - 1))把窗口末端对齐回当天。三个操作缺一个标签就会整体偏移造成前视偏差。注意groupby(isin)是必须的不同债券的流动性水平差异极大跨券计算未来窗口会把高价债和低价债混在一起产生幽灵标签。4.2 特征矩阵把微观结构指标叠成时间序列预警模型的特征不能只用当天的深度指标要叠成“近 1 日、近 5 日、近 20 日”三个窗口。短窗口捕捉突变长窗口捕捉趋势。特征表大致如下特征组窗口构造方式价差1/5/20 日相对价差中位数的均值与标准差深度1/5/20 日一档深度中位数的变化率与波动率斜率1/5/20 日订单簿斜率的均值与最小值衰减1/5/20 日深度衰减率的 10 分位数量能1/5/20 日Amihud 指标的对数在有限样本里堆几十个特征容易过拟合。债券预警样本通常只有几百只券乘几百个交易日有效事件数更少因此我倾向把特征控制在 15 个以内并且只保留与“流动性状态”直接相关的变量不带入股票市场的换手率、波动率因子。4.3 用梯度提升机做阈值分类特征重要度比准确率更重要具体模型上LightGBM 这类梯度提升机足够用不需要上深度学习。它天然处理缺失值和时间截断特征训练快而且特征重要度能直接告诉风控人员“这次预警主要是由哪个指标触发的”。from sklearn.model_selection import train_test_split import lightgbm as lgb features [c for c in df.columns if c.startswith(f_)] X df[features] y df[label_risk] X_tr, X_va, y_tr, y_va train_test_split( X, y, test_size0.3, stratifyy, random_state42 ) model lgb.LGBMClassifier( n_estimators300, learning_rate0.05, max_depth4, num_leaves16, min_child_samples30, class_weightbalanced ) model.fit(X_tr, y_tr) importance pd.Series( model.feature_importances_, indexfeatures ).sort_values(ascendingFalse) print(importance.head(10))max_depth4和num_leaves16是控制过拟合的第一道闸。债券预警样本少、噪声大树太深会记住个别异常券的特征组合。min_child_samples30保证每个叶子节点至少落 30 个样本防止模型学到一个特定日期的高危形态。class_weightbalanced用来处理正负样本比例失衡流动性危机事件占比通常不到 5%不调这个参数模型会全部预测为“无风险”。运行后重点关注importance的前几名。如果排名靠前的都是“近 1 日深度变化率”和“深度衰减率 10 分位”说明模型确实在利用微观结构突变如果Amihud冲到第一很可能数据里有极端成交量的异常值要回头检查指标计算的稳定性。4.4 分级预警概率折算成黄橙红三个等级模型输出的是危机概率风控系统需要的是可执行等级。做法是把验证集上的预测概率按分位数切成三挡概率高于 90 分位为红色75 到 90 分位为橙色50 到 75 分位为黄色。阈值不用拍脑袋而是根据月度可处理的预警条数反推。val_prob model.predict_proba(X_va)[:, 1] red_thr np.percentile(val_prob, 90) orange_thr np.percentile(val_prob, 75) yellow_thr np.percentile(val_prob, 50)这里隐含一个运维约束红色预警如果每月出现太多次风控团队会麻木。所以我把“每 100 个债券交易日里红色预警不超过 5 次”作为调阈值的前提而不是机械追求模型 AUC。分级之后每个等级再绑定动作黄色提示关注橙色要求交易员反馈流动性状况红色触发深度复查。5. 预警阈值验证与报告自动化滚动回测、误报率校准、DeepSeek 生成异动说明预警模型上线前阈值验证比训练更花时间。常见的错误是用全部历史数据一次性切训练和测试集这样评价出来的阈值包含未来信息。正确做法是滚动回测用前 250 个交易日训练往后推 20 个交易日验证再整体平移窗口每一步重新训练并记录预测概率的分布。from sklearn.metrics import precision_recall_curve, f1_score def calibrate_threshold(model, X_va, y_va, max_fpr0.1): prob model.predict_proba(X_va)[:, 1] precision, recall, thresholds precision_recall_curve(y_va, prob) f1 2 * precision * recall / np.maximum(precision recall, 1e-9) best_idx int(np.argmax(f1)) # 用误报率上界校准误报率超过 10% 的阈值一律不选 for i in range(len(thresholds) - 1, -1, -1): pred prob thresholds[i] tn np.sum((pred 0) (y_va 0)) fp np.sum((pred 1) (y_va 0)) if fp / max(tn fp, 1) max_fpr: return thresholds[i] return thresholds[best_idx]这段代码的核心在最后那个循环从高阈值往低阈值扫找到第一个满足误报率上界的阈值。为什么从高往低因为高阈值对应少而准的预警从宽松往紧找会卡在低阈值上让误报率指标虚高。实际使用时不要直接套我代码里的 0.1要根据风控团队的处理容量设定。最后是报告自动化。预警触发后把债券的近期深度指标变化和模型输出的 TOP 特征喂给 DeepSeek让它生成一段自然语言异动描述能省掉交易员手动翻数据的功夫。alert_features importance.head(5).index.tolist() alert_desc client.chat.completions.create( modeldeepseek-chat, messages[ {role: system, content: 你是债券风控助手输出简洁的异动说明不给出投资建议。}, {role: user, content: f债券 {isin} 触发红色预警驱动特征及变化率{alert_features}请写一段 3 句以内的说明。} ], temperature0.1 )这一段的temperature0.1是故意压低的异动说明要客观、可复核不需要模型发挥文采。Prompt 里限定“3 句以内”和“不给出投资建议”是为了让输出符合风控合规约束避免大模型自己脑补操作指令。整套链路跑通之后市场深度指标计算、危机概率预测、报告生成变成一条可回测的管道每次预警都能追溯是哪几个深度指标先动的手。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →