Portfolio, risk, optimization and simulation
This guide covers the four areas that share one failure model: they own their inputs, they report what happened instead of returning a plausible-looking number, and they state their preconditions in the header. The API reference carries the exact fields; this page explains how to use them together.
Portfolio inputs own their data
portfolio_inputs copies the frames it is given at ingestion, so a later mutation
of the caller’s frame never reaches the report. materialize() returns fresh
frames on every call: mutating the result does not affect the next call.
The four maps keys are fixed — returns, positions, transactions and
benchmark_returns. “Not supplied” and “supplied but empty” are different
states: the first materializes to no key at all, the second to one key holding an
empty frame.
Positions are row-oriented, not date-keyed: two rows may share a timestamp, and
the caller’s row order is preserved. The cash column is named cash by default,
counts in the allocation denominator and is excluded from the per-asset output
columns and from the asset count.
Positions, transactions and capacity
Area |
Behaviour worth knowing |
|---|---|
Round trips |
FIFO against a signed price; one block may close and reopen within the same direction |
Turnover |
Uses the per-date book; the first day of a book with no previous state is halved |
Capacity |
Days to liquidate combine position size, rolling mean volume and the tightest single-asset limit; ties resolve to the earliest date, then the first asset |
Transaction frames |
Daily notional adds absolute trade values, so a buy and a sell on the same date reinforce rather than cancel; trading days count distinct dates |
These are accounting rules, not preferences: reports built from the same transactions will match only if the same convention is used on both sides.
Risk estimates
forecast_var and forecast_es return a risk_estimate whose estimate is
negative for losses under the losses_negative sign convention. The label is
carried on the result, so a consumer never has to guess the sign.
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);
}
Method |
What it does |
|
|
|---|---|---|---|
|
Empirical quantile of the cleaned sample |
Ignored |
empty |
|
GPD peaks-over-threshold or GEV block maxima, |
Ignored |
|
|
Conditional variance forecast composed over the horizon |
Used, at least 1 |
|
Preconditions and statuses:
The index must be strictly ascending and duplicate-free;
NaNobservations are dropped first. An empty result set, an unsorted or duplicated index, or an invalid configuration raisesinput_error.statusisokwhen at least two non-NaNobservations were usable and any GARCH fit converged,insufficient_datawhen fewer than two remained, andfailedwhen a GARCH fit did not converge or failed its stationarity checks. A non-converged GARCH fit also fillsnote.inputs_digestis a lowercase SHA-256 over the cleaned timestamps and values, so two runs can be compared without comparing floats.Under the GARCH family,
garch_model::egarchrecurses on the log variance, so a negativeomegais admissible there while the GARCH and GJRomegaintercepts are positive because they enter a variance.
Backtesting what you estimated
backtest_var pairs a forecast series with realised returns and reports Kupiec
unconditional coverage and Christoffersen independence. expected_exceptions is
observations * (1 - confidence_level).
Status |
When |
|---|---|
|
Coverage tests do not reject the model and the sample is adequate |
|
A coverage test rejects the model and the sample is adequate |
|
Fewer than three paired observations or fewer than five expected exceptions |
|
The ES bootstrap path, whose z-score depends on the resample seed |
That is why zero exceptions at 99% on 250 days is inconclusive rather than a
pass: 2.5 expected exceptions is below the five-exception floor. basel_traffic_light
turns the exception count into green/yellow/red using binomial bands that
move with both the sample size and the confidence level.
Walk-forward validation
walk_forward_var re-fits on a rolling window and emits one forecast per
observation after the first complete window. The result reports index,
forecast, realized, refit_timestamps, fits, an optional backtest, and a
status:
Status |
Meaning |
|---|---|
|
Forecasts were produced |
|
The series is not longer than the window |
|
The requested target, horizon or quantile is not implemented; |
The point-in-time invariant is testable: perturbing returns from some index onward must leave every earlier forecast bit-identical, move the first affected fit’s mean and forecast by exactly the perturbation, and leave its standard deviation unchanged. A constant shift moves the level, not the dispersion.
Optimization
Entry point |
Purpose |
|---|---|
|
One of |
|
Frontier points plus the minimum-variance and maximum-Sharpe portfolios |
|
Weights whose risk-contribution shares match an equal or user-supplied budget; fixed 252-period year, weights bounded to |
Constraints are applied by projection, and the result reports diagnostics that
mirror the solved constraint set rather than a convergence flag. One documented
limitation matters when reading results: min_variance without sector
constraints is solved over the unit simplex, so it returns a long-only book
regardless of short_allowed or a negative min_weight.
Simulation
Simulation functions take an explicit seed wherever the result must be reproducible. Two facts are worth keeping straight:
A seed reproduces a stream inside this library; it does not promise the same numbers as a Python or NumPy call unless the function documents that mapping (the MT19937 stream does).
Antithetic variates mirror the return path —
+10%becomes-10%— rather than inverting the wealth level, so a mirrored path is90, not100 / 1.1.
Failure model summary
Situation |
What you get |
|---|---|
Missing or mis-typed column, embedded NUL, unsorted or duplicated index |
|
Not enough observations for the statistic |
|
A fit that cannot converge |
|
A result that must be compared or stored |
|