
简介一份基于Matlab的资本资产定价模型CAPM估计程序源码适合金融工程、投资学、计量经济学等课程的学习者也方便入门量化研究者理解贝塔系数的计算思路。资源以“估计资本资产定价模型”为核心在单个.m文件中完整呈现从数据读取、时间序列整理到回归估计的关键步骤可依据市场收益率与资产收益率的历史数据计算系统性风险贝塔、截距项、拟合优度等统计量也能用于单个资产或投资组合的实证检验为资产定价、风险度量和组合构建提供基础工具。程序结构紧凑代码注释清晰使用者可根据实际数据简单调整参数即可运行还能对照教学案例进行二次开发和功能扩展。通过动手运行不仅能加深对资本资产定价理论的理解也能直接用于实证分析中的关键参数估计。压缩包内仅1个M文件包体约1KB轻量小巧便于直接下载、阅读和修改。已有35人学习过本程序适合课程作业、期末设计、论文实证或课题预研阶段的快速起步与参考借鉴。1. 为什么在Matlab里“估计资本资产定价”不是一句命令就能交差的事“估计资本资产定价”这个题目在金融计量里听着非常基础拿个股超额收益对市场超额收益做一次最小二乘回归截距是alpha斜率是beta完事。但真正落到Matlab里写一个有复用价值的估计程序时你会发现麻烦全在数据准备和统计推断的细节里交易日历对不齐、无风险利率的年化转日频方式不统一、回归残差存在自相关时t统计量不可信、滚动窗口长度怎么选才能保证Beta估计不等于白噪音。很多下载下来的源码包直接把regress的结果当作最终答案这让使用者误以为CAPM估计就是几行代码。这篇文章会把从数据整理、OLS回归、HAC修正到滚动Beta的完整路径用Matlab代码走一遍并给出验证与封装建议让拿到一个.rar源码包的人也能自己判断里面的程序靠不靠谱。如果你准备用Matlab处理股票收益率、估计资产定价模型或者想把零散的回归代码改造成可用于投研的脚本下面这些步骤正好覆盖了从易错点到边界情况的全部常见问题。2. 用Matlab整理出能直接回归的资本资产定价面板数据2.1 把价格序列转成超额收益CAPM的第一个常见错误源资本资产定价模型的回归方程是R_i - R_f alpha beta * (R_m - R_f) epsilon很多人第一步就错在只算了价格的变化率忘记扣无风险利率。这里的R_i和R_m必须和R_f处于同一频率。如果手里是日频价格而搜集到的年度无风险利率是4%直接当常数减进日频数据里beta不会有太大偏差但alpha会严重偏离真实值。我一般建议在数据进入回归之前先用一个统一函数把原始价格处理成标准的超额收益矩阵。下面这段代码用了财务数据、市场指数和无风险利率三个输入输出对齐后的超额收益function [exRet, exMkt, dates] prepCapexRet(P, Mkt, Rf, rfFreq) % P: T x N 个股收盘价矩阵, 每列一支股票, 第一行为最早日期 % Mkt: T x 1 市场指数收盘价 % Rf: 无风险利率序列, 长度为 T 或标量 % rfFreq: daily 表示已按日给出, annual 表示年化利率 T size(P, 1); simpleRet P(2:end, :) ./ P(1:end-1, :) - 1; mktSimple Mkt(2:end) ./ Mkt(1:end-1) - 1; if strcmp(rfFreq, annual) rfDaily (1 Rf).^(1/252) - 1; % 年化复利转日频 else rfDaily Rf(2:end); % 已给出每日值注意对齐 end exRet simpleRet - rfDaily; exMkt mktSimple - rfDaily; dates (2:T); % 损失第一行时间戳返回对应索引 end参数说明P和Mkt必须是按交易日对齐的若两者有缺失日期不要直接进入回归。rfFreq为annual时我优先用复利换算而不是简单除以252这能避免高频数据下的利率近似失真如果无风险利率以年化单利提供并且思考一下通常金融数据库的惯例Rf/252也可以接受但要在结果里注明采用哪种约定。代码返回的exRet是第一支个股的超额收益exMkt是市场超额收益dates只是原始索引并不是真正的datetime对象想要日期刻度可在这个函数外层做一次映射。2.2 交易日历对齐用索引而不是findMatlab里find或ismember在数据量小时顺手但对于二十年日频数据逐个匹配日期会让程序慢一个数量级。常见的做法是先将两个数据源的日期都转化为datetime数组再用intersect得到对齐位置。这里有一个容易被忽略的细节不同数据源即使同为交易日个别日期可能因为交易所临时休市而缺失所以不能假设两张表行数相同。datesStock datetime(priceDatesStr, InputFormat, yyyy-MM-dd); datesMkt datetime(mktDatesStr, InputFormat, yyyy-MM-dd); [commonIdx, ia, ib] intersect(datesStock, datesMkt, stable); P_aligned P(ia, :); Mkt_aligned Mkt(ib, :);用stable确保ia和ib按时间升序排列后面计算二次收益率时才不会打乱时点顺序。注意intersect返回的ia是P中的行位置ib是Mkt中的行位置两者不能互换。如果还要保留一个时间轴变量建议把commonIdx作为主键而不是重新用find去原表里搜一遍。2.3 处理缺失值CAPM回归里最容易被忽略的差距价格序列里偶尔会有NaN比如停牌或者数据供应商没有输出。regress默认遇到NaN会直接报错所以要先决定是删掉整行还是填充。我的做法是如果某一天超过30%的股票都缺失就删掉这一天否则对个别缺失值做前向填充也就是沿用上一个交易日的价格。原因在于CAPM估计用的是所有股票的同期超额收益中间某只股票缺失一天但其他股票正常交易直接用rmmissing会把一条完整横截面削掉导致整个回归样本点减少过多。缺失情况处理方式后果与适用场景单只股票单日缺失前向填充适合停牌一天或流动性较差的标的同日多只股票缺失删除该日全部横截面样本防止填充过多造成收益率出现伪周期尾部缺失截断尾部避免用最后几天的数据凑样本量填充时要注意前向填充后连续日收益率会出现零值这会让该股票在回归里的波动率被低估所以填充比例超过5%的股票应该单独标记不要混入最终回归池。这部分逻辑虽然琐碎但直接影响alpha和beta估计值。3. 在Matlab中实现资本资产定价的OLS估计与Newey-West调整3.1 用fitlm还是regress两者的alpha输出差异看到这里你已经有了对齐后的超额收益矩阵。最直接的回归命令是regress或fitlm。两者在OLS系数上一致但输出结构体不同。regress要求手动拼接常数项而fitlm自动加入截距这正好对应CAPM方程里那个需要被检验是否为0的alpha。exRet_i exRet(:, 1); % 选取第一支股票 X exMkt; % 市场超额收益 mdl fitlm(X, exRet_i, Intercept, true); alpha_i mdl.Coefficients.Estimate(1); beta_i mdl.Coefficients.Estimate(2);fitlm返回的mdl里Coefficients依次是截距和斜率mdl.NumObservations会提醒你实际参与回归的样本量。这里建议不要在参数里写Intercept, true因为fitlm默认就是带截距的写了反而让人觉得你可能纠结过。真正要关注的是mdl.RMSE和残差向量mdl.Residuals.Raw它们在后面的自相关修正中直接派上用场。如果你的环境中安装了优化工具箱也可以用lsqlin做带约束估计比如限制beta必须大于0不过CAPM本来就不该约束符号所以大多数时候fitlm足够。3.2 把回归结果整理成参数表而不是零散变量金融数据里你不只要看一支股票的beta。把几十支股票的回归结果放在同一个表格里才能横向比较。用一个循环把每支股票过一遍fitlm把系数和标准误装进tablenStock size(exRet, 2); coeffTable table(Size, [nStock, 4], ... VariableTypes, {double,double,double,double}, ... VariableNames, {Alpha,Beta,AlphaT,BetaT}); for i 1:nStock mdl_i fitlm(exMkt, exRet(:, i)); se_i mdl_i.Coefficients.SE; coeffTable.Alpha(i) mdl_i.Coefficients.Estimate(1); coeffTable.Beta(i) mdl_i.Coefficients.Estimate(2); coeffTable.AlphaT(i) mdl_i.Coefficients.Estimate(1) / se_i(1); coeffTable.BetaT(i) mdl_i.Coefficients.Estimate(2) / se_i(2); endfitlm自带的tStat字段直接就是t统计量不用手动除。但要注意Coefficients里行的顺序是截距, 斜率如果哪天真有不带常数项的回归这个顺序会变。把结果放进表格的好处是后续可以直接writetable输出到CSV或者在Matlab的变量查看器里筛选abs(AlphaT) 2的股票。3.3 给CAPM回归的t统计量做Newey-West调整CAPM用月度数据回归还好一旦用日频数据残差的自相关几乎必然存在。fitlm的标准误是普通OLS标准误它假设残差独立这会让beta的t统计量虚高。经济学工具箱里有现成的hac函数但很多人下载的源码包里未必调用它。这里给一个手动计算的替代方案T length(exMkt); e mdl.Residuals.Raw; X [ones(T,1), exMkt]; degree floor(4 * (T/100)^(2/9)); % Newey-West自动滞后阶数 weights 1 - (0:degree) ./ (degree 1); s zeros(2, 2); for lag 0:degree XLag X(1lag:end, :); eLag e(1lag:end); Xe (XLag .* eLag); if lag 0 s (Xe * X(1:end-lag, :) / T) * T; else Gamma (X(1lag:end,:) .* e(1lag:end)) * X(1:end-lag,:) / T; s s weights(lag1) * (Gamma Gamma); end end S T * (X * X / T)^(-1) * s * (X * X / T)^(-1) / T; betaSE_HAC sqrt(diag(S) / T);这段代码参照了Newey-West异方差自相关一致协方差矩阵的朴素实现。参数degree的选取公式4*(T/100)^(2/9)是经验法则如果样本量为500滞后阶数大约是6比固定用5更自适应。每个lag都要把样本整体后移注意向量长度的对齐1lag:end和1:end-lag的长度是完全相同的。计算完成后betaSE_HAC(2)就是经过自相关修正的beta标准误用它替换fitlm给出的标准差CAPM估计的结果才更值得写进研报。4. 滚动窗口下的资本资产定价Beta估计与置信区间4.1 为什么要放弃固定Beta事件期与时变暴露上一章的OLS估计给了整个样本期的平均alpha和beta但这个平均值的意义在样本跨度过长时要打折扣。股票的风险暴露会随公司杠杆、行业周期和市场情绪变化一个三年的平均beta可能既不属于第一年也不属于第三年。尤其是做事件研究时政策出台前后各90天的beta差异非常明显。这时滚动窗口估计才是常态。滚动窗口的窗口长度选择是关键太短比如60天beta估计的方差大到失去参考意义太长比如750天又无法捕捉快速变化。我一般用252个交易日作为标准一年窗口对波动率敏感的监控用126个交易日。4.2 滚动回归用for循环也能写得干净利落下面的代码对单只股票计算滚动beta保存每个窗口的最后一天作为该beta的日期window 252; nObs length(exMkt); rollingBeta NaN(nObs, 1); rollingAlpha NaN(nObs, 1); for t window:nObs wBeta exMkt(t-window1 : t); wRet exRet(t-window1 : t, 1); if any(isnan(wBeta)) || any(isnan(wRet)) continue; % 窗口内数据缺失则放弃该点 end b [ones(window,1), wBeta] \ wRet; rollingBeta(t) b(2); rollingAlpha(t) b(1); end plot(dates(1:nObs-1), rollingBeta);这段代码没有用sprintf之类的去造轮子直接用\解最小二乘比fitlm在循环里更快。wBeta和wRet必须是列向量因为ones(window,1)已经规定了列方向。窗口滑到缺失数据时直接continue但这会让returns里对应日期变成NaN绘图时会断开。如果你不希望图断线可以改为用上一个有效值填充不过要明确这用来画图可以用来计算就不严谨。4.3 分块自助法给Beta置信区间比OLS标准误更可信滚动beta在窗口重叠时存在强序列相关直接用OLS标准误构造置信带是错误的。常见做法是用分块自助法把重叠窗口形成的beta序列分成不重叠的块用重采样分布代替正态近似。blockLen 22; % 约一个月交易日块长 numBlocks ceil((nObs - window 1) / blockLen); betaSeq rollingBeta(window:end); alphaSeq rollingAlpha(window:end); bootBeta zeros(1000, 1); rng(3); for boot 1:1000 blkIdx randi(numBlocks, numBlocks, 1); sample []; for b 1:numBlocks startIdx (blkIdx(b)-1)*blockLen 1; endIdx min(startIdx blockLen - 1, length(betaSeq)); sample [sample; betaSeq(startIdx:endIdx)]; end bootBeta(boot) mean(sample); end ciBeta quantile(bootBeta, [0.025, 0.975]);这里选择块长为22是为了保留一个月的日内相关性如果市场存在周度效应也可以试试blockLen5。rng(3)保证结果可重现这比不设随机种子更负责任。quantile给出的置信区间不需要假设beta分布对称在样本量有限时比mean±1.96*std更能反映重尾特征。5. 把资本资产定价估计程序变成可复用的源码验证、函数封装和绘图5.1 用模拟数据验证你的CAPM估计程序没有系统性偏误拿到网上下载的源码包第一件事不是换股票数据跑结果而是先用已知参数的人工数据验证。用模拟数据构造一个真实的alpha0.001、beta1.5的市场超额收益序列跑一遍你的估计流程看看能不能回收这些参数rng(42); nSim 500; mktSim randn(nSim, 1) * 0.02; trueAlpha 0.001; trueBeta 1.5; stockSim trueAlpha trueBeta * mktSim randn(nSim, 1) * 0.01; % 对模拟序列做一次CAPM回归 mdlSim fitlm(mktSim, stockSim); disp(mdlSim.Coefficients.Estimate);如果回归得到的alpha和beta与真实值的差超过千分之一那么问题多半出在前面某步数据的对齐或超额收益转化上。这种验证方式比拿真实数据看R^2更能定位错误因为它有完全可控的真相值。模拟验证通过后再替换成真实价格数据才有底气。5.2 推荐函数签名与返回值设计把零散的脚本整理成函数是源码能落地到其他项目的前提。下面这个签名直接把主题里的资本资产定价估计浓缩成一个入口function results estimateCAPM(price, mkt, rf, freq, method) % price: T x N 价格矩阵 % mkt: 市场指数价格序列 % rf: 无风险利率 % freq: daily 或 weekly % method: OLS 或 HAC % results.alpha, results.beta, results.tStats, results.cov endresults里用结构体返回比返回多个分散变量更不易出错。method参数用于切换是否调用Newey-West修正这样同一个函数既能做快速扫描也能做严谨检验。常见的源码分享里很多人喜欢把figures也作为输出参数但我不建议因为绘图依赖用别人的显示器上下文应该在本函数里绘图并直接保存到文件。5.3 在散点图上画回归线同时满足检验和汇报最后落实到可视化把个股超额收益和市场超额收益画成散点叠加回归线再把原点处的截距是否显著标出来。scatter(exMkt, exRet(:,1), 10, filled, MarkerFaceAlpha, 0.4); hold on; xFit linspace(min(exMkt), max(exMkt), 100); yFit alpha_i beta_i * xFit; plot(xFit, yFit, r-, LineWidth, 2); xlabel(Market Excess Return); ylabel(Stock Excess Return); title(sprintf(CAPM Fit: alpha%.4f, beta%.2f, alpha_i, beta_i)); grid on;这里的sprintf把alpha和beta直接放进标题比单独开一行的text更清晰。MarkerFaceAlpha降低透明度能看出散点的密度分布对大数据量尤其有效。如果想再画证券市场线就把横轴改为若干组合的beta纵轴改为其平均超额收益市场组合通常标记在市场beta1的点上。把窗口长度和置信水平作为输入参数传进去这套程序就能继续用于后续的风险分析而不必每次改代码。本文还有配套的精品资源点击获取