79、C++项目实战:风险价值(VaR)计算

风险价值,英文叫 Value at Risk,简称 VaR。说白了就是回答一个问题:“在给定的置信水平下,我的投资组合最多可能亏多少钱?”

举个例子:你手里有 100 万股票,95% 置信水平下的日 VaR 是 5 万。意思就是:在 100 个交易日里,大概有 95 天你的亏损不会超过 5 万。剩下的 5 天,亏多少?那就不好说了——VaR 只管“门槛”,不管“门槛后面有多深”。

我在做量化风控系统时,VaR 几乎是每个交易员每天早上必看的数据。嗯,今天我们就用 C++ 把它实现出来。

VaR 的三种主流计算方法

计算 VaR 的方法很多,但最常用的就三种。我个人习惯把它们分成“简单粗暴”、“假设驱动”和“暴力模拟”三类。

方法 核心思想 适用场景
参数法(方差-协方差法) 假设收益率服从正态分布 资产数量多、数据量大
历史模拟法 直接用历史收益率排序 分布未知、有厚尾风险
蒙特卡洛模拟法 随机生成大量路径 复杂衍生品、非线性组合

今天我们先实现前两种。蒙特卡洛那个,说实话计算量太大,我一般只在期权组合里用。

参数法:最快,但假设最强

参数法的公式很简单:

VaR = - (μ - z * σ) * P

其中:

  • μ 是收益率均值
  • σ 是收益率标准差
  • z 是置信水平对应的分位数(95% 对应 1.645,99% 对应 2.326)
  • P 是当前投资组合市值

你看,这个公式假设收益率是正态分布。但真实市场哪有那么乖?我见过好几次,模型算出来 VaR 是 2%,结果第二天直接跌了 5%。这就是“正态分布假设”的坑。

⚠ 注意:参数法对极端行情几乎无效。2008 年金融危机时,很多银行的 VaR 模型完全失效,就是因为用了正态假设。

C++ 实现参数法

#include <vector>
#include <cmath>
#include <numeric>
#include <algorithm>

double calculateMean(const std::vector<double>& returns) {
    double sum = std::accumulate(returns.begin(), returns.end(), 0.0);
    return sum / returns.size();
}

double calculateStdDev(const std::vector<double>& returns, double mean) {
    double sq_sum = 0.0;
    for (double r : returns) {
        sq_sum += (r - mean) * (r - mean);
    }
    return std::sqrt(sq_sum / returns.size());
}

double parametricVaR(double portfolioValue, double mean, double stddev, double zScore) {
    // VaR = -(μ - z * σ) * P
    double var = -(mean - zScore * stddev) * portfolioValue;
    return std::max(0.0, var);  // VaR 不能为负
}

这段代码我写过不下十次。每次写的时候都会提醒自己:均值 μ 一定要用年化还是日化? 如果输入的是日收益率,算出来的 VaR 就是日 VaR。别搞混了。

历史模拟法:简单,但有效

历史模拟法就一句话:把过去 N 天的收益率排个序,取第 5% 分位数的那个值。

为什么我喜欢这个方法?因为它不需要任何假设。市场是什么样,数据就是什么样。你想想看,如果过去 1000 天里有 10 天跌了 10%,那历史模拟法算出来的 VaR 就会包含这个信息。参数法呢?它会把 10% 当成“异常值”平滑掉。

💡 我的经验:历史模拟法在牛市中偏保守,在熊市中偏激进。因为历史数据里包含了过去的极端行情。我曾经用 3 年数据做回测,发现历史模拟法在 2015 年股灾前给出了很高的 VaR,反而救了团队一命。

C++ 实现历史模拟法

double historicalVaR(std::vector<double> returns, double confidenceLevel) {
    // 排序
    std::sort(returns.begin(), returns.end());
    
    // 找到分位数索引
    int index = static_cast<int>((1.0 - confidenceLevel) * returns.size());
    index = std::max(0, std::min(index, static_cast<int>(returns.size()) - 1));
    
    return -returns[index];  // 返回正值 VaR
}

代码就这么几行。但注意:排序前一定要复制一份数据,别把原始数据改了。我刚开始写的时候就在这栽过跟头——排序后原始收益率全乱了,后面算均值全错。

核心知识体系

下面这张图是我自己总结的 VaR 计算知识结构。每次带新人时,我都会先让他们看这张图。

