You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This document follows one trading day through the system and explains the
mathematics at each stage. File names point to the code that implements it.
Notation: $r_{i,t}$ is stock $i$'s return on day $t$, $N$ is the number of
stocks, and $T$ the number of days. "Cross-sectional" means across stocks on
one day.
1. Statistical factors: PCA eigenportfolios
qtrade/features/factors.py
Stocks move together, mostly because of a few common forces (the market,
sectors, styles). Statistical arbitrage bets on the part of each move that is
not explained by those forces.
Take the last $W = 252$ days of returns and standardize each stock:
$Z_{t,i} = (r_{i,t} - \bar r_i)/\sigma_i$. The empirical correlation matrix is
$\rho = \frac{1}{W} Z^\top Z$. Its eigenvectors $v^{(1)}, \dots, v^{(k)}$ (computed
through the SVD of $Z$) are the directions of greatest common variation. Each one
becomes a portfolio with weights $Q_{ij} = v^{(j)}_i / \sigma_i$, called an
eigenportfolio. The first is roughly "the market"; the next ones look like
sectors and styles. Their returns are the factors:
$$F_{j,t} = \sum_i Q_{ij}, r_{i,t}.$$
Regress each stock on the factors over the window,
$r_{i,t} = \alpha_i + \beta_i^\top F_t + \varepsilon_{i,t}$, and on later days define
the residual$e_{i,t} = r_{i,t} - \beta_i^\top F_t$. The loadings
$B = [\beta_1 \dots \beta_N]^\top$ are refit monthly. The residuals for the following
month use only the loadings estimated at the refit date, so they use no
future information.
2. The Ornstein–Uhlenbeck s-score
ou_sscore in qtrade/signals/library.py
Avellaneda and Lee model the cumulative residual $X_t = \sum_{s \le t} e_s$ as
a mean-reverting Ornstein–Uhlenbeck process:
$$dX_t = \kappa(m - X_t),dt + \sigma, dW_t .$$
In discrete time this is an AR(1), $X_{t+1} = a + b X_t + \eta_{t+1}$, with
$b = e^{-\kappa \Delta t}$. Fitting $a, b$ by rolling OLS over 60 days gives:
$s$ measures how many equilibrium standard deviations the stock is from fair
value. The signal is $-s$: buy stocks that are cheap relative to their
factors. It is only used when the half-life $\ln 2 / \kappa$ is under about 30
days ($0 < b < e^{-1/30}$).
A useful detail: subtracting a constant $c$ from every $X_t$ leaves $b$ unchanged
and shifts $m$ by exactly $c$. So $s$ does not depend on where the
cumulative sum starts, and one global running sum can serve every
rolling window.
3. Hidden Markov regimes
qtrade/models/hmm.py, qtrade/models/regime.py
The market has hidden states $s_t \in {1..K}$ that follow a Markov chain with
$P(s_t = j \mid s_{t-1} = i) = A_{ij}$. We observe symptoms
$x_t$ (market return, log realized volatility, log dispersion, correlation
share), with $x_t \mid s_t = k \sim \mathcal N(\mu_k, \Sigma_k)$.
Forward recursion (filtering): the only part used for trading.
Each step is normalized to sum to 1. The normalizing constants $c_t$ give the
log-likelihood $\log p(x_{1:T}) = \sum_t \log c_t$ without numerical underflow.
Backward recursion and smoothing: used only for fitting.
$\beta_t(i) = \sum_j A_{ij}, \mathcal N(x_{t+1}; \mu_j, \Sigma_j), \beta_{t+1}(j) / c_{t+1}$, and
$\gamma_t(k) = P(s_t = k \mid x_{1:T}) \propto \alpha_t(k)\beta_t(k)$.
Baum–Welch (EM): repeat until the likelihood stops improving.
EM only finds a local optimum, so it is run from several starting points
(spread out k-means++-style) and the best likelihood wins. The model is refit
once a year on an expanding window, warm-started from the last fit. States are
sorted by volatility so that "state 0 = calm" keeps the same meaning across
refits. The expected time spent in a regime is $1/(1 - A_{kk})$ days.
Target.$y_{i,t}$ is stock $i$'s return from the open of $t+1$ to the open of
$t+1+h$ (with $h = 5$), ranked across stocks and mapped to normal scores
$\Phi^{-1}(\text{rank}/N)$. Ranking removes the market move and tames fat tails.
Ridge regression.$\hat b = (X^\top X + \lambda n I)^{-1} X^\top y$, with
$\lambda$ chosen by forecasting the most recent 20% of the training window
from the earlier 80%. A five-day gap separates the two parts, so overlapping
labels can't leak between them.
Regime ridge. The features are the products $x_{i,t} \otimes p_t$, where $p_t$ is the
vector of HMM probabilities. The forecast is then
$\sum_k p_{k,t}, x_{i,t}^\top b_k$: each regime gets its own signal weights,
blended by how sure we are of the regime.
Kernel ridge with random Fourier features. The Gaussian kernel
$k(x, x') = e^{-|x-x'|^2 / 2\ell^2}$ is the Fourier transform of a Gaussian
(Bochner's theorem). So with $w \sim \mathcal N(0, \ell^{-2} I)$ and
$b \sim U[0, 2\pi]$, the features
$z(x) = \sqrt{2/D}, \cos(Wx + b)$ satisfy $\mathbb E[z(x)^\top z(x')] = k(x,x')$.
Ridge on $z$ is therefore approximately kernel ridge regression: a nonlinear
model at linear cost.
IC weighting. For each signal $j$ and day $t$, the information coefficient
$IC_{j,t} = \operatorname{corr}(x_{\cdot j t}, y_{\cdot t})$. The weight is the
information ratio $\overline{IC}_j / \operatorname{sd}(IC_j)$, shrunk to zero
by the factor $\max(0, 1 - 4/t_j^2)$ when its t-statistic is small.
Walk-forward. Models are refit every 63 days on the previous 1,260 days.
The label for day $d$ is only known at the open of $d+1+h$, so a refit on day
$r$ trains on rows $d \le r - 1 - h$. Nothing after $r$ is ever seen.
Expected returns: Grinold's formula. If a forecast's cross-sectional
z-score $z_i$ has information coefficient $IC$, its best linear prediction of
return is
$$\alpha_i = IC \cdot \sigma_i \sqrt{h} \cdot z_i ,$$
where $\sigma_i$ is the stock's daily idiosyncratic volatility. $IC$ is the
model's own realized out-of-sample IC over the past year, treated as a
Bayesian average with a prior of 0.01 worth 252 pseudo-days:
A model whose edge disappears is therefore sized down automatically, to zero
if its IC turns negative.
Kelly with neutrality. Maximizing expected log-growth to second order,
$\max_w\ w^\top\alpha - \tfrac12 w^\top \Sigma w$, subject to $C^\top w = 0$ where
$C = [\mathbf 1, B]$ (zero net dollars and zero exposure to every PCA factor).
With the factor model $\Sigma = B F B^\top + D$, the constraint $B^\top w = 0$
cancels the factor term entirely. Using a Lagrangian:
This is a closed form, with no numerical optimizer needed.
Leverage. The scale $s = \min(\tfrac12,\ \sigma_{target}/\sigma(w^),\ G_{max}/|w^|_1)$
combines half-Kelly, a 10% annual volatility target, and the 2× gross cap.
Names over the 2% cap are clipped. The book is projected back onto the
neutral subspace $w \leftarrow w - C(C^\top C)^{-1}C^\top w$, and exposure is
refilled across the uncapped names ("water-filling").
The no-trade band. Trading stock $i$ costs about $c_i$ per unit of
weight (spread + slippage + impact). The single-name problem
$$\max_w\ a w - \tfrac{\gamma}{2} d w^2 - c,|w - w_0|$$
has the solution: keep $w_0$ if it lies in
$\big[\frac{a - c}{\gamma d}, \frac{a + c}{\gamma d}\big]$; otherwise trade to the nearest
edge. Because a position is held for several forecast periods, $c$ is
amortized over $L = 1/(1-\rho)$ periods, where $\rho$ is the rank
autocorrelation between today's forecast and the forecast $h$ days ago.
On the synthetic market, the unscaled band beat both
lighter and heavier cost penalties.
6. Costs
qtrade/backtest/costs.py
Trading $Q$ dollars of a stock with daily volatility $\sigma$ and average daily
dollar volume $V$ costs
Deflated Sharpe ratio. Trying $N$ strategies and keeping the best inflates
its Sharpe even when none has skill. The expected maximum Sharpe of $N$
worthless strategies is approximately
where $\gamma \approx 0.5772$ is the Euler–Mascheroni constant. DSR is $PSR(SR^*)$:
the probability that the chosen strategy beats the best of $N$ duds. The report
computes it from the Sharpe ratios of all seven models it tried.