简介:TENET-CODE是一份围绕尾部事件驱动神经网络(TENET)的经典实现代码,面向金融风险管理、量化投资与机器学习方向的研究者和实践者,帮助其理解极端风险事件的建模思路与落地方式。资源包共53个文件,约2.37MB,以R脚本、CSV数据、TXT说明、Markdown文档和PNG图表为主,另含少量Excel表格与R数据文件,覆盖数据预处理、模型训练、参数调优与结果可视化等环节。代码涉及尾部事件识别、神经网络结构、损失函数与优化器选择、金融时间序列特征工程、训练验证集划分及AUC-ROC等评估指标,并包含VaR、CoVaR、连通性网络与系统性重要金融机构等实战模块。已有1250人学习下载,适合希望深入掌握TENET模型实现细节、提升金融市场极端风险预测能力的读者参考。
1. 拆开 TENET-CODE:一份把尾部风险做成网络的 R 代码包
金融风控里有个老问题:正常波动谁都能建模,真正要命的是尾部。2008 年那种级别的回撤、单日暴跌、机构间风险同时爆表,传统线性模型基本抓不住。TENET 这套东西的思路,是把尾部事件当成一等公民,用网络结构去刻画机构之间的极端风险传导。我拿到 TENET-CODE.zip 的第一反应是:这不是一份跑个 demo 就完事的玩具,而是一套完整的 R 工程,从数据清洗、SIM 单指数模型、线性与非线性网络、VaR/CoVaR 一路铺到总联动指标和分组网络。它适合谁?做金融风险量化、想复现系统性风险网络、或者手头有收益率面板数据想找一套能改能跑的框架的人。不适合只想调个包看结论的人,因为这份代码里大量逻辑需要你自己接数据、调参数、看中间输出。
2. TENET 的建模逻辑:为什么用网络而不是回归
2.1 从单指数模型到总联动:TENET 到底在算什么
先把概念理清楚,不然后面看代码会迷。TENET 全称 Tail Event Driven Neural Network,尾部事件驱动神经网络。它的核心不是预测某只股票明天涨跌,而是回答一个更结构化的问题:当某家机构或某个板块进入尾部状态时,风险通过什么路径、以多大强度传到别人身上。
传统做法是跑分位数回归算 CoVaR,一家一家算,算完拼成矩阵。问题是金融机构之间的联动往往是非线性的,市场恐慌时相关性会突然跳升,线性回归在这种时刻会系统性低估风险。TENET 用神经网络去拟合这种非线性映射,再把拟合出来的连接强度整理成网络。
代码包里几个文件名其实已经把模块划分说清楚了:
| 文件/目录 | 作用 |
|---|---|
| TENET_SIM | 单指数模型,把个股收益分解到市场因子 |
| TENET_Linear | 线性版本的网络估计 |
| TENET_VaR_CoVaR | 计算 VaR 与条件 VaR |
| TENET_total_connectedness | 总联动指数 |
| TENET_total_in_out_individual | 单机构的入/出连接度 |
| TENET_total_in_out_groups | 分组层面的入/出连接度 |
| TENET_group_network | 分组网络构建 |
| TENET_SIFIs | 系统重要性金融机构识别 |
这套结构是层层递进的:先 SIM 降维,再估网络,再算联动,最后落到 SIFIs 识别。你如果只想要一个总联动数字,跑到 TENET_total_connectedness 就够了;要做监管视角的机构排序,就得把 in/out individual 和 SIFIs 都跑通。
2.2 环境与数据准备:R 版本、依赖和输入格式
这份代码是 R 写的,历史文件 .Rapp.history、.Rhistory 说明作者是在 RStudio 里长期迭代的。我一般会先确认三件事:R 版本、依赖包、数据格式。
R 版本建议 4.x,太老的 3.x 在神经网络相关包上容易出兼容问题。依赖方面,代码里涉及神经网络训练、分位数计算、矩阵运算,常见需要的是 quantreg、nnet 或 neuralnet、以及基础的 zoo/xts 处理时间序列。具体包名以你解压后脚本里的 library() 调用为准,不要凭记忆装。
数据输入看文件名就知道大概:BS_CFin_20170202_Weekly_Fina.xls 是资产负债表类的财务数据,GICS_Fin_Maco_weekly_Return2008_2016.csv 是 GICS 金融板块加宏观的周度收益率,时间跨度 2008 到 2016。也就是说这套代码默认吃的是周频面板数据,行是时间,列是机构或因子。
# 读取周度收益率面板,第一列通常是日期 ret <- read.csv("GICS_Fin_Maco_weekly_Return2008_2016.csv", header = TRUE, stringsAsFactors = FALSE) # 日期列转成 Date,方便后续按时间切片 ret$Date <- as.Date(ret$Date) # 检查缺失:金融面板里停牌、退市会留 NA colSums(is.na(ret)) # 收益率列去掉日期列,转成矩阵喂给后续模型 ret_mat <- as.matrix(ret[, -1])这段逻辑很直白:读进来、转日期、查缺失、转矩阵。参数上唯一要留意的是日期格式,如果你的日期是 2016/1/3 这种,as.Date 要补 format 参数,否则会整列变 NA,后面全崩。缺失值不要无脑填 0,金融收益率填 0 等于人为制造一个“无波动”的假观测,常见做法是前向填充或直接剔除该机构。
提示:先拿一小段数据跑通全流程,再上全样本。这套代码模块多,全样本第一次跑很容易在某个中间步骤卡住,定位成本高。
3. 跑通核心链路:从 SIM 到 VaR/CoVaR 的操作步骤
3.1 单指数模型 SIM:降维与因子提取
SIM 这一步的作用是把每家机构的收益率拆成市场因子加个体残差。为什么要拆?因为直接拿原始收益率估网络,市场共同波动会淹没机构间的真实传导,你会得到一个人人相连的稠密网络,没有信息量。
# 对每家机构跑单指数模型:r_i = alpha + beta * r_market + e_i sim_fit <- function(stock_ret, market_ret) { fit <- lm(stock_ret ~ market_ret) # 普通最小二乘 list(alpha = coef(fit)[1], beta = coef(fit)[2], resid = residuals(fit)) # 残差留作网络输入 } # 对面板每一列循环,收集残差矩阵 resid_mat <- sapply(seq_len(ncol(ret_mat) - 1), function(j) { sim_fit(ret_mat[, j], ret_mat[, "Market"])$resid })逻辑说明:lm 拟合市场因子,beta 是系统性暴露,resid 是个体特质部分。参数上,市场因子那一列必须明确指定,代码里假设它叫 Market,你换数据要改列名。残差矩阵才是后面网络估计的真正输入。这一步的坑在于 beta 的稳定性,如果某机构 beta 在样本期内剧烈变化,单一 beta 会失真,常见做法是滚动窗口估 beta,但这份代码默认是全样本,你要自己判断是否够用。
3.2 VaR 与 CoVaR:尾部阈值的设定
VaR 是单机构在给定置信水平下的最大损失,CoVaR 是“当某机构处于危机状态时,另一机构的 VaR”。TENET 的尾部驱动就体现在这里:它不建模整个分布,只关注超过阈值的部分。
# 计算单机构 VaR,置信水平 0.95,即 5% 分位 library(quantreg) var_fun <- function(x, tau = 0.05) { quantile(x, probs = tau, na.rm = TRUE) # 经验分位数 } # 对残差矩阵每列算 VaR var_vec <- apply(resid_mat, 2, var_fun, tau = 0.05) # 标记尾部状态:低于 VaR 的观测记为 1 tail_flag <- sweep(resid_mat, 2, var_vec, FUN = "<")参数说明:tau 是尾部概率,0.05 对应 95% 置信。这个值直接决定你抓多少极端样本,设太小(如 0.01)样本不够,神经网络训不动;设太大(如 0.10)尾部就不“尾”了。我一般先用 0.05 跑,看尾部样本量再调。tail_flag 是逻辑矩阵,后续网络估计会用它做条件。注意 quantile 默认算法在样本少时和不同插值方式有差异,跨机构比较时保持同一算法。
3.3 网络估计与总联动:把连接强度拼成矩阵
到这一步才是 TENET 的主体。用神经网络拟合“一家机构尾部状态”对“另一家机构收益”的影响,把拟合出的连接强度整理成邻接矩阵,再算总联动。
library(nnet) # 以机构 j 的尾部状态为输入,预测机构 i 的收益 tenet_link <- function(target_ret, tail_input) { dat <- data.frame(y = target_ret, x = as.numeric(tail_input)) fit <- nnet(y ~ x, data = dat, size = 3, # 隐藏层 3 个节点 linout = TRUE, decay = 0.01, # 回归输出 + 权重衰减 maxit = 500, trace = FALSE) predict(fit, newdata = dat) # 返回拟合值 } # 双重循环构建连接矩阵(机构数多时建议并行) n <- ncol(resid_mat) adj <- matrix(0, n, n) for (i in 1:n) { for (j in 1:n) { if (i != j) { pred <- tenet_link(resid_mat[, i], tail_flag[, j]) adj[i, j] <- cor(resid_mat[, i], pred, use = "complete.obs") } } }逻辑说明:size 是隐藏层节点数,3 是个保守起点,节点越多拟合能力越强但越容易过拟合;decay 是权重衰减,控制复杂度;linout=TRUE 表示输出连续值而不是分类。adj[i,j] 表示 j 的尾部状态对 i 的解释强度,用相关系数衡量。这个双重循环是性能瓶颈,机构上百时纯循环会很慢,常见做法是并行化或先做变量筛选。总联动就是把 adj 矩阵按行按列加总,代码包里 TENET_total_connectedness 干的就是这件事。
注意:神经网络有随机初始化,同一份数据两次跑结果不会完全一样。要可复现,训练前 set.seed 固定种子,并在论文或报告里写明种子。
4. 避坑与排查:这份代码最容易翻车的五个地方
4.1 现象:跑完 adj 矩阵全是接近 1 的高相关
原因:没有做 SIM 降维,直接拿原始收益率估网络,市场共同因子让所有机构看起来高度联动。解决:确认你喂给网络的是 SIM 残差而不是原始收益,检查 resid_mat 是否真的去掉了市场因子。
4.2 现象:nnet 报错或输出全是常数
原因:输入没有标准化,或者尾部标记全 0/全 1 导致输入无变异。解决:对残差做 scale 标准化,检查 tail_flag 每列的均值,接近 0 或 1 的列直接剔除,说明该机构样本期内没有尾部事件。
4.3 现象:VaR 算出来是正数,尾部标记反了
原因:收益率量纲或符号约定不一致,有的数据用百分比有的用小数。解决:统一量纲,确认 VaR 是负的分位数,tail_flag 用“小于 VaR”而不是“大于”。这个坑很隐蔽,标记反了后面全反。
4.4 现象:总联动指数逐年上升,看起来“风险越来越大”
原因:样本期覆盖 2008 到 2016,前期危机期联动本来就高,后期平稳期低,如果窗口没对齐会误读趋势。解决:明确你的滚动窗口长度,对比不同窗口下的联动曲线,不要拿单条曲线下结论。
4.5 现象:SIFIs 排序每次跑都不一样
原因:神经网络随机性加样本敏感性,排名靠后的机构本来就不稳定。解决:多次训练取平均连接矩阵,或者只关注排名前列且跨次稳定的机构,别把第 15 名和第 16 名的差异当信号。
5. 进阶用法:把 TENET 输出接到你自己的风控流程里
跑通全链路只是起点,真正有价值的是把输出用起来。我一般会做三件事。
第一,把总联动指数做成时间序列,和已知的风险事件对齐,看它是不是在事件前就有抬升。这一步不需要改代码,把 TENET_total_connectedness 的输出按时间存下来画图即可。如果联动指数在几次市场剧烈波动前都提前抬头,这套东西对你的场景就有预警价值。
第二,用 in/out 连接度做机构分层。TENET_total_in_out_individual 给的是每家机构的“发出风险”和“接收风险”强度。发出高、接收低的机构是风险源头,接收高、发出低的是风险承接方。监管视角关注前者,交易对手管理关注后者。你可以按这两个维度画散点,把机构分到四个象限,比单一排名信息量大得多。
第三,分组网络做板块级传导。TENET_group_network 和 TENET_total_in_out_groups 把机构聚合到组,适合看银行、保险、券商之间的风险流向。聚合规则要你自己定,常见是按 GICS 子行业或按资产规模分组。分组后节点少,网络图可读性高,适合放进汇报材料。
| 进阶方向 | 用到的模块 | 关键参数 |
|---|---|---|
| 联动预警 | total_connectedness | 滚动窗口长度 |
| 机构分层 | total_in_out_individual | 入/出连接度阈值 |
| 板块传导 | group_network | 分组规则 |
验证方法上,我习惯做两件事:一是把 TENET 的联动结果和简单相关系数矩阵对比,如果两者完全一致,说明你的非线性部分没起作用,参数可能没调对;二是留一段样本外数据,看尾部时期的联动估计是否比线性方法更贴近实际。这两步能帮你判断这套代码在你的数据上到底有没有增量。
从那以后我每次拿到这类网络型风险代码,都强制先跑一遍线性版本做基准,再上非线性,不然根本说不清提升来自模型还是来自数据。希望帮到你。
本文还有配套的精品资源,点击获取