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>
808 lines
50 KiB
TeX
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}
|