教程:从价格生成一份月度业绩报告
本教程构建一个小而完整的产物:价格序列变成收益、头条统计量、滚动视图、月度表以及一份渲染好的报表。大约十五分钟,只需要编译器和已安装的库。
数值是合成的。这里没有任何内容构成投资结论。
准备工作
按安装说明构建并安装库,或把 include/ 与构建出的库加入一个临时工程。下面的片段依次衔接。
#include <fincore/dataframe.hpp>
#include <fincore/metrics/drawdown.hpp>
#include <fincore/metrics/ratios.hpp>
#include <fincore/metrics/returns.hpp>
#include <fincore/report/html.hpp>
#include <fincore/report/models.hpp>
#include <fincore/report/xlsx.hpp>
#include <fincore/rolling_engine.hpp>
#include <cmath>
#include <cstdio>
#include <string>
#include <vector>
第 1 步:把价格变成收益
simple_returns 用每个价格除以前一个,并丢弃第一个观测,保留每对中较晚价格的时间戳。
std::vector<fincore::timestamp> index;
std::vector<double> prices;
const auto day = 86'400'000'000'000LL;
for (int i = 0; i < 260; ++i) {
index.push_back(fincore::utc_timestamp(2025, 1, 2) + i * day);
prices.push_back(100.0 + 6.0 * std::sin(i * 0.11) +
3.0 * std::cos(i * 0.37) + 0.02 * i);
}
fincore::dataframe price_frame;
price_frame.load_index(std::vector<fincore::timestamp>(index));
price_frame.load_column<double>("close", prices,
hmdf::nan_policy::dont_pad_with_nans);
const auto returns = fincore::simple_returns(price_frame, "close");
std::printf("returns: %zu rows\n", returns.shape().first);
检查行数:260 个价格产生 259 个收益,第一个时间戳是第二个价格的日期。如果数量不对,通常是输入索引重复了日期或未排序。
第 2 步:头条统计量
const double sharpe = fincore::sharpe_ratio(returns, "returns");
const double sortino = fincore::sortino_ratio(returns, "returns");
const double drawdown = fincore::max_drawdown(returns, "returns");
const double calmar = fincore::calmar_ratio(returns, "returns");
std::printf("sharpe %.3f sortino %.3f max dd %.3f calmar %.3f\n",
sharpe, sortino, drawdown, calmar);
这四个指标都以 252 期年化并丢弃 NaN。从不下跌的序列没有回撤,因此 calmar_ratio 返回 NaN 而不是一个很大的数 —— 这是刻意的,值得在你自己的输出里处理。
第 3 步:滚动视图
rolling_engine 在每个完整的尾部窗口上评估指标。因此 20 个观测的窗口从第 20 行开始产出,而不是第 1 行。
fincore::rolling_engine rolling{returns, std::nullopt, 20U, 252.0};
const auto series = rolling.compute(
{fincore::rolling_metric::sharpe,
fincore::rolling_metric::volatility});
for (const auto& [name, frame] : series) {
const auto& values = frame.get_column<double>(fincore::returns_column);
std::printf("%-12s %zu rows, last %.4f
", name.c_str(),
frame.shape().first, values.back());
}
映射的键是指标名;输出帧仍然使用常规的收益列名,因此一个循环即可读取两条序列。
第 4 步:月度表
aggregate_returns 按日历月分组,并在聚合值旁添加整数年份与期内序号列。
const auto monthly = fincore::aggregate_returns(
returns, fincore::return_aggregation_period::monthly, "returns");
std::printf("months: %zu\n", monthly.shape().first);
月度值在月内复利,而不是相加。这个区别通常是手写表格与本表不一致的原因。
第 5 步:与基准比较
const auto benchmark_prices = price_frame; // stand-in for a real index
const auto benchmark = fincore::simple_returns(benchmark_prices, "close");
std::printf("beta %.3f alpha %.4f\n",
fincore::beta(returns, benchmark),
fincore::alpha(returns, benchmark));
两帧按共有的时间戳配对,因此节假日日历不同的基准会缩短样本,而不是让样本错位。
第 6 步:构建并渲染报表
fincore::report_document document;
document.domain = "portfolio";
document.title = "Monthly performance review";
document.metadata = {{"source", "tutorial"}, {"frequency", "daily"}};
fincore::report_section section;
section.key = "performance";
section.title = "Performance";
auto add_metric = [§ion](std::string key, std::string label, double value) {
fincore::report_metric metric;
metric.key = std::move(key);
metric.label = std::move(label);
metric.number = value;
section.metrics.push_back(std::move(metric));
};
add_metric("sharpe", "Sharpe ratio", sharpe);
add_metric("sortino", "Sortino ratio", sortino);
add_metric("max_drawdown", "Maximum drawdown", drawdown);
fincore::report_table table;
table.key = "monthly_returns";
table.title = "Monthly returns";
table.columns = {"month", "return"};
for (std::size_t row = 0; row < monthly.shape().first; ++row) {
table.rows.push_back(
{fincore::report_cell::number_cell(
static_cast<double>(row + 1U)),
fincore::report_cell::number_cell(
monthly.get_column<double>("returns")[row])});
}
section.tables.push_back(std::move(table));
document.sections.push_back(std::move(section));
const std::string html = fincore::render_html(document, {.language = "en"});
const std::string workbook = fincore::render_xlsx(document);
把 html 与 workbook 写入文件并打开:HTML 是自包含的,工作簿每个 section 一个工作表。同一文档渲染两次会产生相同字节,因此文件可以安全地哈希。
你构建了什么
一个按文档化的预热规则从价格推导出的收益帧。
四个头条统计量,各自带有退化行为。
两条按完整窗口规则对齐的滚动序列。
一张在月内复利的月度表。
一次按共同交易日配对的基准比较。
一份渲染成两种格式的报表文档。
接下来往哪走
指标与滚动窗口:你刚打印的每个数字背后的定义。
组合、风险、优化与模拟:加入 VaR、期望损失与回测结论。
因子研究:从单条序列走向面板。
报表、运行时与扩展:其他后端与 artifact bundle 契约。