Portfolio_Risk_Management/docs/measurement_and_models.tex
herzogflorian b5da424dfe quantcore: standalone Apptainer setup for students' DGX Sparks
The portfolio data, risk and mean-CVaR optimisation service, prepared for
students who run it on their own DGX Spark without root, Docker or Tailscale.

- deploy/apptainer/qc.sh: install, start/stop/status/logs, cli/api/python,
  data import/export and Yahoo setup/update. cuOpt and quantcore run as two
  Apptainer instances of the user and talk over 127.0.0.1; the API and the
  test bench listen on 127.0.0.1 only. Builds quantcore.sif without root, or
  falls back to a plain Python image plus a virtualenv when the Spark allows
  no unprivileged builds. Detects taken ports and suggests free ones.
- deploy/apptainer/quantcore.def + constraints.txt: the image, with the
  library versions of the verified production container pinned.
- cuOpt under Apptainer: NVIDIA's image sets HOME=/opt/cuopt, read-only in a
  SIF, so the solver died on its first job; qc.sh gives it a writable home.
- solvers: a cuOpt that disappears during a solve now falls back to HiGHS
  instead of answering 500 (requests' ConnectionError escaped the fallback).
- client: idempotent GETs retry a dropped keep-alive connection, which made
  `quantcore-api update --wait` fail when a poll met uvicorn's 5 s timeout.
- config: cuOpt defaults to http://127.0.0.1:5000 and `quantcore serve` to
  127.0.0.1; the Docker images keep passing their own values.
- docs/SPARK_APPTAINER.md: the students' guide; README, USAGE, ARCHITECTURE
  and SPARK_DEPLOYMENT point to it.

Tested on a DGX Spark (GB10, Apptainer 1.5.4, unprivileged user) in both
modes: selftest on every endpoint, optimisation on cuOpt, Yahoo update,
port conflicts; 139 tests pass in the container against the live cuOpt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 09:55:28 +02:00

808 lines
50 KiB
TeX

% !TEX encoding = UTF-8 Unicode
% ---------------------------------------------------------------------------------------------
% quantcore -- documentation of the measurement layer, the scenario models and the
% expected-return model, as implemented in quantcore/measurement and quantcore/models.
% Build: latexmk -pdf docs/measurement_and_models.tex
% ---------------------------------------------------------------------------------------------
\documentclass[11pt,a4paper]{article}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage[british]{babel}
\usepackage{lmodern}
\usepackage{microtype}
\usepackage[margin=2.4cm]{geometry}
\usepackage{amsmath,amssymb,amsthm,mathtools}
\usepackage{booktabs,array}
\usepackage{enumitem}
\usepackage{xcolor}
\usepackage[colorlinks=true,linkcolor=blue!50!black,citecolor=blue!50!black,urlcolor=blue!50!black]{hyperref}
\newtheorem{theorem}{Theorem}[section]
\newtheorem{proposition}[theorem]{Proposition}
\theoremstyle{definition}
\newtheorem{definition}[theorem]{Definition}
\newtheorem{example}[theorem]{Example}
\theoremstyle{remark}
\newtheorem{remark}[theorem]{Remark}
\DeclareMathOperator{\CVaR}{CVaR}
\DeclareMathOperator{\VaR}{VaR}
\DeclareMathOperator{\E}{\mathbb{E}}
\DeclareMathOperator{\Var}{Var}
\DeclareMathOperator{\Cov}{Cov}
\DeclareMathOperator{\diag}{diag}
\DeclareMathOperator{\tr}{tr}
\newcommand{\R}{\mathbb{R}}
\newcommand{\pos}[1]{\left[#1\right]^{+}}
\newcommand{\code}[1]{\texttt{\hyphenchar\font=-1 #1}}
\newcommand{\bs}{\boldsymbol}
\setlength{\parskip}{4pt}
\setlength{\parindent}{0pt}
\title{Risk and Performance Measurement, Scenario Generation\\ and the Expected-Return Model\\[1ex]
\large From log-returns to VaR/CVaR, from the covariance estimators to simulated horizon
scenarios, and from market capitalisations to Black--Litterman expected returns with
Idzorek confidence views, as implemented in \code{quantcore}}
\author{AISE502 -- AI-Augmented Portfolio Intelligence Platform\\ \small Deterministic quant core, modules \code{quantcore.measurement} and \code{quantcore.models}}
\date{September 2026}
\begin{document}
\maketitle
\begin{abstract}
\noindent
This note is the companion of \emph{Mean--CVaR Portfolio Optimisation in Whole Shares}
(the file \code{optimization\_model.pdf} in \code{docs/}). It documents the three model layers that feed the
optimiser and the platform's risk and performance figures: (i) the \emph{measurement} layer ---
log-returns in CHF, moving-window and exponentially weighted estimators of mean, volatility and
covariance, Value-at-Risk and Conditional Value-at-Risk at 95\,\% and 99\,\% for a day, a week
or a month, and the performance statistics; (ii) the \emph{stock-price models} that turn the
latest covariance into a set of horizon scenarios --- geometric Brownian motion and the
empirical-innovation model $\varepsilon_t=\Sigma_{t-1}^{-1/2}r_t$, $r(j\,|\,T)=\Sigma_T^{1/2}\varepsilon_j$;
and (iii) the \emph{expected-return model} --- market-capitalisation weights, the broad-market
average, reverse optimisation, and the Black--Litterman posterior with views whose uncertainty is
calibrated from a degree of belief following Idzorek. Every formula is stated with its
conventions and defaults, the numerical safeguards (covariance shrinkage, innovation
re-standardisation) are motivated with measurements on the platform's own data set, and the last
section maps the mathematics to the functions, parameters and output fields of the code.
\end{abstract}
\tableofcontents
% ============================================================================================
\section{Data, conventions and notation}
% ============================================================================================
The universe consists of $N$ stocks with adjusted closing prices $P_i(t)$ in their listing
currency $c(i)$, observed on the \emph{union calendar} of all exchanges (a day on which at least
one exchange in the universe trades). A holiday of one exchange is filled with that exchange's
previous close (at most five days), so every trading day of the union calendar has a price for
every stock. Prices that are cleaned away (broken adjustments, spike ticks; see
\code{docs/METHODOLOGY.md}, Section~1) are treated the same way.
FX rates $X_c(t)$ are quoted as CHF per one unit of currency $c$ (so $X_{\mathrm{CHF}}\equiv1$),
and the base currency of everything below is CHF.
Three frequencies are used: \emph{daily} (union calendar, 252 periods per year), \emph{weekly}
(Friday closes, 52) and \emph{monthly} (month-end closes, 12); $\pi$ denotes the number of
periods per year of the chosen frequency. A \emph{horizon} is expressed as a number $h$ of
periods: at daily frequency a day is $h=1$, a week $h=5$, a month $h=21$; at weekly frequency
a week is $h=1$ and a month $h=4$.
Throughout, $r$ denotes a log-return, $R=e^{r}-1$ the corresponding simple return, and
\emph{losses are positive numbers}. Vectors are columns; $\mathbf 1$ is the vector of ones.
% ============================================================================================
\section{Returns}
% ============================================================================================
\subsection{Log-returns and the currency conversion}
The one-period log-return of stock $i$ in its listing currency is
\begin{equation}
r_i(t) \;=\; \ln P_i(t) - \ln P_i(t-1).
\label{eq:logret}
\end{equation}
The price in CHF is $P_i^{\mathrm{CHF}}(t)=P_i(t)\,X_{c(i)}(t)$, hence its log-return is the
\emph{sum} of the local return and the FX return,
\begin{equation}
r_i^{\mathrm{CHF}}(t) \;=\; r_i(t) + \big(\ln X_{c(i)}(t) - \ln X_{c(i)}(t-1)\big),
\label{eq:chfret}
\end{equation}
exactly and without any cross term --- the reason log-returns are the working unit of the whole
platform. (With simple returns the conversion is $(1+R_i)(1+R_X)-1$, which is neither additive
nor symmetric.) The implementation converts prices with the FX frame and takes the log-return of
the product; \eqref{eq:chfret} is checked by a unit test.
\subsection{Aggregation over time and over assets}
Under the i.i.d.\ assumption of the price models (Section~\ref{sec:models}) log-returns add over
time: the $h$-period return is $r^{(h)}(t)=\sum_{k=0}^{h-1} r(t-k)$. The historical risk
measures use \emph{overlapping} $h$-period sums (more observations, mildly autocorrelated);
non-overlapping blocks are available as an option.
Over assets, log-returns do not add. The exact log-return of a portfolio whose value weights
$w_i$ are fixed at the start of the period is
\begin{equation}
r_p \;=\; \ln\Big(1+\sum_i w_i\,(e^{r_i}-1)\Big),
\label{eq:portret}
\end{equation}
because simple returns aggregate linearly. A cash residual $1-\mathbf 1^\top w$ earns zero. This
formula (\code{portfolio\_log\_returns}) is used wherever a portfolio return series is needed
--- the historical portfolio VaR and the portfolio performance statistics.
\subsection{Why log-returns are annualised additively}
Means of log-returns scale with time: $\E[r^{(h)}]=h\mu$, and under independence
$\Var[r^{(h)}]=h\sigma^2$. An annualised log-return is therefore $\pi\mu$, an annualised
volatility $\sqrt{\pi}\,\sigma$; the geometric (compounded) annual return is $e^{\pi\mu}-1$. Both
are reported.
% ============================================================================================
\section{Moving-window and exponentially weighted estimators}
\label{sec:estimators}
% ============================================================================================
Every statistic exists in two flavours (\code{quantcore.measurement.estimators}).
\subsection{Moving window}
With window length $w$ (default 200 periods) the estimates at date $t$ use the observations
$t-w+1,\dots,t$ with equal weight $1/w$: the sample mean $m_t$, the sample standard deviation
(divisor $w-1$), the sample covariance
\begin{equation}
\Sigma_t^{\mathrm{roll}} \;=\; \frac{1}{w-1}\sum_{k=0}^{w-1}(r_{t-k}-m_t)(r_{t-k}-m_t)^\top .
\label{eq:rollcov}
\end{equation}
Rows with a missing value are dropped for matrices; a series needs at least $w/2$ observations
before a value is reported.
\subsection{Exponential weighting}
With decay $\lambda\in(0,1)$ the observation $k$ periods back receives the weight
$\lambda^k$ (RiskMetrics \cite{RiskMetrics1996}); the weights are normalised over the available
history,
\begin{equation}
\omega_k \;=\; \frac{\lambda^k}{\sum_{j=0}^{n-1}\lambda^j}, \qquad k=0,\dots,n-1,
\label{eq:ewmw}
\end{equation}
(pandas \code{ewm(alpha=1-$\lambda$, adjust=True)}), and the estimators are the weighted
moments
\begin{equation}
m_t^{\mathrm{ewm}} = \sum_k\omega_k\, r_{t-k}, \qquad
\Sigma_t^{\mathrm{ewm}} = \sum_k \omega_k\,(r_{t-k}-m_t^{\mathrm{ewm}})(r_{t-k}-m_t^{\mathrm{ewm}})^\top,
\label{eq:ewmcov}
\end{equation}
with the diagonal of \eqref{eq:ewmcov} as the EWM variance (\code{var(bias=True)}). Both
estimators are \emph{centred}: the corresponding mean is subtracted. The effective window is
$1/(1-\lambda)$ observations (half-life $\ln 0.5/\ln\lambda$):
\begin{center}
\small
\begin{tabular}{@{}lcccc@{}}
\toprule
$\lambda$ & 0.94 & 0.97 & 0.99 & 0.997 \\
\midrule
effective window $1/(1-\lambda)$ & 17 & 33 & 100 & 333 \\
half-life (periods) & 11.2 & 22.8 & 69.0 & 230.7 \\
weight of the observation 100 periods back & 0.002 & 0.048 & 0.366 & 0.741 \\
\bottomrule
\end{tabular}
\end{center}
$\lambda=0.94$ is the RiskMetrics value for daily data and the default of the measurement
services (fast adaptation, noisy); $\lambda=0.99$ is the default of the optimisation covariance
(a smoother matrix that still adapts within a quarter). For matrices the history is truncated
where the weights fall below $10^{-4}$ (917 observations for $\lambda=0.99$).
\subsection{Sequences of matrices}
The innovation filter of Section~\ref{sec:fhs} needs $\Sigma_{t-1}$ for \emph{every} date $t$,
i.e.\ the covariance estimated from the data up to and including $t-1$. Storing $T$ matrices of
size $N\times N$ is impossible for a thousand stocks, so \code{covariance\_sequence} streams
them: the rolling estimator recomputes \eqref{eq:rollcov} for each date ($O(wN^2)$ per step),
while the exponentially weighted estimator uses the exact recursion of the centred filter with
$\alpha=1-\lambda$,
\begin{equation}
d_t = r_t - m_{t-1}, \qquad m_t = m_{t-1} + \alpha\, d_t, \qquad
\Sigma_t = \lambda\,\Sigma_{t-1} + \alpha\, d_t\,(r_t-m_t)^\top ,
\label{eq:ewmrec}
\end{equation}
which costs $O(N^2)$ per step and, after a warm-up of $\max(50, 2/(1-\lambda))$ observations
initialised with \eqref{eq:ewmcov}, coincides with the normalised-weight estimate up to the
factor $1/(1-\lambda^n)\to1$ (verified by a unit test at 5\,\% for $\lambda=0.97$).
\subsection{Shrinkage towards the diagonal}
\label{sec:shrinkage}
A covariance estimated from $n$ effective observations is ill-conditioned when $N$ is not small
against $n$ and singular for $N>n$: the smallest eigenvalues are underestimated, the largest
overestimated (the Marchenko--Pastur effect), and anything that inverts the matrix --- the
innovation filter, mean-variance weights --- amplifies the error. The classical remedy is
linear shrinkage \cite{LedoitWolf2004} towards a structured target; the platform shrinks towards
the diagonal (the target of Schäfer and Strimmer \cite{SchaeferStrimmer2005}), which keeps the
variances and pulls the correlations towards zero:
\begin{equation}
\Sigma^{(s)} \;=\; (1-s)\,\Sigma + s\,\diag(\Sigma), \qquad s\in[0,1].
\label{eq:shrink}
\end{equation}
The intensity can be given, or estimated from the data (\code{shrinkage="auto"}): for centred
observations $x_k$ ($k=1,\dots,n$), products $w_{kij}=x_{ki}x_{kj}$ and sample covariances
$s_{ij}=\frac{n}{n-1}\bar w_{ij}$, the intensity that minimises the expected squared Frobenius
loss of \eqref{eq:shrink} is
\begin{equation}
s^\ast \;=\; \frac{\sum_{i\ne j}\widehat{\Var}(s_{ij})}{\sum_{i\ne j}s_{ij}^2},
\qquad
\widehat{\Var}(s_{ij}) \;=\; \frac{n}{(n-1)^3}\sum_{k=1}^n \big(w_{kij}-\bar w_{ij}\big)^2 ,
\label{eq:ss}
\end{equation}
clipped to $[0,1]$. It is small for a long, strongly correlated sample and approaches one when
$N$ approaches $n$ --- exactly when the sample covariance becomes unreliable. For the EWM
estimator the intensity is computed on the last $3/(1-\lambda)$ observations with equal weights.
On the platform's data set (daily CHF returns, $\lambda=0.99$) the automatic intensity is 0.047
for the 100 largest stocks, 0.069 for 300 and 0.114 for the full universe of 745 names. The
sequence \eqref{eq:ewmrec} uses one intensity for all its matrices.
% ============================================================================================
\section{Risk measures}
\label{sec:risk}
% ============================================================================================
\subsection{Loss, VaR and CVaR}
For a position of value $V_0$ and a horizon log-return $r$, the loss in return space is
$\ell=-r$ and in money it is $V_0(1-e^{r})=V_0(1-e^{-\ell})$. The Value-at-Risk at level
$\beta$ is the $\beta$-quantile of the loss distribution, the Conditional Value-at-Risk (expected
shortfall) the mean of the loss beyond it,
\begin{equation}
\VaR_\beta = q_\beta(\ell), \qquad
\CVaR_\beta = \frac{1}{1-\beta}\int_\beta^1 q_\gamma(\ell)\,d\gamma
\;\;\big(=\E[\ell\mid\ell\ge\VaR_\beta]\text{ for continuous }\ell\big),
\label{eq:varcvar}
\end{equation}
see \cite{AcerbiTasche2002,MFE2015}. The platform reports both at $\beta=0.95$ and $0.99$.
Because $\ell\mapsto V_0(1-e^{-\ell})$ is increasing, quantiles commute with it, and a
return-space VaR translates into money exactly as $V_0(1-e^{-\VaR_\beta})$; the CVaR in money
is computed the same way from the return-space CVaR (which is exact for VaR and a very good
approximation for CVaR: for a weekly CVaR of 4.6\,\% the difference between the exact tail mean
of $V_0(1-e^{-\ell})$ and $V_0(1-e^{-\CVaR})$ is below 0.1\,\% of $V_0$; the scenario-based
portfolio risk of Section~\ref{sec:portfoliorisk} works in money directly and is exact).
\subsection{Gaussian (GBM) VaR and CVaR}
If $r\sim N(\mu,\sigma^2)$ then $\ell=-r\sim N(-\mu,\sigma^2)$, its $\beta$-quantile is
$-\mu+z_\beta\sigma$ with $z_\beta=\Phi^{-1}(\beta)$, and the tail mean follows from
$\E[Z\mid Z\ge z_\beta]=\varphi(z_\beta)/(1-\beta)$ for a standard normal $Z$:
\begin{equation}
\VaR_\beta = -\mu + z_\beta\,\sigma, \qquad
\CVaR_\beta = -\mu + \sigma\,\frac{\varphi(z_\beta)}{1-\beta}.
\label{eq:normal}
\end{equation}
The multipliers are $z_{0.95}=1.645$, $\varphi(z_{0.95})/0.05=2.063$, $z_{0.99}=2.326$,
$\varphi(z_{0.99})/0.01=2.665$. Under the i.i.d.\ assumption the horizon parameters are
$\mu_h=h\mu$ and $\sigma_h=\sqrt h\,\sigma$; $\mu$ and $\sigma$ come from either estimator of
Section~\ref{sec:estimators}. The mean is kept (a positive drift lowers the loss quantile
one-for-one); for daily data it is negligible, for a month it is not.
\begin{example}[Reference values]
A stock with daily volatility $\sigma=1\,\%$ and zero mean, one week ($h=5$,
$\sigma_h=2.236\,\%$): $\VaR_{0.95}=3.678\,\%$, $\CVaR_{0.95}=4.612\,\%$, $\VaR_{0.99}=5.202\,\%$,
$\CVaR_{0.99}=5.960\,\%$. For a position of CHF~100\,000 the exact money figures are
$\VaR_{0.95}=3\,611$, $\CVaR_{0.95}=4\,508$, $\VaR_{0.99}=5\,069$, $\CVaR_{0.99}=5\,786$ (the
linear approximation $V_0\cdot\CVaR$ would give 4\,612 and 5\,960). The daily figures
$\VaR_{0.95}=1.645\,\%$, $\CVaR_{0.95}=2.063\,\%$, $\VaR_{0.99}=2.326\,\%$, $\CVaR_{0.99}=2.665\,\%$
and the weekly ones are pinned by \code{tests/test\_risk.py}.
\end{example}
\subsection{Historical VaR and CVaR}
The empirical counterparts replace the Gaussian by the empirical distribution of the realised
$h$-period returns $r^{(h)}$ over a window of $\max(5w,750)$ periods:
\begin{equation}
\VaR_\beta^{\mathrm{hist}} = -\hat q_{1-\beta}\big(r^{(h)}\big), \qquad
\CVaR_\beta^{\mathrm{hist}} = -\frac{1}{|\mathcal T|}\sum_{t\in\mathcal T} r^{(h)}_t, \quad
\mathcal T=\big\{t: r^{(h)}_t\le \hat q_{1-\beta}\big\},
\label{eq:hist}
\end{equation}
with $\hat q$ the empirical quantile (linear interpolation). They capture fat tails and skewness
that the Gaussian misses, at the price of noise: at $\beta=0.99$ with 750 daily observations the
tail contains eight points.
\subsection{Discrete scenario sets}
For $S$ equiprobable losses (a simulated scenario set, Section~\ref{sec:models}) the definition
\eqref{eq:varcvar} evaluates exactly: with $k=(1-\beta)S$ and the losses sorted decreasingly
$\ell_{(1)}\ge\ell_{(2)}\ge\dots$,
\begin{equation}
\CVaR_\beta = \frac1k\Big(\sum_{s=1}^{\lfloor k\rfloor}\ell_{(s)} + (k-\lfloor k\rfloor)\,\ell_{(\lfloor k\rfloor+1)}\Big),
\qquad \VaR_\beta = \ell_{(\lceil k\rceil)} ,
\label{eq:scen}
\end{equation}
which is the value the Rockafellar--Uryasev linear programme of the optimisation returns
(function \code{scenario\_cvar}; see Remark~2.3 of the companion note). For $S=2\,000$ and
$\beta=0.99$ the tail has $k=20$ scenarios.
\subsection{Per-asset tables and time series}
\code{RiskService.asset\_risk} returns, for a list of stocks, a frequency, a horizon, a window
$w$ and a decay $\lambda$, one row per stock with the annualised rolling and EWM volatilities and,
for each confidence level $\beta\in\{0.95,0.99\}$, the Gaussian VaR and CVaR from the rolling
estimator, the same from the EWM estimator, and the historical VaR and CVaR --- six figures per
level, all in log-return units over the horizon (Table~\ref{tab:impl}). \code{risk\_series} gives
the same quantities as time series: Gaussian figures from the rolling / EWM volatility and mean
series, historical ones from rolling quantiles and rolling tail means of the one-period returns,
scaled by $\sqrt h$ for longer horizons (an approximation used only in the series).
\subsection{Portfolio risk in money}
\label{sec:portfoliorisk}
A portfolio is given in \emph{numbers of shares} $n_i$ plus cash $C$. With today's CHF prices
$P_i$ the value is $V_0=C+\sum_iP_in_i$ and the value weights are $w_i=P_in_i/V_0$ (the cash
weight $C/V_0$ is riskless). Three methods are offered (\code{portfolio\_risk}):
\begin{itemize}[nosep]
\item \textbf{normal}: $\sigma_p^2=w^\top\Sigma w$ with the rolling or EWM covariance,
$\mu_p=w^\top m$ with the window mean, then \eqref{eq:normal} with $h\mu_p$ and
$\sqrt h\sigma_p$, converted to money;
\item \textbf{historical}: the realised portfolio return series \eqref{eq:portret} over the
last $\max(5w,750)$ periods, aggregated to $h$ periods, then \eqref{eq:hist};
\item \textbf{scenarios}: money P\&L $\Pi_s=V_0\sum_iw_i(e^{r_{s,i}}-1)$ for each scenario of a
simulated set, then \eqref{eq:scen} on the losses $-\Pi_s$ --- exact in money, and
precisely the risk the optimiser constrains.
\end{itemize}
The result (\code{PortfolioRisk}) carries the value, the weights, the horizon, the method, the
annualised portfolio volatility, and per confidence level the VaR and CVaR in return space and in
money. A unit test checks that Gaussian scenarios with the same mean and covariance reproduce the
normal figures within sampling error.
% ============================================================================================
\section{Performance measures}
% ============================================================================================
All statistics (\code{performance\_table}) are computed from one-period log-returns $r_t$,
$t=1,\dots,T$, at the chosen frequency with $\pi$ periods per year and the annual CHF risk-free
rate $r_f$ (0 by default):
\begin{align}
\text{cumulative return} &= e^{\sum_t r_t}-1, \quad
\text{ann.\ log-return} = \pi\,\bar r, \quad
\text{ann.\ return} = e^{\pi\bar r}-1, \notag\\
\text{ann.\ volatility} &= \sqrt{\pi}\,\hat\sigma, \quad
\text{Sharpe} = \frac{\pi\bar r - r_f}{\sqrt\pi\,\hat\sigma}, \quad
\text{Calmar} = \frac{e^{\pi\bar r}-1}{-\mathrm{MDD}}, \notag\\
\text{Sortino} &= \frac{\pi\bar r - r_f}{\sqrt{\pi\cdot\frac1T\sum_t \min(r_t,0)^2}},
\label{eq:perf}
\end{align}
where $\hat\sigma$ is the sample standard deviation (divisor $T-1$) and the maximum drawdown
$\mathrm{MDD}=\min_t\big(V_t/\max_{s\le t}V_s-1\big)$ is taken on the value path
$V_t=\exp(\sum_{s\le t}r_s)$. Skewness and excess kurtosis of $r_t$, the hit ratio (share of
positive periods), the best and the worst period, the number of observations and the start and
end dates complete the row.
\emph{Rolling} versions use the window mean and standard deviation of
Section~\ref{sec:estimators} for the annualised return, volatility and Sharpe ratio, plus the
cumulative return over the window and the running drawdown; \emph{EWM} versions use the
exponentially weighted mean and standard deviation.
For a portfolio in shares the return series is either the \emph{constant-mix} series
\eqref{eq:portret} with today's value weights (the natural benchmark of a rebalanced
allocation) or the \emph{buy-and-hold} series $\ln(V_t/V_{t-1})$ with $V_t=\sum_iP_i(t)n_i$
(the share numbers held over the whole history). Both are available; the API's
\code{/performance/portfolio} defaults to constant mix.
% ============================================================================================
\section{Stock-price models and scenario generation}
\label{sec:models}
% ============================================================================================
The optimiser and the scenario-based portfolio risk need a joint distribution of the horizon
log-returns of the candidate stocks. Two models are implemented
(\code{quantcore.models.simulation}); both assume that one-period log-returns are i.i.d.\ (no
autocorrelation), so that an $h$-period return is the sum of $h$ independent one-period draws,
both use the \emph{latest} covariance $\Sigma_T$ of a \code{CovarianceModel} (rolling window or
EWM, with shrinkage), and both take the expected returns $\mu$ from the expected-return model of
Section~\ref{sec:er} (converted to the log-drift $m$ as described in Section~\ref{sec:sampling-mm}).
The output is an $S\times N$ matrix of horizon log-returns $r_{s,i}$, optionally with scenario
probabilities, together with its provenance (model, covariance model and the shrinkage actually
applied, sampling scheme, seed, horizon, date, a fingerprint of the matrix), the \code{ScenarioSet}.
\subsection{Geometric Brownian motion}
Under multivariate GBM the horizon log-return is multivariate normal,
\begin{equation}
r^{(h)} \;\sim\; N\big(h m,\; h\Sigma_T\big),
\label{eq:gbm}
\end{equation}
sampled as $r_s=hm+\sqrt h\,A z_s$ with $z_s\sim N(0,I)$ and $AA^\top=\Sigma_T$ (the
eigen-root $V\Lambda^{1/2}$ with eigenvalues descending by default, so that the first coordinate
of $z_s$ drives the market factor; the symmetric and Cholesky roots give the same distribution).
The draws $z_s$ are scrambled Sobol' points passed through $\Phi^{-1}$ and moment-matched
(Section~\ref{sec:sampling-mm}), so mean and covariance of the set are exact. GBM is the natural
partner of the Gaussian risk figures of Section~\ref{sec:risk} and of mean-variance thinking; it
has no fat tails, no skewness and no tail dependence.
\subsection{Empirical innovations (filtered historical simulation)}
\label{sec:fhs}
The second model keeps the shape of the observed return distribution and only rescales it to
today's covariance. It is the multivariate form of filtered historical simulation
\cite{BaroneAdesi1999}.
\paragraph{Filtering.} For every date $t$ after the warm-up of the estimator, the return vector
is standardised with the covariance known at the end of the previous day,
\begin{equation}
\varepsilon_t \;=\; \Sigma_{t-1}^{-1/2}\, r_t ,
\label{eq:innov}
\end{equation}
where $\Sigma_{t-1}$ comes from the streamed sequence \eqref{eq:ewmrec} (or the rolling
recomputation) and $\Sigma^{-1/2}$ is the \emph{symmetric} inverse root $V\Lambda^{-1/2}V^\top$
from the eigendecomposition $\Sigma=V\Lambda V^\top$ (eigenvalues floored at $10^{-10}$ of the
largest; the Cholesky root is available and used automatically above 250 stocks, where the
$O(TN^3)$ eigendecompositions become slow). If the model were exactly right, the innovations would
be uncorrelated with unit variance --- and, unlike Gaussian noise, they carry the empirical fat
tails, skewness and non-linear co-movements of the data.
\paragraph{Resampling.} With $A=\Sigma_T^{1/2}$ (symmetric root) and log-drift $m$, the $s$-th
horizon scenario draws $h$ dates $j_1,\dots,j_h$ with replacement from the innovation history
and sets
\begin{equation}
r_s \;=\; hm + A\sum_{k=1}^{h}\tilde\varepsilon_{j_k} ,
\label{eq:fhs}
\end{equation}
where $\tilde\varepsilon$ are the centred and re-standardised innovations described next. Since
the $h$ draws are independent with identity covariance, $\Cov(r_s)=hA A^\top=h\Sigma_T$,
consistent with \eqref{eq:gbm}, while the marginal shape of every asset and the joint tail
behaviour come from the data. The dates are drawn by scrambled Sobol' points in $h$ dimensions
mapped to the ranks of the innovations sorted by their market-factor component (so that every
day of the horizon covers the loss distribution evenly), alternatively uniformly (\code{"mc"})
or balanced (every date equally often); the resampled sums are moment-matched to mean zero and
covariance $hI$ (Section~\ref{sec:sampling-mm}).
\paragraph{Centring and re-standardisation.} The innovations are centred so that the scenario
mean equals the supplied drift exactly (the drift is the expected-return model's job, not the
sample mean's). Their variance, however, is systematically \emph{above one}: any lagged
volatility filter is too low right after a shock, so the standardised returns of the following
days are too large --- the classical reason why filtered historical simulation rescales the
residuals before resampling \cite{BaroneAdesi1999}. Measured on the platform's data set (daily
CHF returns 2016--2026, EWM $\lambda=0.99$, automatic shrinkage, symmetric root):
\begin{center}
\small
\begin{tabular}{@{}lccc@{}}
\toprule
number of stocks $N$ & 10 & 30 & 100 \\
\midrule
median variance of the raw innovations & 1.11 & 1.25 & 1.61 \\
scenario volatility / model volatility without re-standardisation & 1.06 & 1.13 & 1.28 \\
\bottomrule
\end{tabular}
\end{center}
The growth with $N$ is the conditioning effect of Section~\ref{sec:shrinkage}: with an
effective window of 100 days and 100 stocks, $\Sigma_{t-1}$ is nearly singular and its inverse
root inflates the directions it underestimates. The implementation therefore rescales the
centred innovation sample before resampling: \code{standardize="diagonal"} (default) divides
each asset's innovations by their sample standard deviation, \code{"full"} whitens them with
the inverse root of their sample covariance (only when the history has at least $3N$ dates).
After rescaling, the per-asset scenario volatility matches the model volatility within
$\pm2\,\%$ (diagonal) or $\pm0.5\,\%$ (full), and the scenario correlations match the model's
with a mean absolute error of 0.02 (diagonal) or 0.004 (full). The diagonal option leaves every
asset's marginal distribution intact and works for any $N$, which is why it is the default.
Finally, innovations beyond $\pm8$ are winsorised as a numerical guard against the same
conditioning effect (about 0.2\,\% of the values on the data set); this is a safeguard, not a
model choice, and it is reported.
\subsection{Sampling, moment matching, the drift convention and importance sampling}
\label{sec:sampling-mm}
\paragraph{Why not plain Monte Carlo.} The optimiser is a sample-average approximation: it sees
the model only through the scenario set, and the sample mean of a plain Monte-Carlo set of
$S$ scenarios is off by $\sigma_i/\sqrt S$ per asset --- for $S=2\,000$ and weekly volatilities
of 3--11\,\% that is 3--13\,\% per year, the size of the expected returns themselves. Measured
on the 30 largest stocks, two seeds then give portfolios that differ by 100\,\% of wealth and a
true expected profit 10\,\% below the optimum. The remedies (all in
\code{quantcore.models.sampling}, derived and measured in the optimisation note) are:
scrambled Sobol' points instead of pseudo-random draws ($S$ is rounded up to a power of two);
\emph{moment matching}, the affine transform $\tilde z_s=C^{-1/2}(z_s-\bar z)$ of the sample
that makes its (probability-weighted) mean and covariance exactly $0$ and $I$, so that every
scenario set has mean $hm$ and covariance $h\Sigma_T$ exactly and only the shape of the tail is
sampled; and the model expected returns in the optimiser's objective instead of the scenario
mean. With these, the turnover between two seeds falls to 4--6\,\%, the flatness of the optimum.
\paragraph{Drift convention.} The expected-return model delivers \emph{arithmetic} expected
returns $\mu$ (Section~\ref{sec:er}): reverse optimisation and Black--Litterman are statements
about simple returns. The scenarios are generated in log-returns and turned into money with
$R=e^{r}-1$, whose expectation under normality is $e^{hm+h\sigma^2/2}-1$. The log-drift is
therefore the It\^o-corrected
\begin{equation}
m_i \;=\; \frac{\ln(1+h\mu_i)}{h}-\frac{\sigma_i^2}{2},
\qquad\text{so that}\qquad \E\big[e^{r^{(h)}_i}-1\big]=h\mu_i
\label{eq:ito}
\end{equation}
exactly (\code{drift\_convention="arithmetic"}, the default). Using $\mu_i$ itself as the
log-drift (\code{"log"}) adds $\sigma_i^2/2$ to the expected simple return: on the data set the
median bonus over the 30 largest stocks is 5.4\,\% per year against a median $\mu$ of 8.9\,\%,
and for the most volatile names it is 20--34\,\% (Micron: $\mu=16.9\,\%$ would become
$65.6\,\%$) --- a strong and unintended tilt towards volatile stocks. The scenario set reports
the expected simple return per asset (\code{expected\_simple}) that the optimiser uses.
\paragraph{Importance sampling.} Only the $(1-\beta)S$ scenarios in the tail matter for the
CVaR. By default (\code{importance\_sampling="auto"}) the sampling distribution is therefore
tilted towards the losses of the \emph{cap-weighted portfolio of the candidate stocks}: its loss
direction is $d=-A^\top v/\|A^\top v\|$, and two blocks of a quarter of the scenarios each are
shifted along $d$ by $z_{0.95}$ and $z_{0.99}$ (GBM: $z_s\mapsto z_s+c_kd$; empirical model: the
dates are drawn with exponentially tilted probabilities
$q^k_t\propto\exp(\vartheta_k d^\top\tilde\varepsilon_t)$), so that one set covers both usual
confidence levels. The shift is a covariance statement: in return space it is
$-c_k\Sigma v/\sqrt{v^\top\Sigma v}$, so each asset moves by its covariance with the reference
portfolio --- a high-beta stock is stressed hard, a negatively correlated one moves up --- and,
an exponential tilt of a Gaussian being a mean shift, the weighted set still has exactly the
model covariance and correlations. Every scenario carries the self-normalised likelihood ratio of the mixture
proposal as its probability $p_s$, bounded by $1/(1-\sum_k\varepsilon_k)$; the weighted set is
moment-matched with these probabilities, the optimiser uses $p_s$ in its CVaR rows and the risk
figures of Section~\ref{sec:risk} use the weighted tail. At equal $S$ the tail noise falls by a
factor 2--4; at $\beta=0.99$ 2\,048 tilted scenarios are as good as 8\,192 plain ones. Because
the direction comes from the market portfolio rather than from a solution of the optimisation,
the scenario set does not depend on the risk limit, the constraints or the confidence level ---
one set serves a whole family of runs (\code{"adaptive"} tilts towards a first-pass solution
instead, which is sharper at $\beta=0.99$ but gives up that property).
\paragraph{Which model when.} GBM is the transparent baseline; the empirical model is the
default of the optimisation because CVaR is a tail measure and the empirical tails are heavier
than Gaussian ones (the innovation kurtosis on the data set is well above zero). Both models
produce identical second moments by construction, so differences in the optimised portfolios
are entirely due to the tail shape.
\paragraph{Cost.} The filter needs one eigendecomposition per date: for 100 stocks and 2\,500
dates about two seconds, for 745 stocks with the Cholesky root and 1\,500 dates about six
seconds (four BLAS threads). The innovations depend only on the data and the covariance model,
so they can be cached between optimisations with different views or constraints.
% ============================================================================================
\section{The expected-return model}
\label{sec:er}
% ============================================================================================
The scenario drift $\mu$ --- and therefore the mean side of the mean--CVaR trade-off --- comes
from a Black--Litterman model (\code{quantcore.models.black\_litterman}) on \emph{weekly} CHF
log-returns over the last ten years. Weekly data reduce the asynchronous-trading noise of the
daily union calendar, and ten years give $\approx520$ observations for the long-term covariance.
Everything in this section is per week; annual figures are $52\times$ the means and
$\sqrt{52}\times$ the volatilities.
\subsection{Market capitalisation, weights and the broad-market average}
The market capitalisation of stock $i$ in CHF is
$M_i(t)=\text{shares outstanding}_i\times P_i(t)\times X_{c(i)}(t)$ with today's shares
outstanding held constant back in time. The \emph{cap-weighted broad market index} of the whole
universe has the log-return
\begin{equation}
r_M(t) \;=\; \ln\Big(1+\sum_i \omega_i(t-1)\,\big(e^{r_i(t)}-1\big)\Big), \qquad
\omega_i(t-1)=\frac{M_i(t-1)}{\sum_jM_j(t-1)},
\label{eq:index}
\end{equation}
i.e.\ \eqref{eq:portret} with weights that drift with prices between rebalancings, exactly like
an index. Its average over the ten-year window, converted to an arithmetic expectation,
$\mu_M=\overline{r_M}+\tfrac12\widehat{\Var}(r_M)$, is the platform's estimate of the
expected market return ("the average of a broad market weighted by current capitalisation");
on the data set as of 2026-09-21 the mean log-return is 11.5\,\% per year with a volatility of
16.2\,\%, i.e.\ $\mu_M=12.9\,\%$. The current weights $w_i=M_i(T)/\sum_jM_j(T)$, renormalised over the candidate stocks
of an optimisation, are the \emph{market weights} of the model.
\subsection{Reverse optimisation: the equilibrium returns}
A mean-variance investor with risk aversion $\delta$ solves
$\max_w\; w^\top\mu-\frac{\delta}{2}w^\top\Sigma w$, whose first-order condition is
$\mu=\delta\Sigma w$. Black and Litterman \cite{BlackLitterman1992,HeLitterman1999} read this
backwards: if the market portfolio $w$ is the optimal portfolio of the representative investor,
the excess returns that make it optimal are the \emph{implied equilibrium returns}
\begin{equation}
\pi \;=\; \delta\,\Sigma\,w, \qquad
\delta \;=\; \frac{\mu_M - r_f}{w^\top\Sigma w},
\label{eq:pi}
\end{equation}
where $\delta$ is chosen such that the market portfolio itself earns exactly the market excess
return: $w^\top\pi=\delta\,w^\top\Sigma w=\mu_M-r_f$. Here $\Sigma$ is the ten-year weekly
covariance of the candidate stocks and $r_f$ the weekly CHF risk-free rate (0 by default). The
equilibrium returns are proportional to each stock's covariance with the market portfolio ---
its beta --- and are the neutral starting point: an investor without views holds the market.
On the data set, with the 100 largest stocks as candidates (their market portfolio has a
volatility of 19.7\,\%), $\delta\approx3.3$; the equilibrium returns range from about 2.7\,\%
(Unilever, Nestl\'e --- low beta in CHF) to about 23\,\% (Nvidia) per year.
\begin{remark}
$\mu_M$ is the average of the \emph{broad} index of all 745 stocks, whereas $w$ and $\Sigma$
refer to the candidate subset of an optimisation. This follows the specification (one
market-wide average) and keeps $\delta$ stable across candidate sets; the alternative of
using the candidate portfolio's own average is a one-line change.
\end{remark}
\subsection{The Black--Litterman posterior}
The model treats the (unknown) vector of expected excess returns $\mu$ as a random variable with
the equilibrium as prior, $\mu\sim N(\pi,\tau\Sigma)$, where the scalar $\tau$ (0.05 by default)
expresses how uncertain the equilibrium is relative to the return covariance. $K$ views are
linear statements about $\mu$ with their own uncertainty,
\begin{equation}
P\mu \;=\; Q+\varepsilon, \qquad \varepsilon\sim N(0,\Omega), \quad \Omega=\diag(\omega_1,\dots,\omega_K),
\label{eq:views}
\end{equation}
where each row $p_k$ of $P$ ($K\times N$) is a portfolio: $p_k=e_i$ for an \emph{absolute} view
("stock $i$ will return $q_k$"), $p_k=e_i-e_j$ for a \emph{relative} view ("$i$ will outperform
$j$ by $q_k$"). Combining prior and views by Bayes' rule (equivalently, generalised least squares)
gives the posterior distribution of $\mu$ with mean and covariance
\begin{equation}
\mu_{\mathrm{BL}} = \big[(\tau\Sigma)^{-1}+P^\top\Omega^{-1}P\big]^{-1}\big[(\tau\Sigma)^{-1}\pi+P^\top\Omega^{-1}Q\big], \qquad
\Sigma_\mu = \big[(\tau\Sigma)^{-1}+P^\top\Omega^{-1}P\big]^{-1}.
\label{eq:bl1}
\end{equation}
The implementation uses the algebraically identical form that needs no inverse of $\tau\Sigma$
(a $K\times K$ system only), which is numerically preferable and also valid when $\Sigma$ is
singular:
\begin{equation}
\mu_{\mathrm{BL}} = \pi + \tau\Sigma P^\top\big(P\tau\Sigma P^\top+\Omega\big)^{-1}(Q-P\pi), \qquad
\Sigma_{\mathrm{BL}} = \Sigma + \tau\Sigma - \tau\Sigma P^\top\big(P\tau\Sigma P^\top+\Omega\big)^{-1}P\tau\Sigma .
\label{eq:bl2}
\end{equation}
The posterior mean is the prior corrected in the direction of the views by the amount by which
they disagree with the prior ($Q-P\pi$), weighted by how confident they are relative to the
prior; $\Sigma_{\mathrm{BL}}=\Sigma+\Sigma_\mu$ is the covariance of the returns themselves
(return noise plus the remaining uncertainty about the mean) and is reported for completeness.
Without views, $\mu_{\mathrm{BL}}=\pi$. The posterior mean is the drift handed to the scenario
models: per day, $\mu=\mu_{\mathrm{BL}}^{\mathrm{ann}}/252$ (total return, i.e.\ excess plus
$r_f$).
\subsection{Idzorek's confidence: from a degree of belief to $\Omega$}
The weak point of the model is $\Omega$: nobody can state the variance of a view. Idzorek
\cite{Idzorek2005} replaces it by a \emph{confidence} $C_k\in(0,1]$ with a portfolio meaning.
Consider one view $k$ alone. At 100\,\% confidence ($\omega_k=0$) the posterior is
$\mu_{100}=\pi+\tau\Sigma p_k^\top(p_k\tau\Sigma p_k^\top)^{-1}(q_k-p_k\pi)$ and the
unconstrained mean-variance weights move from $w$ to $w_{100}=(\delta\Sigma)^{-1}\mu_{100}$.
Idzorek defines the confidence as the fraction of that move the investor wants to make:
the target weights are $w+C_k\,(w_{100}-w)$, and $\omega_k$ is the value for which the
posterior weights $w(\omega_k)=(\delta\Sigma)^{-1}\mu_{\mathrm{BL}}(\omega_k)$ hit the target
(in the paper by a one-dimensional least-squares search). For a single view this calibration
has a closed form. From \eqref{eq:bl2} with one view,
\begin{equation}
w(\omega_k)-w \;=\; (\delta\Sigma)^{-1}\tau\Sigma p_k^\top\,\frac{q_k-p_k\pi}{p_k\tau\Sigma p_k^\top+\omega_k}
\;=\; \frac{\tau}{\delta}\,p_k^\top\,\frac{q_k-p_k\pi}{p_k\tau\Sigma p_k^\top+\omega_k},
\end{equation}
so the tilt is always in the direction $p_k^\top$ and its size relative to the full tilt
($\omega_k=0$) is $p_k\tau\Sigma p_k^\top/(p_k\tau\Sigma p_k^\top+\omega_k)$. Setting this equal to
$C_k$ gives
\begin{equation}
\boxed{\;\omega_k \;=\; \frac{1-C_k}{C_k}\;p_k\,\tau\Sigma\,p_k^\top\;}
\label{eq:idzorek}
\end{equation}
\cite{Walters2014}: the view's variance is the prior variance of the view portfolio scaled by
the odds against the view; $C_k=1$ gives $\omega_k=0$ (the view is imposed), $C_k=0.5$ gives
$\omega_k=p_k\tau\Sigma p_k^\top$ (the He--Litterman choice, available as
\code{omega\_method="he\_litterman"}), $C_k\to0$ removes the view. The implementation uses
\eqref{eq:idzorek} for every view and then computes the joint posterior \eqref{eq:bl2}; the
numerical search of the paper is kept as \code{idzorek\_omega\_numeric} and a unit test shows
that both agree to $10^{-4}$. With several views, each $\omega_k$ is calibrated for its view
alone, as in the paper.
\begin{example}[Four assets]
\label{ex:bl}
Annual volatilities $(20,25,15,30)\,\%$, correlations
$\rho_{12}=0.5,\rho_{13}=0.3,\rho_{14}=0.2,\rho_{23}=0.4,\rho_{24}=0.1,\rho_{34}=0.3$, market
weights $(0.4,0.3,0.2,0.1)$, market return 7\,\%, $r_f=1\,\%$, $\tau=0.05$, weekly
covariance $\Sigma=\Sigma^{\mathrm{ann}}/52$. Then $w^\top\Sigma^{\mathrm{ann}}w=0.0250$
(market volatility 15.8\,\%), $\delta=0.06/0.0250=2.40$, and the equilibrium total returns
are $\pi+r_f=(7.36,\ 8.80,\ 4.35,\ 5.50)\,\%$ --- the market portfolio earns
$0.4\cdot7.36+0.3\cdot8.80+0.2\cdot4.35+0.1\cdot5.50=7.00\,\%$. An absolute view "asset 2
returns 15\,\%" with confidence $C=0.4$ gives $\omega=\frac{0.6}{0.4}\,p\tau\Sigma p^\top=1.5\times
6.01\cdot10^{-5}=9.01\cdot10^{-5}$ (weekly units; the numerical search returns the same value)
and the posterior total returns $(8.35,\ 11.28,\ 4.94,\ 5.80)\,\%$: asset 2 moves 40\,\% of the
way from 8.80\,\% to the 15\,\% it would reach at full confidence, and the correlated assets
follow. The unconstrained weights move from $0.3$ to $0.4656$ for asset 2 --- exactly
$0.3+0.4\,(0.7139-0.3)$ --- and stay unchanged for the others, as an absolute view on one
asset only tilts that asset. The numbers are pinned by \code{tests/test\_black\_litterman.py}.
\end{example}
\subsection{Views from the AI layer: sentiment with a degree of belief}
The platform's news pipeline delivers, per stock, an assessment "positive / negative" with a
degree of belief rather than a return forecast. \code{view\_from\_sentiment} turns
$(\text{stock } i,\ \text{sentiment } s\in[-1,1],\ \text{confidence } C)$ into the absolute view
\begin{equation}
q \;=\; \pi_i^{\mathrm{ann}} + r_f^{\mathrm{ann}} + s\cdot\gamma\cdot\sigma_i^{\mathrm{ann}}, \qquad \gamma=0.25 \text{ by default},
\label{eq:sentiment}
\end{equation}
i.e.\ a fully positive assessment claims one quarter of the stock's annual volatility above its
equilibrium return (about $+5\,\%$ for a 20\,\%-volatility stock), and the degree of belief
becomes the Idzorek confidence $C$. The strength $\gamma$ is a modelling parameter; the view
carries the provenance of the insight that produced it (\code{source}). On the data set, a
sentiment of $+0.8$ with confidence $0.6$ on Nestl\'e states $q=6.1\,\%$ against an equilibrium
of $2.7\,\%$, calibrates $\omega=1.8\cdot10^{-5}$ and lifts the posterior to $4.7\,\%$ per
year. Explicit views (\code{assets}, \code{q}, \code{confidence}), including relative ones, are
accepted alongside.
\subsection{Units and alternatives}
Views state \emph{annual total} returns; internally they are converted to weekly excess returns
($q/52$ minus the weekly $r_f$ times the sum of the view weights, which vanishes for relative
views). Results are reported per week and annualised; the optimiser receives the posterior
total return per day, $\mu_{\mathrm{BL}}^{\mathrm{ann}}/252$, as the \emph{arithmetic}
expected return, converts it to the It\^o-corrected log-drift \eqref{eq:ito} of \eqref{eq:gbm}
and \eqref{eq:fhs}, and uses the expected simple return over the horizon,
$h\mu_{\mathrm{BL}}^{\mathrm{ann}}/252$, in its objective. Inputs that are log-return means
(the index average, the historical means) are converted with $+\tfrac12\sigma^2$. Two simpler
models are available for comparison: \code{expected\_returns="historical"} (the ten-year weekly
means, converted) and \code{"zero"} (no drift, pure risk minimisation).
% ============================================================================================
\section{Implementation map}
% ============================================================================================
\begin{table}[h]
\centering
\footnotesize
\begin{tabular}{@{}p{3.2cm}p{5.6cm}p{6.4cm}@{}}
\toprule
concept & code & parameters (defaults) / output \\
\midrule
log-returns, alignment, frequencies, horizons & \code{measurement.returns}: \code{log\_returns}, \code{align\_prices}, \code{resample\_prices}, \code{horizon\_periods}, \code{horizon\_returns}, \code{portfolio\_log\_returns} & \code{freq} daily $|$ weekly $|$ monthly; \code{horizon} day $|$ week $|$ month $|$ integer \\
CHF conversion & \code{data.fx.FXConverter}; \newline \code{MarketDataService.prices}, \code{.returns} & \code{currency="base"} (CHF) $|$ \code{"local"} \\
rolling / EWM series & \code{estimators.rolling\_mean}, \code{rolling\_vol}, \code{ewm\_mean}, \code{ewm\_vol} & \code{window=200}, \code{lam=0.94}; annualised with the periods per year \\
covariance matrices & \code{estimators.rolling\_cov}, \code{ewm\_cov}, \code{covariance}, \code{cov\_to\_corr}, \code{covariance\_sequence}; \newline \code{models.covariance.CovarianceModel} & \code{method} rolling $|$ ewm, \code{window}, \code{lam}, \code{shrinkage} $\in[0,1]$ or \code{"auto"} \\
VaR / CVaR & \code{risk.normal\_var}, \code{normal\_cvar}, \code{historical\_var}, \code{historical\_cvar}, \code{scenario\_cvar}, \code{loss\_to\_money} & $\beta\in\{0.95,0.99\}$; losses positive; money via $V_0(1-e^{-\ell})$ \\
per-asset risk table & \code{RiskService.asset\_risk} $\to$ \code{asset\_risk\_table} & columns \code{mean\_rolling\_ann}, \code{vol\_rolling\_ann}, \code{vol\_ewm\_ann}, \code{var95\_normal\_rolling}, \code{cvar95\_normal\_rolling}, \code{var95\_normal\_ewm}, \code{cvar95\_normal\_ewm}, \code{var95\_hist}, \code{cvar95\_hist} (and the same for 99), \code{horizon\_periods}, \code{n\_obs}, \code{as\_of} \\
risk time series & \code{RiskService.risk\_series} & same names, one row per date \\
portfolio risk & \code{RiskService.portfolio\_risk} $\to$ \code{PortfolioRisk} & \code{holdings} (shares), \code{cash}, \code{method} normal $|$ historical $|$ scenarios, \code{cov\_method}; per level \code{var}, \code{cvar}, \code{var\_money}, \code{cvar\_money} \\
performance & \code{performance\_table}, \code{rolling\_performance}, \code{ewm\_performance}, \code{PerformanceService} & \code{risk\_free} from the settings; \code{rebalanced=True} = constant mix \\
scenario models & \code{models.simulation.GBMSimulator}, \newline \code{EmpiricalInnovationSimulator} $\to$ \code{ScenarioSet} & \code{n\_scenarios=4096}, \code{seed=42}, \code{sampling} sobol $|$ mc $|$ antithetic $|$ lhs $|$ balanced, \code{moment\_match=True}, \code{tilt} (\code{TiltSpec}: direction, shifts, mixtures), \code{drift\_convention} arithmetic $|$ log, \code{root}, \code{standardize} diagonal $|$ full $|$ None, \code{clip=8}, \code{max\_history=2500}; output \code{probabilities}, \code{expected\_simple}, \code{hash} \\
sampling schemes & \code{models.sampling}: \code{standard\_normal\_sample}, \code{moment\_match}, \code{pca\_root}, \code{tilt\_normal\_sample}, \code{tilted\_resample}, \code{resample\_indices}, \code{scenario\_hash} & Sobol' sizes are powers of two; likelihood ratios bounded by $1/(1-\sum_k\code{mixture}_k)$ \\
one set per family of runs & \code{OptimizationService.scenario\_key}, \code{.market\_exposure}, \code{.clear\_scenario\_cache}; \code{ScenarioSet.with\_drift}, \code{.shocks}, \code{.shock\_hash} & the key excludes risk limit, confidence, constraints, costs, objective, cash, holdings and the expected-return model \\
market index and weights & \code{MarketDataService}: \newline \code{cap\_weighted\_index}, \code{market\_caps}, \code{market\_weights} & shares outstanding $\times$ price $\times$ FX \\
Black--Litterman & \code{BlackLittermanModel}, \code{View}, \code{view\_from\_sentiment}, \code{idzorek\_omega} $\to$ \code{BlackLittermanResult} & \code{tau=0.05}, \code{periods\_per\_year=52}, \code{omega\_method} idzorek $|$ he\_litterman; \code{prior\_ann}, \code{posterior\_ann}, \code{omega}, \code{delta}, \code{market\_return\_ann} \\
pipeline in the optimiser & \code{OptimizationService}: \newline \code{expected\_returns}, \code{scenarios} & \code{bl\_years=10}, \code{expected\_returns} black\_litterman $|$ historical $|$ zero, \code{views}, \code{covariance} = \{ewm, 0.99, auto\}, \code{scenario\_model} empirical $|$ gbm $|$ gaussian\_exact, \code{importance\_sampling} auto $|$ market $|$ adaptive $|$ off, \code{row\_generation} auto $|$ on $|$ off \\
HTTP & \code{/risk/*}, \code{/performance/*}, \code{/model/expected-returns}, \code{/model/scenarios}, \code{/market/*} & see \code{/docs} \\
\bottomrule
\end{tabular}
\caption{Where each concept lives in \code{quantcore}.}
\label{tab:impl}
\end{table}
\paragraph{Determinism and provenance.} Every figure is a deterministic function of the stored
data and the stated parameters; simulations take an explicit seed. Every result carries the
parameters that produced it (window, $\lambda$, shrinkage applied, model, seed, horizon, date,
views with their calibrated $\omega$), so that the advisor layer can cite them and a reviewer
can recompute them.
% ============================================================================================
\begin{thebibliography}{99}
\bibitem{RiskMetrics1996}
J.P.~Morgan/Reuters, \emph{RiskMetrics --- Technical Document}, 4th edition, New York, 1996.
\bibitem{LedoitWolf2004}
O.~Ledoit and M.~Wolf, \emph{A well-conditioned estimator for large-dimensional covariance
matrices}, Journal of Multivariate Analysis 88(2), 365--411, 2004.
\bibitem{SchaeferStrimmer2005}
J.~Sch\"afer and K.~Strimmer, \emph{A shrinkage approach to large-scale covariance matrix
estimation and implications for functional genomics}, Statistical Applications in Genetics and
Molecular Biology 4(1), Article 32, 2005.
\bibitem{AcerbiTasche2002}
C.~Acerbi and D.~Tasche, \emph{On the coherence of expected shortfall},
Journal of Banking \& Finance 26(7), 1487--1503, 2002.
\bibitem{MFE2015}
A.\,J.~McNeil, R.~Frey and P.~Embrechts, \emph{Quantitative Risk Management: Concepts,
Techniques and Tools}, revised edition, Princeton University Press, 2015.
\bibitem{BaroneAdesi1999}
G.~Barone-Adesi, K.~Giannopoulos and L.~Vosper, \emph{VaR without correlations for portfolios
of derivative securities}, Journal of Futures Markets 19(5), 583--602, 1999.
\bibitem{BlackLitterman1992}
F.~Black and R.~Litterman, \emph{Global portfolio optimization},
Financial Analysts Journal 48(5), 28--43, 1992.
\bibitem{HeLitterman1999}
G.~He and R.~Litterman, \emph{The intuition behind Black--Litterman model portfolios},
Goldman Sachs Investment Management Research, 1999.
\bibitem{Idzorek2005}
T.\,M.~Idzorek, \emph{A step-by-step guide to the Black--Litterman model: incorporating
user-specified confidence levels}, working paper, Ibbotson Associates, 2005 (also in
S.~Satchell (ed.), \emph{Forecasting Expected Returns in the Financial Markets}, 2007).
\bibitem{Walters2014}
J.~Walters, \emph{The Black--Litterman model in detail}, working paper (SSRN), 2014.
\end{thebibliography}
\end{document}