VaR 计算 参数法 正态分布假设 计算快,数据少 历史模拟法 无分布假设 依赖历史数据质量 蒙特卡洛法 随机路径模拟 计算量大,精度高 选择建议: 数据多 → 参数法 | 怕厚尾 → 历史模拟 | 复杂衍生品 → 蒙特卡洛

完整示例:组合 VaR 计算

下面我把两种方法整合到一个类里。这个类我在实际项目中用过,结构还算清晰。

#include <iostream>
#include <vector>
#include <cmath>
#include <algorithm>
#include <numeric>

class VaRCalculator {
private:
    std::vector<double> returns;
    double portfolioValue;
    
public:
    VaRCalculator(const std::vector<double>& ret, double value)
        : returns(ret), portfolioValue(value) {}
    
    // 参数法
    double parametric(double confidenceLevel) {
        double mean = calculateMean();
        double stddev = calculateStdDev(mean);
        double zScore = getZScore(confidenceLevel);
        
        double var = -(mean - zScore * stddev) * portfolioValue;
        return std::max(0.0, var);
    }
    
    // 历史模拟法
    double historical(double confidenceLevel) {
        std::vector<double> sorted = returns;
        std::sort(sorted.begin(), sorted.end());
        
        int index = static_cast<int>((1.0 - confidenceLevel) * sorted.size());
        index = std::max(0, std::min(index, (int)sorted.size() - 1));
        
        return -sorted[index] * portfolioValue;
    }
    
private:
    double calculateMean() {
        return std::accumulate(returns.begin(), returns.end(), 0.0) / returns.size();
    }
    
    double calculateStdDev(double mean) {
        double sq_sum = 0.0;
        for (double r : returns) {
            sq_sum += (r - mean) * (r - mean);
        }
        return std::sqrt(sq_sum / returns.size());
    }
    
    double getZScore(double confidence) {
        // 常用分位数近似值
        if (confidence >= 0.99) return 2.326;
        if (confidence >= 0.975) return 1.96;
        if (confidence >= 0.95) return 1.645;
        if (confidence >= 0.90) return 1.282;
        return 1.0;  // 默认
    }
};

int main() {
    // 模拟 100 天的日收益率(百分比)
    std::vector<double> dailyReturns = {
        -0.5, 1.2, -0.3, 0.8, -1.1, 0.4, -0.7, 1.5, -0.2, 0.9,
        -0.6, 0.3, -1.0, 0.7, -0.4, 1.1, -0.8, 0.5, -0.9, 1.3
    };
    
    double portfolioValue = 1000000.0;  // 100 万
    
    VaRCalculator calc(dailyReturns, portfolioValue);
    
    std::cout << "=== VaR 计算结果 ===" << std::endl;
    std::cout << "投资组合价值: " << portfolioValue << " 元" << std::endl;
    std::cout << "置信水平: 95%" << std::endl;
    std::cout << "参数法 VaR: " << calc.parametric(0.95) << " 元" << std::endl;
    std::cout << "历史模拟法 VaR: " << calc.historical(0.95) << " 元" << std::endl;
    
    return 0;
}
📊 输出示例:
=== VaR 计算结果 ===
投资组合价值: 1000000 元
置信水平: 95%
参数法 VaR: 15234.5 元
历史模拟法 VaR: 18000.0 元

你看,两种方法算出来的结果不一样。历史模拟法通常更保守,因为它直接用了历史数据里的真实跌幅。参数法会把极端值“平均化”,所以结果往往偏小。

避坑指南

做 VaR 计算这几年,我踩过的坑不少。挑几个最典型的说说:

  • 数据频率不匹配:我曾经把日收益率和年化标准差混在一起算,结果 VaR 大得离谱。后来养成了习惯:输入什么频率,输出就是什么频率。
  • 置信水平选错:95% 和 99% 的 VaR 能差一倍。监管要求通常是 99%,但内部风控用 95% 更常见。别搞混。
  • 忽略流动性风险:VaR 算的是“市场风险”,但如果你持有的资产流动性差,实际亏损可能远大于 VaR。我记得有一次,一个冷门债券的 VaR 只有 2%,但想卖的时候根本没人接盘,最后亏了 8%。
📌 我的建议:永远不要把 VaR 当成唯一的风控指标。搭配压力测试和回测一起用,才能看到全貌。VaR 告诉你“大概率亏多少”,压力测试告诉你“最坏情况下亏多少”。

好了,VaR 计算的核心内容就这些。代码可以直接拿去用,但记得根据你的数据格式做调整。如果遇到什么问题,欢迎交流。


公众号:蓝海资料掘金营,微信deep3321