Cookbook
Short, working answers to tasks that come up again and again. Each recipe is self-contained; the guides explain the reasoning behind the rules.
Rolling metrics for one series
#include <fincore/rolling_engine.hpp>
fincore::rolling_engine engine{returns, std::nullopt, 60U, 252.0};
const auto series = engine.compute({fincore::rolling_metric::sharpe,
fincore::rolling_metric::volatility,
fincore::rolling_metric::max_drawdown});
for (const auto& [name, frame] : series) {
const auto& values = frame.get_column<double>(fincore::returns_column);
// values.size() == returns.shape().first - 60 + 1
}
The output starts at the first complete window, so it is shorter than the
input by window - 1 rows. Pass a benchmark frame as the second argument only if
you ask for rolling_metric::beta.
The deepest drawdowns as a table
#include <fincore/metrics/drawdown.hpp>
for (const auto& row : fincore::gen_drawdown_table(returns, 5U, "returns")) {
if (std::isnan(row.net_drawdown_percent)) {
continue; // unused row: fewer than five episodes in the series
}
std::printf("%.2f%% peak %lld valley %lld\n",
row.net_drawdown_percent,
static_cast<long long>(*row.peak),
static_cast<long long>(*row.valley));
}
Depth is a positive percentage of the prior peak, unlike max_drawdown,
which returns a negative fraction. When highs tie, the window starts at the
latest one.
VaR plus a coverage verdict
#include <fincore/risk/backtesting.hpp>
#include <fincore/risk/models.hpp>
fincore::risk_forecast_config config;
config.confidence_level = 0.99;
const auto estimate = fincore::forecast_var(returns, config);
const auto verdict = fincore::backtest_var(forecast, realized, 0.99, 0.05);
if (estimate.status != fincore::risk_estimate_status::ok) {
// insufficient_data or failed: do not report the number as a result
} else if (verdict.status == fincore::risk_backtest_status::inconclusive) {
// fewer than three pairs or fewer than five expected exceptions
}
estimate is negative for losses under the losses_negative convention. An
inconclusive verdict is not a pass: 250 days at 99% expects 2.5 exceptions,
below the five needed to test.
GARCH VaR over a multi-day horizon
config.method = fincore::risk_forecast_method::garch;
config.garch_kernel = fincore::garch_model::gjr_garch;
config.horizon = 10U;
const auto ten_day = fincore::forecast_var(returns, config);
Only the GARCH method composes over horizon; historical and EVT estimates
ignore it. A fit that does not converge returns failed with a note instead of
a number.
Quantile means from a factor panel
const auto model = fincore::analyze_factor(prepared, factor_config);
const auto& table = model.mean_returns_by_quantile;
for (std::size_t q = 0; q < table.quantiles.size(); ++q) {
for (std::size_t l = 0; l < table.labels.size(); ++l) {
std::printf("Q%d %s %.5f\n", table.quantiles[q],
table.labels[l].c_str(),
table.means[q * table.labels.size() + l]);
}
}
Row keys are quantiles, column keys are labels, and the buffer is row-major. Read
labels from the same table when you save it; a table without its label order is
ambiguous.
A point-in-time panel from revisions
#include <fincore/factor/pit.hpp>
std::vector<fincore::pit_observation> observations{
{"AAA", {as_of, known_at, effective_from, 1.25}, true},
{"BBB", {as_of, known_at, effective_from, -0.40}, false}, // out of universe
};
const auto prepared = fincore::prepare_pit_factor_data(
observations, prices, evaluation_dates);
const auto model = fincore::analyze_factor(prepared, factor_config);
Ordering is checked, not repaired: known_at must not precede as_of, and
effective_from must not precede known_at. On each evaluation date an asset
contributes the revision with the largest (effective_from, known_at, as_of)
among those already effective and known; assets outside the universe are skipped
silently, which is why a date can have fewer cross-sections than assets.
A reproducible simulation
#include <fincore/simulation/paths.hpp>
const auto paths = fincore::geometric_brownian_motion(
100.0, // initial value
0.08, // annualized drift
0.20, // annualized volatility
1.0, // total years
1.0 / 252.0, // time step
1'000U, // path count
42U); // seed
The same seed reproduces the same frame inside this library. That is a reproducibility guarantee, not a promise of NumPy identity — only functions whose documentation names a reference stream carry that promise.
Add a table to a report and export it
fincore::report_section section;
section.key = "risk";
section.title = "Risk";
fincore::report_table table;
table.key = "var";
table.title = "VaR by confidence level";
table.columns = {"level", "var", "verdict"};
for (const auto& row : rows) {
table.rows.push_back({fincore::report_cell::text_cell(row.level),
fincore::report_cell::number_cell(row.value),
fincore::report_cell::text_cell(row.verdict)});
}
section.tables.push_back(std::move(table));
document.sections.push_back(std::move(section));
const std::string html = fincore::render_html(document, {.language = "zh"});
language = "zh" renders the Chinese labels from the same document; the cells you
supply are printed as given, so translate your own labels at the call site.