KMV模型Python源码解析:EstAssetValue资产价值与违约概率计算 简介本资源是一套面向金融工程学习者与信用风险建模实践者的KMV模型MATLAB实现代码包适用于高校金融/统计专业学生、风控从业者及量化研究入门者用于理解并复现基于默顿期权理论的违约概率估算方法。压缩包共含4个核心.m文件总大小仅2KB轻量紧凑其中EstAssetValue.m负责企业资产价值估计KMVcompute.m实现违约距离与EDF计算主逻辑KMVOptSearch.m支持参数优化KMVfun.m提供分布拟合与辅助运算功能。已有131人下载学习代码结构清晰、模块职责明确可直接运行调试便于对照《KMV模型与Python实现详解》博文内容深入掌握资产波动率映射、破产边界设定及正态分布尾部概率求解等关键环节。虽为MATLAB版本但函数接口规范易于向PythonNumPy/Pandas迁移是开展信用风险建模实操与教学演示的实用参考基线代码。 如果有人给你一份KMV模型的Python源码你会从哪儿下手几天前我刚整理完一套kmv.rar压缩包里面正好是人大那个流传挺广的KMV代码核心函数叫EstAssetValue这套代码就是用Python做KMV模型的资产价值估计以及违约距离和违约概率的计算。今天我就把这份代码的完整逻辑、每个函数的实际作用、还有我在跑数据时踩过的坑一次性说清楚。这套代码解决的是信用风险量化中最经典的问题如何从上市公司的股权市场价值倒推出公司资产的市场价值和资产波动率。学过金融工程的都知道公司资产本身没有直接的市场价格KMV模型用期权定价的思路把股权看成以公司资产为标的的看涨期权从而反解出隐含的资产价值。代码适合两类人来参考一是写信用风险、违约概率方向毕业论文的学生二是做债券投研、信贷风控的从业者想自己把KMV指标算出来做分析。1. 为什么都在找KMV模型的EstAssetValue代码1.1 KMV模型的核心思路网上搜“KMV代码”的人特别多但大部分搜到的都是MATLAB版本Python版本的相对少一些而且质量参差不齐。KMV模型这个思路是穆迪KMV公司提出来的核心思想一句话就能概括公司的股权价值E可以看成是一个以公司资产价值V为标的、以负债账面价值D为执行价格的看涨期权。为什么能这么类比因为有限责任制度下股东的最大损失就是投入的股本但如果公司经营得好、资产价值远超负债股东能享受所有剩余收益。这种收益结构和买入一个看涨期权一模一样损失有限、收益无限。所以经典Merton模型把它写成Black-Scholes形式这是后续所有计算的理论根基。KMV模型在Merton模型基础之上做了些调整但最核心的计算步骤还是那两步第一步从股权市值E和股权波动率σ_E反推出资产价值V和资产波动率σ_V。第二步根据计算出的V和σ_V算出违约距离DD再映射成预期违约率EDF。第一步里最关键、也最容易写错的就是资产价值的求解因为理论上V和σ_V是两个未知数但只有两个方程而且方程是非线性的没法直接解析求解必须用数值迭代。这就是EstAssetValue这个函数存在的意义——它干的就是整个KMV计算链条里最硬核的一环。1.2 为什么Python版本值得细看人大的那份KMV源代码我印象里先是在一些金融论坛上流传开的后来被反复上传到各种网盘。很多人以为它是MATLAB源码其实里面有一份Python实现文件结构大致是这样kmv.rar ├── KMVPython.py # 主计算逻辑 ├── EstAssetValue.py # 资产价值估计核心函数 ├── data/ │ ├── stock_price.csv │ └── debt_data.csv └── README.txt用Python的好处很明显。首先数据预处理比MATLAB顺手Pandas处理股票行情、财务报表数据非常方便其次Python的SciPy库自带各种求根函数直接调fsolve就能解决非线性方程组的问题最后如果后续要做批量计算把几十家公司的数据放在DataFrame里循环跑就行效率高代码也容易维护。但那份源码也有问题——注释少、命名随意、对网上抄代码的人不友好。里面的EstAssetValue函数尤其绕用了嵌套函数加SciPy的fsolve如果没弄懂数学原理直接拿去跑数据大概率会遇到报错或者结果明显不合理的情况。这篇文章我把这个函数拆开揉碎把每一步的数学推导和代码对应关系都讲清楚。2. 核心代码解构EstAssetValue到底在算什么2.1 方程组怎么来的在展示代码之前得先把数学推导理清楚不然看代码只能靠猜。KMV模型要解的方程组是下面这个前提假设是公司资产价值服从几何布朗运动方程一来自Black-Scholes欧式看涨期权定价公式表示股权价值E和资产价值V的关系[ E V \cdot N(d_1) - D \cdot e^{-rT} \cdot N(d_2) ]方程二来自伊藤引理推导出的股权波动率与资产波动率的关系[ \sigma_E \frac{V}{E} \cdot N(d_1) \cdot \sigma_V ]其中[ d_1 \frac{\ln(V/D) (r \sigma_V^2 / 2) \cdot T}{\sigma_V \cdot \sqrt{T}} ][ d_2 d_1 - \sigma_V \cdot \sqrt{T} ]这里各个参数的含义分别是V是公司资产市场价值σ_V是资产价值波动率E是股权市场价值σ_E是股权价值波动率D是公司负债的账面价值r是无风险利率T是债务期限通常设为1年N是标准正态分布的累积分布函数。这个方程组里E、D、r、T、σ_E都是已知数V和σ_V是未知数。难点在于d₁本身也含V和σ_V所以方程组没有解析解必须用数值方法迭代逼近。2.2 代码逐行拆解人大的那份Python代码核心部分长这样我做了些整理和变量重命名方便阅读import numpy as np from scipy.stats import norm from scipy.optimize import fsolve def EstAssetValue(E, sigma_E, D, r0.03, T1.0, x0None): 用牛顿迭代法求资产价值V和资产波动率sigma_V。 参数 ---- E : float 股权市场价值通常用总股本 * 股票年均价 sigma_E : float 股权价值波动率通常用日收益率标准差年化 D : float 公司负债的账面价值短期负债 长期负债或者直接用总负债 r : float 无风险利率默认0.03可改成一年期国债收益率 T : float 债务期限默认1年 x0 : array_like, optional 迭代初值建议传[E, sigma_E]附近的值 返回 ---- V : float 公司资产市场价值 sigma_V : float 公司资产价值波动率 if x0 is None: x0 [E, sigma_E] def equations(x): V x[0] sigma_V x[1] d1 (np.log(V / D) (r 0.5 * sigma_V ** 2) * T) / (sigma_V * np.sqrt(T)) d2 d1 - sigma_V * np.sqrt(T) eq1 V * norm.cdf(d1) - D * np.exp(-r * T) * norm.cdf(d2) - E eq2 (V / E) * norm.cdf(d1) * sigma_V - sigma_E return [eq1, eq2] V, sigma_V fsolve(equations, x0) return V, sigma_V这份代码的逻辑不复杂但有几个细节容易被忽略。equations函数里计算d₁和d₂时用的是np.log(V / D)而不是np.log(V) - np.log(D)两者数学上等价但前者在V和D量级差异特别大时直接算比值可以避免精度丢失算是一个不错的代码习惯。我猜有人看到这里会问为什么初始值x0传的是[E, sigma_E]这是因为KMV模型已知的、最接近最终结果的值就是E和σ_E在绝大多数情况下资产价值会比股权价值高一点毕竟要覆盖负债资产波动率会比股权波动率低一些因为有负债的杠杆效应。所以从[E, sigma_E]出发做迭代收敛速度通常很快这算是行业里的常见做法不是凭空拍的。2.3fsolve和手写牛顿迭代怎么选人大的源码用的是SciPy的fsolve也就是对非线性方程组做迭代求解。还有一套常见实现是手动写牛顿迭代法很多教材都建议这么干。我个人的经验是如果只是单家公司的计算用fsolve完全够用省心省力但如果你要批量处理几十家公司跑滚动时间窗口那手动写一个带收敛控制的迭代器反而更可控因为fsolve内部的一些细节比如雅可比矩阵的数值逼近方法在极端数据下偶尔会抽风。手动牛顿迭代的核心思路是这样的把方程组F(x) 0在初始点附近做一阶泰勒展开然后用迭代公式x(k1) x(k) - J⁻¹(x(k)) · F(x(k))去逼近零点其中J是雅可比矩阵。这个逻辑用代码写的话大概是下面这个风格def newton_solve(E, sigma_E, D, r, T, tol1e-6, max_iter100): V E * 1.2 sigma_V sigma_E * 0.8 def F(x): V, sigma_V x d1 (np.log(V / D) (r 0.5 * sigma_V ** 2) * T) / (sigma_V * np.sqrt(T)) d2 d1 - sigma_V * np.sqrt(T) eq1 V * norm.cdf(d1) - D * np.exp(-r * T) * norm.cdf(d2) - E eq2 (V / E) * norm.cdf(d1) * sigma_V - sigma_E return np.array([eq1, eq2]) for i in range(max_iter): F_val F([V, sigma_V]) h 1e-5 J np.zeros((2, 2)) for j in range(2): x_plus [V, sigma_V] x_minus [V, sigma_V] if j 0: x_plus[0] h x_minus[0] - h else: x_plus[1] h x_minus[1] - h J[:, j] (F(x_plus) - F(x_minus)) / (2 * h) delta np.linalg.solve(J, -F_val) V delta[0] sigma_V delta[1] if np.linalg.norm(delta) tol: break return V, sigma_V这份手动实现里有个细节值得注意就是数值微分步长h的选取。我用的是1e-5这个值是个经验取值太大导数近似不准太小会因为浮点数舍入误差引发问题。如果你处理的数值量级特别大比如亿级别的资产可以把h也相应调整一般来说取1e-5 * max(1.0, abs(x))这种相对步长更稳妥。3. 实操全过程从数据到违约概率的完整计算3.1 数据准备在算EstAssetValue之前你得先把三个关键输入准备好股权市场价值E、股权价值波动率σ_E、公司负债账面价值D。股权市场价值E的计算公式不复杂总股本乘以股票年平均收盘价。如果该公司有A股和H股或者有多个证券交易所上市的股票处理起来会很麻烦但大多数A股公司只在一个市场上市直接用E share_total * avg_close_price股权波动率σ_E的计算最常用的方法是用历史收益率的标准差做年化。假设你有一整年的日线收盘价数据先计算每日简单收益率或者对数收益率然后取标准差再乘以根号252一年大概是252个交易日import pandas as pd df pd.read_csv(stock_price.csv, parse_dates[date]) df[ret] df[close].pct_change() sigma_daily df[ret].std() sigma_E sigma_daily * np.sqrt(252)用对数收益率还是简单收益率我自己做的时候习惯用对数收益率因为它在数学性质上更好而且能避免一天涨跌停导致的非对称性。但如果是做教材案例复现用简单收益率也能对上结果差别通常在万分位级别。负债账面价值D这里有一个很关键的细节KMV模型在考虑违约点时用的不是总负债而是短期负债加上长期负债的一半。但EstAssetValue这个函数虽然理论上只需要一个D值在嵌套方程组里你输入的D会影响最终的V和σ_V计算结果。所以一般来说D值取多少要先想清楚你的目的如果你想计算资产价值的动态变化D取总负债相对合理。如果你想计算违约距离那D其实对应的是违约点DP应该用短期负债 0.5 * 长期负债。算违约距离的时候通常是先解出V和σ_V再用违约点DP去算DD所以严格来说EstAssetValue的D参数应该传总负债或者长期债务的结构信息。但为了跟主流文献保持一致很多人直接用短期负债加一半长期负债作为D传给函数这样后面算DD时直接套公式整体结果和经典论文对得上。3.2 从资产价值到违约距离和违约概率当EstAssetValue返回了V和σ_V你已经完成了KMV模型中最难的部分。接下来算违约距离DD和预期违约率EDF就是纯粹的公式套用[ DD \frac{V - DP}{V \cdot \sigma_V} ][ EDF N(-DD) ]这里N是标准正态分布的累积分布函数scipy.stats.norm.cdf(-DD)直接就能算。我自己在复现某篇论文时用了一家制造业上市公司三年的数据分析得到过一组比较直观的数字大概的中间过程给你参考一下数值做了脱敏处理指标第一年第二年第三年股权市值E亿元58.3062.8055.10股权波动率σ_E0.420.380.51总负债D亿元32.5035.2040.80资产价值V亿元82.6488.3584.72资产波动率σ_V0.290.270.34违约距离DD1.922.231.26看到这个结果你就能直观理解KMV模型的价值了第三年这家公司的违约距离明显变小说明信用风险在上升而单纯的负债率指标其实看不出这么大的变化因为负债率的变化幅度远没有DD的降幅来得敏感。这也正是KMV模型比单纯的杠杆比率更有信息量的原因——它同时考虑了资产价值、波动率和负债结构三个维度。3.3 批量计算的工程化写法只算一家公司很简单但做论文或者实际风控场景面对的是几十上百家公司这时就得用循环加上DataFrame结构来批量处理。我的建议是先把所有公司的参数整理成一个表每行代表一家公司一个时间窗口的输入参数然后用类似下面这段代码批量计算import pandas as pd from tqdm import tqdm # params_df 包含列company, E, sigma_E, D results [] for _, row in tqdm(params_df.iterrows(), totallen(params_df)): V, sigma_V EstAssetValue( Erow[E], sigma_Erow[sigma_E], Drow[D], r0.03, T1.0 ) DP row[short_debt] 0.5 * row[long_debt] DD (V - DP) / (V * sigma_V) EDF norm.cdf(-DD) results.append({ company: row[company], V: V, sigma_V: sigma_V, DD: DD, EDF: EDF }) result_df pd.DataFrame(results)这里用tqdm显示进度条是个人习惯数据量大的时候能直观看到跑了多少。scipy.optimize.fsolve对同一份数据的计算时间大概在毫秒级所以即便有几百家公司跑完也就是几十秒的事性能压力不在计算本身反而在数据清洗那一环。4. 常见问题与排查技巧实录4.1 迭代不收敛或者结果异常fsolve虽然好用但偶发不收敛的情况是真实存在的。我遇到过的典型场景是负债率特别高的公司比如总负债是股权市值三倍以上的。这种情况下一开始如果用默认初始值[E, sigma_E]去迭代很容易陷入局部振荡最终算出V为负值或者σ_V趋近于0这种明显不合理的解。排查思路是先看初始值合不合理。如果E是20亿但D是200亿资产价值V理论上应该远超股权价值初始值V20亿显然太离谱。这时手动调整初始值为[E D, sigma_E]或者[E * 1.5, sigma_E * 0.9]往往能解决问题。这个调整思路不是拍脑袋而是因为企业的资产至少得覆盖债务否则资不抵债这个事实本身就是重要的风险信号初始值太偏反而让数值算法无所适从。另一个更隐蔽的问题是D的输入值是0或者E为0尤其是在批量处理时遇到数据缺失没有清洗干净np.log(V / D)这里直接除零或者对负数取对数报错信息会带出RuntimeWarning但代码不一定立刻中断。所以我在实际代码里都会加一个最基础的参数校验先判断输入的正负性和非零性再进入迭代求解的过程。4.2 股权波动率计算上的坑波动率是KMV模型里最能影响结果的参数之一甚至比负债数据的影响还大。我见过好几个人用同样的实证数据但算出来的DD差别很大逐项检查后发现是波动率的口径不一致有的用简单收益率有的用对数收益率有的用样本标准差有的用总体标准差还有的用252个交易日年化有的却用了365。这些细节导致的差异在单家公司上可能不明显但放在模型对比或论文复现里就是致命的误差来源。我的判断标准很简单样本标准差默认ddof1加上对数收益率再加上252天年化。这个组合在金融实证里是主流做法跟大多数已发表论文的口径一致。如果实在要跟某个具体论文对比先确认对方用的哪种口径再说别直接拿结果硬碰硬。4.3 负债数据的时间对齐问题财务报表里的负债数据是季度或者年度的而股价是日频的这就存在一个频率匹配的问题。最常见的处理方式有两种一是用最新一期的负债数据作为当期D二是用过去四个季度的滚动平均负债。后者更平滑但前者更灵敏。我个人的习惯是算年度窗口的KMV指标时用上年末或者最近年报季报的负债数据。比如我要算2023年全年每季度的KMV那每个季度的D值就取该季度之前最近一次披露的负债数据。这样可以做到向前看偏差最小比较接近真实违约预测场景的信息环境和决策时序。4.4 手动迭代和fsolve结果不一致如果同时实现了fsolve版本和手写牛顿迭代版本你会发现两个版本算出来的V和σ_V可能在小数点后四五位有细微差异。这很正常因为数值算法的终止条件不同fsolve的容差设置和手写的tol1e-6不完全一致收敛路径也不一样。关键是要分清哪个更可信。我的经验是如果两个版本结果差异在0.1%以内都不用担心这是数值误差的正常范围如果差异超过1%那就要检查是不是初始值差异太大导致算法收敛到了不同局部解。虽然KMV方程组理论上是单调的正常情况只有唯一解但在极端参数附近还是可能出现数值病态。4.5 关于违约概率映射的说明严格意义的穆迪KMV并不会直接用N(-DD)算EDF而是有一个基于历史违约数据的大样本映射表把DD映射到实际违约频率上。但这份人大的源代码里没有映射表它默认用正态分布来近似。对论文而言N(-DD)是可以接受的近似很多学术文献也这么处理但在商业风控场景最好结合行业历史违约数据做校准否则算出来的EDF绝对值没有直接参考意义只能看相对变化趋势来排序。做课题或者论文的时候其实经常遇到“代码跑通了但不知道结果对不对”的困扰。我的建议是拿一家真实公司去对照公开的评级变化或者已知的信用事件。比如某家公司历史上出现过债券违约或者评级大幅下调你用它的财务数据和股价数据跑KMV应该能看到违约距离在事件发生前明显下降。如果连这个事实都拟合不出来那大概率是某个输入参数出了问题而不是模型的锅。5. 代码之外的几点建议kmv.rar这套代码网上还有所谓“代做KMV”的需求说白了就是很多同学写论文时在模型和代码这一环卡住了。但以我带团队和帮人审代码的经验来看KMV模型本身没那么神秘Python实现的核心也就一百来行真正的功夫都花在数据清洗、参数口径选择和结果合理性检验上。如果你只是拿代码交作业不用太纠结算法的微优化把EstAssetValue的逻辑完整跑通再配上一两个公司案例的结果已经足够说明问题。但如果你想真正把KMV用到实际的债券分析或者信贷策略里我建议在现有代码基础上拓展三件事第一把单期静态的KMV改成滚动时间窗口的动态KMV这样可以看到违约风险的时序变化。实现上就是按季度或者按月滚动计算每次用最近一年的股价数据和最新的负债数据。第二加入行业对比视角。同一家公司的DD绝对值没有参照系很难判断高低但你把它和同行业的可比公司放在一个截面里观察分位数就能清楚知道它在行业里处于什么风险水平。第三做好参数敏感性分析。r取值是2%还是4%、T是0.75还是1.25、D是总负债还是短期负债加一半长期负债这些都会影响最终DD的结果。写论文时做一张敏感性分析表既能说明你理解了参数的经济学含义也能让审稿人或者答辩老师觉得论证扎实。这一点在学术场景下尤其加分。EstAssetValue这个函数名拆开来看就是“Estimate Asset Value”——估计资产价值它代表的是整个KMV计算链条的起点和枢纽。把这个函数吃透了后面的一切都是水到渠成。拿着这份源码别急着跑数先把方程组写纸上推一遍再对照代码里的每一行看它在算什么然后动手替换成自己的数据跑一遍整个过程走完你才算真正把KMV握在了手里。本文还有配套的精品资源点击获取