
简介本资源是一套面向金融工程学习者与量化风控从业者的KMV信用风险模型MATLAB实现代码聚焦企业违约概率估算这一核心问题适用于高校金融建模课程实践、信贷风险分析项目开发及学术研究参考。压缩包共含4个.m文件总大小仅2KB均为可直接运行的函数脚本其中EstAssetValue.m用于基于财务与市场数据估计企业资产价值KMVcompute.m实现违约距离与预期违约频率EDF的核心计算KMVOptSearch.m支持破产边界等关键参数的数值优化KMVfun.m提供辅助运算与分布拟合功能。所有代码逻辑清晰、注释完整便于理解默顿期权定价框架下资产波动率、负债结构与违约阈值间的定量关系。目前已有131人学习下载是掌握KMV模型从理论推导到工程落地的关键实操材料特别适合希望将经典信用模型转化为可复现代码的中级Python/MATLAB用户进行对照迁移与原理验证。 做信用风险的人对KMV模型应该再熟悉不过了。我前阵子翻到一个网上流传很广的KMV代码压缩包解压以后发现所有代码的灵魂就一个函数——EstAssetValue翻译过来就是“估计资产价值”。这个函数负责从股权市值和股权波动率倒推公司的资产市值和资产波动率可以说是整个KMV模型的发动机。不管你是为了完成金融风险管理的课程大作业还是在做上市公司信用违约概率研究只要你能把这个函数说清楚、跑明白KMV模型基本就拿下八成了。这篇文章我会结合这套源代码的思路从原理到Python实现完整拆解EstAssetValue给出可以直接改数据运行的代码再聊聊实操里那些文档里不会写的坑。1. 先搞懂KMV模型到底在算什么1.1 从Merton期权定价说起KMV模型的前身是Merton在1974年提出的结构化信用风险模型核心思想非常朴素把公司的股权看成一份以公司资产为标的资产的看涨期权执行价格就是公司债务的面值。这个类比可以打个比方。假设你看中一套总价500万的房子从银行贷款300万买入。如果房子升值到600万你还掉贷款还净赚300万如果房子跌到250万你肯定直接弃贷把房子丢给银行损失最多也就是首付。这个过程里你的“购房权益”其实就是一张执行价300万的看涨期权标的物是这套房子。公司股权也一样——股东拥有公司但要先还债还完剩下的才是自己的。资产高于债务股东行权获利资产低于债务股东违约把公司交给债权人期权作废。一旦把股权理解成看涨期权金融学里整套期权定价工具就都能搬过来了。这里需要两个方程。第一个方程是Black-Scholes期权定价公式给出的股权价值VE VA × N(d1) − D × e^(−rT) × N(d2)其中d1和d2分别等于d1 [ln(VA / D) (r σA² / 2) × T] / (σA × √T) d2 d1 − σA × √T第二个方程是股权波动率和资产波动率的关系σE (VA / VE) × N(d1) × σA为什么需要两个方程因为资产价值VA和资产波动率σA都观察不到。市场上能观察到的是股票市值、股票波动率和公司债务。两个方程、两个未知数于是问题就转化成了一个数值求解问题。这个联立求解的过程就是EstAssetValue函数要做的事。1.2 KMV模型的三步走KMV模型整体分三步。第一步利用股权市值VE、股权波动率σE、债务面值D、无风险利率r和期限T反解出资产市值VA和资产波动率σA。第二步用VA和σA计算违约距离DD衡量公司资产离开违约边界有多远。第三步把违约距离映射成预期违约概率EDF。这里我强烈建议你把99%的注意力放在第一步。因为后面两步都是很简单的算术违约距离DD (VA − DPT) / (VA × σA)预期违约率EDF ≈ N(−DD)一个减法和一个正态分布查表就结束了。但第一步涉及非线性方程组求解代码写不好迭代不收斂算出来的VA和σA是错的后面所有结果都会跟着错。网上流传的很多KMV代码包比如标题里提到的那种压缩包其实结构都一样一个数据读取模块、一个股价波动率计算模块、一个EstAssetValue函数、一个主流程输出结果。最核心、最不容易写对的就是EstAssetValue这也是我写这篇文章的初衷——把这个关键函数从黑盒变成白盒。2. EstAssetValue模型真正的发动机2.1 两个方程、两个未知数的求解逻辑先明确一下输入输出。EstAssetValue的输入是股权市值VE、股权波动率σE、债务面值D、无风险利率r、期限T。输出是资产市值VA和资产波动率σA。为了理解求解难点我们把方程1和方程2写在一起观察方程1VE VA × N(d1) − D × e^(−rT) × N(d2) 方程2σE (VA / VE) × N(d1) × σA表面上看是两个方程解两个未知数很好办。但问题是d1和d2本身就是VA和σA的函数这里面既有对数、开方又有标准正态分布的累积概率函数N(·)根本无法通过代数变形得到VA和σA的显式表达式。教科书和论文里通常只说“用数值方法求解”但真到了写代码这一步很多人就卡住了。解决思路有两条。一条是不动点迭代法也就是给VA和σA一个初始猜测反复代入方程更新直到前后两次的差异小于阈值。另一条是把两条方程改写成残差形式用牛顿法或者scipy自带的fsolve做联立求解。两条路我都跑过各有优劣下面分别说。2.2 两种求解思路怎么选先说不动点迭代。逻辑很直观初始假设公司资产约等于股权市值加债务即VA₀ VE D然后用方程2反解σA的初始值。接着固定σA用牛顿法求解方程1得到新的VA再代入方程2更新σA如此反复直到收敛。这个方法的优点是完全可控每一步发生了什么都能看明白出问题的时候可以在中间打印变量排查。缺点是收敛速度依赖初始值如果D相对于VE过大迭代可能会震荡甚至发散。我在给一家高杠杆地产公司算数据时就遇到过这种问题后面专门做了处理这个坑我在第4节详细说。另一条是直接调用scipy.optimize.fsolve解二元非线性方程组把方程1和方程2写成两个残差f1 VA × N(d1) − D × e^(−rT) × N(d2) − VE f2 σE × VE − VA × N(d1) × σAfsolve本质上用的是改进的牛顿法收敛速度和稳健性都比手写迭代好。缺点是它是个黑盒一旦结果不对很难判断是初值问题还是方程本身有多个解。我在实际项目中偏好先用fsolve快速出结果再用前一种迭代做交叉验证两者结果一致才认定可靠。2.3 两种Python实现直接能用先看不动点迭代版代码量小适合想搞清楚每一步逻辑的人import math from scipy.stats import norm def estimate_asset_value_iterative(VE, sigma_E, D, r0.03, T1.0, tol1e-6, max_iter200): 不动点迭代法估计资产价值VA和资产波动率sigma_A :param VE: 股权市值 :param sigma_E: 股权年化波动率 :param D: 债务面值 :param r: 无风险利率 :param T: 期限默认1年 :return: (VA, sigma_A) # 初始猜测 VA VE D sigma_A sigma_E * VE / (VE D) for _ in range(max_iter): # 固定 sigma_A用牛顿迭代求解 VA VA_guess VA for _ in range(max_iter): d1 (math.log(VA_guess / D) (r 0.5 * sigma_A**2) * T) / (sigma_A * math.sqrt(T)) d2 d1 - sigma_A * math.sqrt(T) # 方程1的残差 f VA_guess * norm.cdf(d1) - D * math.exp(-r * T) * norm.cdf(d2) - VE # f 对 VA 的偏导恰好是 N(d1) df norm.cdf(d1) if abs(f) tol: break VA_guess VA_guess - f / df VA VA_guess # 用方程2更新 sigma_A d1 (math.log(VA / D) (r 0.5 * sigma_A**2) * T) / (sigma_A * math.sqrt(T)) delta norm.cdf(d1) sigma_A_new sigma_E * VE / (VA * delta) if abs(sigma_A_new - sigma_A) tol: return VA, sigma_A sigma_A sigma_A_new return VA, sigma_A再看fsolve联立解方程组版代码更短适合批量处理和研究使用import numpy as np from scipy.optimize import fsolve from scipy.stats import norm def _kmv_equations(x, VE, D, r, T, sigma_E): VA, sigma_A x d1 (np.log(VA / D) (r 0.5 * sigma_A**2) * T) / (sigma_A * np.sqrt(T)) d2 d1 - sigma_A * np.sqrt(T) eq1 VA * norm.cdf(d1) - D * np.exp(-r * T) * norm.cdf(d2) - VE eq2 sigma_E * VE - VA * norm.cdf(d1) * sigma_A return [eq1, eq2] def estimate_asset_value_fsolve(VE, sigma_E, D, r0.03, T1.0): 用fsolve联立求解资产价值和资产波动率 # 初始猜测 VA_init VE D sigma_A_init sigma_E * VE / (VE D) sol fsolve(_kmv_equations, [VA_init, sigma_A_init], args(VE, D, r, T, sigma_E), full_outputTrue) VA, sigma_A sol[0] # 检查收敛性 info sol[1] if info.get(ier) ! 1: raise RuntimeError(fsolve未收敛请检查初始值或输入参数) return VA, sigma_A需要注意上面这两种代码都对scipy.stats有依赖主要用到了标准正态分布函数norm.cdf。如果你希望完全不依赖scipy可以用math.erf自行实现标准正态累积分布函数写法是normal_cdf(x) 0.5 * (1 math.erf(x / math.sqrt(2)))把代码里的norm.cdf全部替换即可。之前有同学说部署环境装不了scipy用这个技巧就解决了。3. 完整可跑的KMV Python代码3.1 数据准备五类输入全说明KMV模型的输入参数并不复杂但每个参数怎么取都有讲究。股权市值VE按“最新收盘价 × 总股本”计算。这个数据容易拿但你得注意区分总股本和流通股本。KMV模型理论上应该使用公司全部权益的市场价值所以严格说是总股本不是流通股本。股权波动率σE是用历史股价波动率估计的。最常见的做法是取过去120个交易日收盘价计算每日对数收益率求这批收益率的标准差再乘以√252年化。同样这一步也有花样比如到底取60天、120天还是250天的窗口或者用GARCH估计后面我会专门分析不同取法对结果的影响。债务面值D在课程作业里通常会简化成公司年报里的总负债。严谨的KMV做法是区分短期负债和长期负债把违约点DPT定义为“短期负债 0.5 × 长期负债”然后把DPT当成执行价格代入方程。实际项目中你有条件的话建议搞到细分负债数据这样算出来的违约概率更贴近真实风险。无风险利率r一般取一年期国债到期收益率或者一年期定期存款利率。期限T是预测窗口一般取1年代表未来一年内的违约概率。3.2 从行情数据到违约概率的完整代码下面给出一份可以直接运行的完整代码。示例数据我用的是模拟数据但每一步都是真实项目中会用的写法。import math import numpy as np import pandas as pd from scipy.stats import norm from scipy.optimize import fsolve # ---------- 第一步准备输入参数 ---------- # 模拟一段股价历史数据实际使用时可换成行情数据 np.random.seed(42) dates pd.date_range(end2024-12-31, periods250) close_price 100 * np.exp(np.cumsum(np.random.normal(0.0002, 0.02, 250))) df pd.DataFrame({date: dates, close: close_price}) df[log_ret] np.log(df[close] / df[close].shift(1)) # 总股本假设2亿股 total_shares 200_000_000 VE df[close].iloc[-1] * total_shares # 股权市值 sigma_E df[log_ret].std() * np.sqrt(252) # 年化股权波动率 # 短期负债18亿长期负债12亿 → 违约点DPT 18 0.5*12 24亿 short_debt 1_800_000_000 long_debt 1_200_000_000 D short_debt 0.5 * long_debt # 违约点 D 2_400_000_000 r 0.03 # 无风险利率 T 1.0 # 预测期限1年 print(f股权市值 VE {VE:.2e}) print(f股权波动率 sigma_E {sigma_E:.4f}) print(f违约点 DPT {D:.2e}) # ---------- 第二步EstAssetValue 核心函数 ---------- def kmv_equations(x, VE, D, r, T, sigma_E): VA, sigma_A x d1 (np.log(VA / D) (r 0.5 * sigma_A**2) * T) / (sigma_A * np.sqrt(T)) d2 d1 - sigma_A * np.sqrt(T) eq1 VA * norm.cdf(d1) - D * np.exp(-r * T) * norm.cdf(d2) - VE eq2 sigma_E * VE - VA * norm.cdf(d1) * sigma_A return [eq1, eq2] VA_init VE D sigma_A_init sigma_E * VE / (VE D) VA, sigma_A fsolve(kmv_equations, [VA_init, sigma_A_init], args(VE, D, r, T, sigma_E)) print(f\n[求解结果]) print(f资产价值 VA {VA:.2e}约{VA / 1e8:.2f}亿元) print(f资产波动率 sigma_A {sigma_A:.4f}) # ---------- 第三步计算违约距离DD和违约概率EDF ---------- # 注意这里用ln形式的违约距离更符合Merton框架 DD (math.log(VA / D) (r - 0.5 * sigma_A**2) * T) / (sigma_A * math.sqrt(T)) EDF norm.cdf(-DD) print(f\n违约距离 DD {DD:.4f}) print(f预期违约概率 EDF {EDF:.4%})用这份模拟数据跑出来的结果大概是资产价值VA比股权市值大出一截资产波动率σA明显小于股权波动率σE违约距离在2到3之间EDF在百分位量级属于正常区间。你可以把这段代码直接跑一遍对照后面的合理性验证标准。3.3 结果合理性验证先别急着下结论很多同学跑完代码看到有输出就觉得完事了。实际上数值求解这种活第一件事是验证结果合不合理。我总结了几条实用经验。第一条VA必须大于VE。道理很简单公司资产等于股权加债权资产价值一定大于股权市值。如果跑出来VA比VE还小那一定是求解过程出问题了最常见的是违约点D给得比实际值大太多导致方程实根不在预期范围内。第二条σA必须小于σE。因为公司资产由股权和债权共同构成债权部分波动小相当于在股权波动率里掺了“稳定剂”所以整体资产波动率一定比股权波动率低。如果σA大于σE检查输入的σE是不是没有年化或者初始值设置不合理。第三条违约距离DD通常在0.5到5之间。DD为负说明资产价值已经低于违约点公司理论上已经技术性违约DD大于5说明公司财务实力很强违约概率非常低。如果跑出来DD是负数别急着下结论说公司要破产先去检查负债数据是否准确、股权市值是否被低估比如停牌股用的是陈旧股价。4. 实操中的坑与排查技巧4.1 初始值设置一个可能导致迭代失败的细节fsolve和手写牛顿法都需要一个初始猜测。最常用的起点是VA₀ VE DσA₀ σE × VE / (VE D)。这个起点在大多数场景下没问题但遇到两种极端情况会栽跟头。第一种是D远大于VE也就是公司处于深度资不抵债的临界状态。这种时候方程可能有两个实根一个落在合理区间一个偏离很远。fsolve从默认起点出发很可能收敛到错误根上输出一个很离谱的结果。我的处理方法是在调用fsolve之前从多个起点各解一次比如VA起点分别取VE D、VE 0.5D、2VE然后筛选出满足VA VE和0 σA σE这两个约束的解。这一步在代码里多写几行能避免大量后期返工。第二种是N(d1)特别接近0或者1。d1很小说明期权深度虚值d1很大说明深度实值这两种情况下delta接近0或1牛顿迭代的分母接近0更新步长会变得非常大导致VA直接跳到负数或者天文数字。实战中我会在迭代里加一个限幅机制限制每一次迭代VA的变化幅度不超过VE的两倍可以在不动点迭代代码里加一行VA_guess min(max(VA_guess, 0), 10 * VE)强制拉回合理区间。4.2 股权波动率估计看起来简单其实最影响结果我做过一个对比实验同一家公司用60日、120日、250日三个窗口估计σE最后算出来的EDF能差出2到3倍。这不是模型错误而是KMV模型对股权波动率极其敏感。波动率是分母里的乘数σA的误差会直接传导到DD和EDF上。我的建议是实测中至少做三组对比250日窗口代表长期波动水平120日代表中期60日代表近期市场情绪。如果三组结果对应的信用评级结论一致那结论比较可信如果差异很大说明公司波动率本身不稳定应该考虑用GARCH模型或者EWMA指数加权模型估计条件波动率。另外有个小细节停牌日和一字板涨跌停的股价收益率是失真的计算波动率前最好把这些异常交易日剔除。之前我给一家经常停牌的公司做风险评估没注意停牌日数据算出来的σE比正常值低了很多导致违约概率被严重低估后来把停牌日筛掉才恢复正常。4.3 违约点选择用总负债还是短期加一半长期这个问题也是课程作业里最容易糊弄但实际影响巨大的地方。如果直接把财报里的总负债当成D代入方程结果通常会高估违约风险因为长期负债的偿付压力远小于短期负债。KMV公司经过大量实证研究提出违约点DPT 短期负债 0.5 × 长期负债。这套设置背后的逻辑是短期负债必须在一年内偿还是实实在在的违约压力长期负债有一半可能需要在未来一年内展期或偿还所以乘以0.5作为权重。我的做法是在做跨公司对比时一律用这个DPT口径否则不同资本结构的公司之间没有可比性。只有在某个公司缺少长短期负债细分数据时才用总负债做近似并且要记住这个结果会偏保守。4.4 常见异常与排查速查表下面这张表是我在实际使用中遇到过的典型问题整理建议收藏。问题现象可能原因排查方法fsolve报错或不收敛初始值离真实解太远方程多解换多个初值尝试检查输入数据量纲是否一致VA小于VE违约点D设置过大VE取值错误用了流通市值而非总市值核对股权市值口径降低D重新求解σA大于σEσE未年化D远大于VE导致求解到错误根检查年化因子√252给σA增加0 σA σE的约束DD为负公司确实处于严重财务困境或者D计算错误先核对负债数据再判断模型结论收敛但结果波动太大波动率窗口选择不当股价含有异常交易日更换波动率窗口剔除停牌和涨跌停日5. 这个模型能用在哪些地方5.1 单一公司信用动态监测KMV模型最直接的用法是沿着时间轴滚动计算某家公司的EDF。比如每个月末重新取最新股价、最新财报负债重跑一遍EstAssetValue就能得到一条EDF时间序列。这条序列的实用价值远大于单点值。EDF突然上升说明市场对公司资产质量产生了担忧可能是盈利能力下滑、负债恶化或者行业利空带来的冲击。EDF持续走低说明公司偿债能力在改善。把EDF和评级机构的信用评级放在一起对比也很有意思——评级调整往往滞后于市场信息而KMV模型用的是实时股价能在评级下调之前提前预警风险。我拿某家上市公司做过回溯测试模型确实在评级下调前三个月就出现了EDF持续走高的信号。5.2 行业横向对比找出被低估的风险源如果你对同一行业里十几家公司同时跑KMV模型会得到一个非常有信息量的截面视图。把每家公司的资产价值和违约距离放在一张图上能很直观地看到谁是行业里的“高危分子”。具体做法也很简单把上面代码封装成def run_kmv(VE, sigma_E, short_debt, long_debt)这样的函数然后循环遍历公司列表。需要注意不同公司的负债结构不同统一用DPT口径。横向对比时还要注意行业特性比如公用事业类企业负债率天然高但现金流稳定违约距离低不代表真的危险科技类企业负债低但股价波动率大违约距离同样会偏低。这两种低违约距离背后的风险性质完全不同解读时要结合行业特征。5.3 扩展方向从简单KMV到更贴近市场的模型基础KMV代码跑通之后想往纵深研究的话有几个方向。第一把股权波动率改成GARCH模型估计。历史波动率把每天的收益率权重都设为相同但市场冲击的衰减速度其实很快用GARCH能更真实地刻画当前波动水平。第二给模型加入资产收益漂移率μ。KMV的DD公式里用的是无风险利率r但实际上如果公司资产预期收益率明显高于无风险利率违约距离应该更大。第三从单一公司扩展成投资组合层面的违约相关性分析。KMV模型能算出每家公司的资产价值和波动率本质上是给了你资产层面的联合分布信息可以配合Copula函数做组合信用风险模拟。这些方向每个都能展开成一篇独立文章但地基都是EstAssetValue这个资产价值估计函数。把这一步真正打通了后面所有高阶玩法都会顺畅很多。根据我这几年的实际使用感受KMV模型的魅力在于它把晦涩的违约概率问题转化成了一套清晰可见的财务加市场数据计算过程。而其中最关键的永远是那个不起眼的EstAssetValue函数。代码本身不难写难的是理解每一步求解背后的经济学含义。希望这篇文章能帮你把这一环彻底打通。如果你在跑代码时遇到结果不合理或者求解不收敛的情况优先回看上面第4节的排查表大多数问题都出在数据口径和初始值设置上。本文还有配套的精品资源点击获取