教程:从价格生成一份月度业绩报告

本教程构建一个小而完整的产物:价格序列变成收益、头条统计量、滚动视图、月度表以及一份渲染好的报表。大约十五分钟,只需要编译器和已安装的库。

数值是合成的。这里没有任何内容构成投资结论。

准备工作

按安装说明构建并安装库,或把 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 = [&section](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 一个工作表。同一文档渲染两次会产生相同字节,因此文件可以安全地哈希。

你构建了什么

  • 一个按文档化的预热规则从价格推导出的收益帧。

  • 四个头条统计量,各自带有退化行为。

  • 两条按完整窗口规则对齐的滚动序列。

  • 一张在月内复利的月度表。

  • 一次按共同交易日配对的基准比较。

  • 一份渲染成两种格式的报表文档。

接下来往哪走