投资组合、风险、优化与模拟

本指南覆盖共享同一套失败模型的四个领域:它们拥有自己的输入,报告发生了什么而不是返回看起来合理的数字,并在头文件里写明前置条件。API 参考给出确切字段;本页说明如何把它们配合使用。

组合输入拥有自己的数据

portfolio_inputs 在接收时就复制传入的帧,因此之后修改调用方的帧永远不会影响报表。materialize() 每次调用都返回新帧:修改结果不会影响下一次调用。

四个映射键是固定的 —— returns、positions、transactions 与 benchmark_returns。“未提供”和“提供了但为空”是两种状态:前者物化为没有键,后者物化为一个持有空帧的键。

持仓是面向行的,不是按日期键控的:两行可以共享同一个时间戳,调用方的行顺序被保留。现金列默认名为 cash,计入配置分母,并从逐资产输出列与资产计数中排除。

持仓、交易与容量

领域

值得了解的行为

往返交易

按带符号价格做 FIFO;同一方向下同一块可以平仓后重新开仓

换手

使用逐日账面;没有前一日状态的第一天按一半计算

容量

清仓天数综合持仓规模、滚动平均成交量与最紧的单资产限制;并列时先取最早日期,再取第一个资产

交易帧

每日名义金额累加绝对交易值,因此同一天的买入与卖出相互叠加而不是抵消;交易日数统计的是不同日期

这些是记账规则而不是偏好:只有两侧使用同一约定,同一批交易构建的报表才会一致。

风险估计

forecast_var 与 forecast_es 返回 risk_estimate,在 losses_negative 符号约定下,其 estimate 对亏损为负。符号约定随结果一起返回,消费方无需猜测。

fincore::risk_forecast_config config;
config.method = fincore::risk_forecast_method::historical;
config.confidence_level = 0.99;

const auto var = fincore::forecast_var(returns, config);
if (var.status == fincore::risk_estimate_status::ok) {
    std::printf("1-day 99%% VaR: %.4f\n", var.estimate);
}

方法

做什么

horizon

kernel

historical

清洗后样本的经验分位

忽略

空

evt

GPD 超阈值或 GEV 块最大值,threshold/block_size 可选

忽略

evt_var、evt_cvar

garch

在期限上组合条件方差预测

使用,至少为 1

conditional_var、conditional_es

前置条件与状态:

  • 索引必须严格升序且无重复;先丢弃 NaN 观测。结果为空、索引乱序或重复、配置非法都会抛 input_error。

  • 当至少有两个可用非 NaN 观测且任何 GARCH 拟合都收敛时 status 为 ok;剩余少于两个时为 insufficient_data;GARCH 拟合未收敛或未通过平稳性检查时为 failed。未收敛的 GARCH 拟合还会填写 note。

  • inputs_digest 是对清洗后时间戳与数值的小写 SHA-256,因此两次运行可以在不比较浮点的情况下对照。

  • 在 GARCH 家族中,garch_model::egarch 在对数方差上递推,因此负的 omega 在那里是允许的;GARCH 与 GJR 的 omega 截距为正,因为它们进入方差。

回测你估计的东西

backtest_var 把预测序列与实际收益配对,报告 Kupiec 无条件覆盖与 Christoffersen 独立性。expected_exceptions 为 observations * (1 - confidence_level)。

状态

何时

pass

覆盖检验不拒绝模型且样本充足

fail

覆盖检验拒绝模型且样本充足

inconclusive

配对观测少于三个或期望例外少于五个

experimental

ES bootstrap 路径,其 z 分数取决于重采样种子

这就是 250 天 99% 下零例外是 inconclusive 而不是通过的原因:2.5 个期望例外低于五个的门槛。basel_traffic_light 用随样本量与置信水平变动的二项分带,把例外数转成 green/yellow/red。

滚动前推验证

walk_forward_var 在滚动窗口上重新拟合,并在第一个完整窗口之后为每个观测产出一个预测。结果报告 index、forecast、realized、refit_timestamps、fits、可选 backtest 与状态:

状态

含义

ok

产生了预测

insufficient_data

序列长度不超过窗口

unsupported

请求的目标、期限或分位未实现;unsupported_reason 会说明

时点不变量是可检验的:从某个下标起扰动收益,必须让此前每个预测逐位不变,让第一个受影响的拟合均值与预测恰好移动该扰动量,并让其标准差保持不变。常数平移改变水平,不改变离散度。

优化

入口

用途

optimize_portfolio(objectives.hpp)

max_sharpe、min_variance、target_return 或 target_risk 之一,支持箱式、行业与换手约束

efficient_frontier(frontier.hpp)

前沿点,以及最小方差与最大夏普组合

risk_parity(risk_parity.hpp)

风险贡献份额匹配等权或用户给定预算的权重;固定 252 期年,求解时权重限制在 [1e-6, 1]

约束通过投影施加,结果报告的诊断镜像实际求解的约束集合,而不是一个收敛标志。读结果时有一条已记录的限制:不带行业约束的 min_variance 在单位单纯形上求解,因此无论 short_allowed 或负 min_weight 如何设置,它都返回长仓组合。

模拟

需要可复现的结果处,模拟函数都接受显式种子。有两点需要分清:

  • 种子只在本库内复现同一条流;除非函数写明了映射(MT19937 流写明了),否则不承诺与 Python 或 NumPy 给出相同数值。

  • 对偶变量镜像的是收益路径 —— +10% 变成 -10% —— 而不是反转财富水平,因此镜像路径是 90,而不是 100 / 1.1。

失败模型小结

情形

得到什么

列缺失或类型不符、内嵌 NUL、索引乱序或重复

input_error

统计量观测不足

NaN 或 insufficient_data,绝不静默返回 0

拟合无法收敛

带 note 的 failed 状态,或带原因的 unsupported

需要比较或存储的结果

inputs_digest(SHA-256),而不是裸浮点