Exponentially weighted estimators¶
Author: Artur Sepp
Implemented in qis — Quantitative Investment Strategies. Software citation: CITATION.cff.
An exponentially weighted (EWM) estimator replaces an equal-weight window by weights \((1-\lambda)\lambda^k\) on the observation \(k\) rows old. In qis a single recursion, \(m_t=\lambda m_{t-1}+(1-\lambda)x_t\), produces every such estimator: run on observations it is a mean, on squares a variance, on outer products a covariance matrix and on cross products a beta. This chapter derives the properties of the weights, states how the implementation seeds the recursion, removes means and treats missing rows, and proves the results that users of the estimators rely on. The contracts are those of the current release; where a release changed a number, the section says what changed.
Overview¶
The chapter answers five questions.
How much history does a span use? A span \(N\) has decay \(\lambda=1-2/(N+1)\), mean lag \((N-1)/2\), effective sample size \(N\) and half-life close to \(0.35N\).
What does the recursion start from, and what does it do at a gap? The seed is the state before a column’s first observation, the missing-data policy is an argument, and two seeds use the full sample.
Which second moment is estimated? By default a second moment about zero; a centred estimate needs an explicit mean adjustment.
Which properties survive? An EWM covariance matrix is positive semidefinite for complete data and, under the default missing-data policy of the covariance functions, with gaps. The EWM Newey–West variance is non-negative by construction.
How are EWM outputs normalised? Unit-variance scaling, the two-span trend filter, EWM Sharpe ratios and the EWM score each have an exact definition, stated here.
Question |
qis entry point |
Output |
|---|---|---|
Smoothed level or mean |
|
EWM mean \(m_t\), same container as the input |
Volatility |
|
\(\hat\sigma_t\) per period, or annualised |
Volatility corrected for autocorrelation |
|
Newey–West EWM volatility and its variance ratio |
Covariance matrix at the last date or at every date |
|
\(n\times n\) matrix or \(T\times n\times n\) tensor |
Covariance from vol-normalised returns |
|
Covariance tensor, normalised tensor, vols |
Beta, cross moment or correlation |
|
Time series per column |
Trend signal |
|
Unit-variance two-span filter |
Running Sharpe ratio |
|
Annualised EWM mean or Sharpe path |
Outlier score and cleaning |
|
Score; cleaned series |
Inputs, notation, and assumptions¶
Convention |
This article |
|---|---|
Return basis |
Any input series; the estimators do not transform it. The examples use simple returns |
Sampling grid |
The rows of the input. The decay applies per row, so a span counts observations, not calendar time |
Annualisation |
Off by default. With |
Mean adjustment |
|
Timing |
An estimate dated \(t\) uses rows up to and including \(t\) and applies from \(t+1\). The seeds |
Output units |
Per period in the units of the input: \(x\) for a mean or volatility, \(x^2\) for a variance; betas and correlations are dimensionless |
qis default |
|
Symbol or input |
Meaning |
Units and convention |
|---|---|---|
\(x_t\), \(y_t\) |
Input observations at row \(t\): returns, signals or residuals |
Units of the input; rows \(t=0,\dots,T-1\) |
\(\lambda\), \(N\) |
EWM decay and span, \(\lambda=1-2/(N+1)\) |
|
\(H\) |
Half-life, \(\lambda^H=1/2\) |
Rows |
\(\omega_k\) |
Stationary weight on the observation \(k\) rows old |
Dimensionless; the weights sum to one |
\(\bar k\), \(N_{\mathrm{eff}}\) |
Mean lag and Kish effective sample size of the weights |
Rows; observations |
\(m_t\), \(m_{-1}\) |
EWM mean and its seed, the state before the first observation |
Units of \(x\) |
\(t_0\) |
Row of a column’s first finite observation |
Rows |
\(\mathcal{E}_\lambda(X)_t\) |
EWM of a series \(X\) with decay \(\lambda\) and a zero seed |
Units of \(X\) |
\(\hat\mu_t\), \(\tilde x_t\) |
Mean removed under |
Units of \(x\) |
\(v_t\), \(\hat\sigma_t\) |
EWM second moment of \(\tilde x\) and its square root |
Per period unless annualised |
\(\underline v_t\), \(\gamma\) |
Rolling quantile floor of \(v_t\) and its level |
|
\(\hat\Sigma_t\) |
EWM covariance matrix |
Per period; seed \(\hat\Sigma_{-1}\) is |
\(\Gamma_t\), \(\hat\rho_t\) |
EWM covariance of vol-normalised returns; its correlation matrix |
Dimensionless |
\(M^{xy}_t\), \(M^{xx}_t\), \(M^{yy}_t\) |
EWM cross and second moments |
Units of \(xy\), \(x^2\), \(y^2\) |
\(\hat\beta_t\) |
EWM beta |
Units of \(y\) per unit of \(x\) |
\(\xi_t\) |
IID noise with mean zero and unit variance |
Used in the variance results |
\(N_L\), \(N_S\), \(\lambda_L\), \(\lambda_S\) |
Long and short spans and decays of the two-span filter |
\(1\le N_S<N_L\) |
\(\kappa\), \(F_t\) |
Normaliser and output of the two-span filter |
Dimensionless |
\(X_t\) |
Cumulative sum \(\sum_{j=0}^{t}x_j\), with \(X_{-1}=0\) |
Log price relative when \(x\) are log returns |
\(q\), \(c_{k,t}\) |
Newey–West lag count ( |
Rows; units of \(x^2\) |
\(K(k)\), \(g_s\) |
Bartlett kernel \(\max(0,1-\lvert k\rvert/(q+1))\); EWM-weighted observation |
Dimensionless; units of \(x\) |
\(\eta\) |
Normalising constant of |
Dimensionless |
\(c\) |
Volatility floor of the EWM score; a score threshold in the outlier filters |
Units of \(x\); dimensionless |
\(z_t\) |
EWM score |
Dimensionless |
\(h\) |
Horizon of |
A whole number of rows, from |
Rows are counted from \(t=0\), the first row of the input. Unless stated otherwise the input is a panel without missing rows; the section on missing data states what changes when it has them. Every estimate dated \(t\) includes the observation at \(t\). To use one inside a backtest, apply it from \(t+1\), and prefer seeds and mean adjustments that are point in time.
Methodology¶
The recursion and its weights¶
Definition. Given a decay \(\lambda\in[0,1)\) and a seed \(m_{-1}\), the EWM of \(x\) is
The seed is the state before row 0, and \(x_0\) updates it like every later observation. With a
span \(N\) the decay is \(\lambda=1-2/(N+1)\); span overrides ewm_lambda in every function of
the chapter. The default ewm_lambda=0.94 is the daily decay of RiskMetrics (J.P. Morgan and
Reuters, 1996), a span of about 32.3 rows.
Identity (unrolled recursion). For \(t\ge -1\),
Proof. The case \(t=-1\) is the seed. If the identity holds at \(t-1\), substituting it into \(\lambda m_{t-1}+(1-\lambda)x_t\) multiplies every earlier weight by \(\lambda\) and adds weight \(1-\lambda\) on \(x_t\). \(\square\)
The observation weights and the seed weight sum to \(\lambda^{t+1}+(1-\lambda)(1-\lambda^{t+1})/(1-\lambda)=1\). As \(t\) grows the seed weight vanishes and the weights approach the stationary weights
With the seed InitType.X0, \(m_{-1}=x_0\), so \(m_0=x_0\) and the recursion is exactly pandas’
Series.ewm(span=N, adjust=False).mean(): pandas sets its smoothing factor to
\(2/(N+1)=1-\lambda\), starts at \(x_0\) and runs the same update. The pandas default
adjust=True instead normalises the weights over the available history,
\(\sum_{k=0}^{t}\lambda^k x_{t-k}/\sum_{k=0}^{t}\lambda^k\), and differs during warm-up. A column
that starts after missing rows is handled the same way, from its first finite observation, and at
interior gaps the default FFILL equals pandas with ignore_na=True.
Span, mean lag, effective sample size and half-life¶
Identity (mean lag). The mean age of the stationary weights is
Proof. Differentiating the geometric series gives \(\sum_k k\lambda^k=\lambda/(1-\lambda)^2\); multiply by \(1-\lambda\). With \(\lambda=(N-1)/(N+1)\) and \(1-\lambda=2/(N+1)\) the ratio is \((N-1)/2\). \(\square\)
Identity (effective sample size). The Kish effective sample size of the stationary weights is
Proof. \(\sum_k\omega_k^2=(1-\lambda)^2/(1-\lambda^2)=(1-\lambda)/(1+\lambda)\), and \((1+\lambda)/(1-\lambda)=\big(2N/(N+1)\big)\big/\big(2/(N+1)\big)=N\). \(\square\)
Kish (1965) defines \(N_{\mathrm{eff}}\) so that a weighted mean of IID observations with variance \(\sigma^2\) has variance \(\sigma^2/N_{\mathrm{eff}}\).
Identity (half-life). The half-life \(H=\ln 2/(-\ln\lambda)\) satisfies \(\lambda^H=1/2\): the weight at lag \(H\) is half the weight at lag 0, and the lags younger than \(H\) carry half of the total weight, \(\sum_{k<H}\omega_k=1-\lambda^H\) in the continuous sense. For large \(N\),
Proof. \(-\ln\lambda=\ln\big((N+1)/(N-1)\big)=2\operatorname{artanh}(1/N)=2/N+2/(3N^3)+\dots\); invert the series. \(\square\)
The error of \(0.347N\) is about \(0.12/N\) rows. The cruder first-order approximation \(-\ln\lambda\approx 1-\lambda\) gives \(H\approx 0.347(N+1)\), which overstates the half-life by about a third of a row.
Span \(N\) |
\(\lambda\) |
Half-life \(H\) |
Mean lag \((N-1)/2\) |
Effective \(N\) |
Lag beyond which 5% of weight lies |
|---|---|---|---|---|---|
12 |
0.8462 |
4.15 |
5.5 |
12 |
17.9 |
36 |
0.9459 |
12.47 |
17.5 |
36 |
53.9 |
52 |
0.9623 |
18.02 |
25.5 |
52 |
77.9 |
260 |
0.9923 |
90.11 |
129.5 |
260 |
389.4 |
The last column is \(\ln 20/(-\ln\lambda)\approx 1.5N\). The RiskMetrics decay 0.94 has span 32.3, half-life 11.2 rows and mean lag 15.7 rows.
Insight
An \(N\)-span EWM and an \(N\)-observation equal-weight window have the same mean lag \((N-1)/2\) and the same effective sample size \(N\), so they carry the same information at the same average age. They differ in shape: the EWM puts half of its weight on the most recent \(0.35N\) rows and keeps 5% beyond lag \(1.5N\), where the window has none.

The exhibit recovers the weights \(\omega_k\) from qis.compute_ewm as the response to a unit
shock and draws them against equal-weight windows of \(N=12\) and \(N=36\) observations. Each pair
shares its mean lag, 5.5 and 17.5, and its effective sample size \(N\), as the identities above
require. The EWM starts at \(2/(N+1)\), about twice the window weight, and decays geometrically
with half-lives of 4.15 and 12.47 observations instead of stopping at lag \(N\).
Initial conditions¶
InitType selects the seed \(m_{-1}\) when no explicit init_value is passed. The seed is the
state before a column’s first finite observation \(x_{t_0}\), which then updates it:
\(m_{t_0}=\lambda m_{-1}+(1-\lambda)x_{t_0}\). It is computed on the scale of the series the
recursion runs on, so in compute_ewm_vol it is a statistic of \(\tilde x^2\).
|
Seed \(m_{-1}\) |
Information used |
|---|---|---|
|
0 |
None; \(x_{t_0}\) enters with weight \(1-\lambda\) and the output is shrunk towards zero by the factor \(1-\lambda^{t-t_0+1}\) |
|
The column’s first finite observation, so \(m_{t_0}=x_{t_0}\) |
That observation only; pandas |
|
Full-sample |
The whole sample (look-ahead) |
|
For a second-moment recursion, the full-sample variance of the observations with |
The whole sample (look-ahead) |
By the unrolled recursion the seed keeps weight \(\lambda^{t-t_0+1}\) at row \(t\). It falls below 5% after \(\ln 20/(-\ln\lambda)\approx 1.5N\) rows: 54 rows at \(N=36\), 389 at \(N=260\). Until then a full-sample seed leaks later information into early estimates.
No default seeds with the full sample. Up to qis 5.30.3 compute_ewm_cross_xy seeded the
denominators \(M^{xx}\) and \(M^{yy}\) of CrossXyType.BETA and CrossXyType.CORR with full-sample
second moments (var_init_type=InitType.MEAN); it now seeds them with each column’s first square
(InitType.X0), and var_init_type=InitType.MEAN restores the former seed. ewm_xy_convolution
passes var_init_type=InitType.ZERO.
The other entry points of the chapter are point in time by default: compute_ewm,
compute_ewm_vol, compute_ewm_newey_west_vol, compute_rolling_mean_adj,
compute_ewm_beta_alpha_forecast, the optional mean adjustment of EwmLinearModel.fit and the
vol-normalised covariance tensor use X0; compute_ewm_sharpe, compute_ewm_long_short_filter,
compute_one_factor_ewm_betas, the moments of ewm_xy_convolution and the covariance functions
start from zero. Before this release the vol-normalised tensor seeded its volatility with the
full-sample mean of \(x^2\), EwmLinearModel.fit seeded its mean adjustment with full-sample means,
and compute_ewm_beta_alpha_forecast defaulted to MEAN, which weighted its first beta towards
the full-sample slope through the origin.
InitType.VAR seeds a second-moment recursion with the full-sample variance of the observations:
in compute_ewm_vol and compute_ewm_newey_west_vol the seed is \(\operatorname{Var}(\tilde x)\),
of order \(10^{-4}\) for daily returns with 1% volatility. A mean recursion cannot take it:
compute_ewm and compute_rolling_mean_adj raise ValueError, and an EWMA mean adjustment
requested together with VAR is seeded with MEAN. Before this release VAR seeded the
variance with the variance of \(x^2\), of order \(10^{-8}\), and seeded a mean with a variance.
X0 takes each column’s first finite observation, so a column that starts after missing rows
starts at \(m_{t_0}=x_{t_0}\), exactly like a column that starts on row 0, and its path is the one
the same observations would give without the leading rows. Before its first observation a
column’s output is missing. Before this release X0 took the first row, so a column that
started late was seeded with 0 and started at \((1-\lambda)x_{t_0}\): with the leading missing row
that qis.to_returns produces by default, the first EWM variance was \((1-\lambda)r_1^2\) and the
first inverse-volatility weight \(1/\sqrt{1-\lambda}\approx 4.1\) times too large at
\(\lambda=0.94\).
Mean adjustment¶
The volatility, Newey–West and cross-moment functions first replace \(x_t\) by
\(\tilde x_t=x_t-\hat\mu_t\), with \(\hat\mu_t\) chosen by MeanAdjType:
|
\(\hat\mu_t\) |
Timing and implementation notes |
|---|---|---|
|
0 |
The second moment about zero, the default |
|
Full-sample mean |
Look-ahead. The |
|
Expanding mean of rows \(t_0,\dots,t\) |
Point in time; includes \(x_t\), so \(\tilde x_{t_0}=0\) |
|
\(m_t\) with the same \(\lambda\) |
Point in time; includes \(x_t\); seeded by |
For returns the difference between NONE and a centred estimate is usually small, because the
squared mean is small relative to the variance. A 10% annual mean with 16% volatility adds
\((0.1/252)^2\approx 1.6\times 10^{-7}\) to a daily variance of \(1.0\times 10^{-4}\), 0.16% of it;
on a monthly grid the share is 3.3%. For signals, spreads and levels the difference is not small,
and a second moment about zero is not a variance.
Identity (EWMA-centred residual). Because \(m_t\) includes \(x_t\),
Proof. \(x_t-\lambda m_{t-1}-(1-\lambda)x_t=\lambda x_t-\lambda m_{t-1}\). \(\square\)
Proposition (shrinkage of EWMA-centred residuals). If \(x_t\) are IID with variance \(\sigma^2\), then in the stationary limit
Proof. \(x_t\) is independent of \(m_{t-1}\), \(\mathbb{E}m_{t-1}=\mathbb{E}x_t\), and \(\operatorname{Var}(m_{t-1})=\sigma^2(1-\lambda)/(1+\lambda)\) by the unit-variance proposition below. Hence \(\mathbb{E}(x_t-m_{t-1})^2=2\sigma^2/(1+\lambda)\); multiply by \(\lambda^2\) from the identity. \(\square\)
The factor is 0.920 at \(N=36\) and 0.989 at \(N=260\): an EWM volatility with
MeanAdjType.EWMA is biased low by 4.1% and 0.6% respectively. It is the price of centring on a
mean that has already seen the observation being centred.
Missing observations¶
NanBackfill decides the state at a row where the update is not finite, after a column’s first
observation. The rule applies entry by entry, so in a covariance recursion a missing asset affects
only its own row and column.
|
State at a missing row |
Output at the row |
|---|---|---|
|
\(m_t=m_{t-1}\): time stops for this series |
The state; pandas |
|
\(m_t=\lambda m_{t-1}\): the missing observation is a zero |
The state |
|
\(m_t=0\): the memory is erased and the next observation gives \((1-\lambda)x_t\) |
The state, 0 |
|
\(m_t=0\), as |
Missing; an exact zero estimate elsewhere is reported as zero |
Before a column’s first observation the output of the column-wise functions is missing under
every policy. The matrix recursions have no per-column start: an asset that has not started holds
the zero seed in its row and column, and NAN_FILL reports those entries as missing. Before this
release ZERO_FILL and NAN_FILL returned zeros for leading missing rows, NAN_FILL returned
no missing values outside the covariance tensors, and inside them it reported every entry that
was exactly zero, a genuine zero covariance included, as missing.
Pitfall
A missing return that is economically a zero return corresponds to
DEFLATED_FFILL, not to ZERO_FILL: ZERO_FILL sets the estimate to zero, so one missing
daily return wipes out a year of variance history.
Volatility and annualisation¶
Definition. The EWM variance and volatility are
compute_ewm_vol applies the steps in this order: mean adjustment, squaring, seeding (with X0,
\(v_{t_0}=\tilde x_{t_0}^2\)), the recursion, the optional floor, warm-up masking, annualisation
and the square root (apply_sqrt=False returns the variance). An explicit init_value is the
seed \(v_{-1}\) before the first observation, so \(v_{t_0}=\lambda v_{-1}+(1-\lambda)\tilde x_{t_0}^2\).
Annualisation. It applies when
annualize=Trueorannualization_factoris given. For pandas input \(\mathrm{AN}\) is inferred from the index byqis.infer_annualisation_factor_from_df, which returns 12 for month-ends and 252 for business days, and falls back to 252 with a warning when no frequency can be inferred. A bare ndarray has no index and uses \(\mathrm{AN}=1\) with a warning. Otherwise \(\mathrm{AN}=1\).Floor. With
vol_floor_quantile\(=\gamma\), \(\underline v_t\) is the trailing rolling \(\gamma\)-quantile of \(v\) overvol_floor_quantile_roll_periodrows (default 1300, five years of 260 days), with at least 20% of the window present and the'lower'interpolation. The floor is point in time. Since the square root is monotone, flooring the variance at its quantile is flooring the volatility at its quantile; \(\gamma=0.16\) is the suggested value. The floor works for DataFrames, Series and one- and two-dimensional ndarrays; before this release a Series raisedValueErrorand a one-dimensional ndarray returned a \(T\times T\) array.Warm-up.
warmup_periodsets that many leading finite values of each column to missing.
Covariance and correlation¶
Definition. For a vector of observations \(x_t\) of \(n\) assets, compute_ewm_covar and
compute_ewm_covar_tensor run
from the seed \(\hat\Sigma_{-1}\) given by covar0 (zero by default; non-finite entries become
zero). The first returns \(\hat\Sigma_{T-1}\) and the second the whole path. Both the matrix and
the scalar recursions update at row 0; the matrix recursions start from the zero seed and
compute_ewm_vol from X0, so
elementwise, where \(v_t\) is the X0-seeded variance of compute_ewm_vol. The functions take \(x\)
as given: there is no mean-adjustment argument, and a centred estimate requires demeaning the
input first.
Proposition (positive semidefiniteness). If \(\hat\Sigma_{-1}\) is positive semidefinite and every \(x_t\) is complete, every \(\hat\Sigma_t\) is positive semidefinite.
Proof. For any vector \(a\), \(a^{\top}\hat\Sigma_t a=\lambda\,a^{\top}\hat\Sigma_{t-1}a+(1-\lambda)(a^{\top}x_t)^2\), which is non-negative if \(a^{\top}\hat\Sigma_{t-1}a\) is. Equivalently, \(\hat\Sigma_t\) is a combination with non-negative coefficients of the seed and of rank-one matrices \(x_s x_s^{\top}\). \(\square\)
The matrix is semidefinite, not definite: with a zero seed and fewer than \(n\) rows its rank is at most the number of rows.
Proposition (missing data). With missing entries, DEFLATED_FFILL, the default of the
covariance functions, and ZERO_FILL (the state of NAN_FILL) preserve positive
semidefiniteness; FFILL does not.
Proof. Under DEFLATED_FFILL the row and column of a missing asset \(i\) become
\(\lambda\hat\Sigma_{t-1}\), which is exactly the full update with \(x_{i,t}\) replaced by zero;
apply the previous proposition. Under ZERO_FILL the block of observed assets is updated with
their observed sub-vector, a principal submatrix of a semidefinite matrix plus a rank-one term,
and the rest is set to zero; padding a semidefinite block with zero rows and columns keeps it
semidefinite. For FFILL, take \(\lambda=0.94\), the seed with unit diagonal and off-diagonal
0.99, and \(x_t=(\text{missing},0)\): the held entries give
\(\begin{pmatrix}1&0.99\\0.99&0.94\end{pmatrix}\), whose determinant \(0.94-0.99^2=-0.0401\) is
negative. \(\square\)
Pitfall
Asynchronous gaps, such as exchange holidays in a multi-country panel, combined
with an explicit NanBackfill.FFILL can produce EWM covariance matrices with negative
eigenvalues and correlations above one. compute_ewm_covar, compute_ewm_covar_tensor,
compute_ewm_covar_tensor_vol_norm_returns and compute_ewm_covar_newey_west therefore
default to DEFLATED_FFILL, which is also the default path of compute_ewm_corr_df and
compute_data_pca_r2; before this release they defaulted to FFILL.
With is_corr=True each matrix is converted to a correlation matrix
\(\hat\Sigma_{ij}/\sqrt{\hat\Sigma_{ii}\hat\Sigma_{jj}}\) (entries of assets without positive
variance are missing, the diagonal is one). An EWM correlation of raw returns is uncentred: it
is the weighted cosine of two return vectors, not the Pearson correlation, unless the returns are
mean-adjusted first. For one-dimensional input compute_ewm_covar makes a single update and, like
the two-dimensional path, honours is_corr.
Vol-normalised covariance. compute_ewm_covar_tensor_vol_norm_returns estimates the
correlation on returns scaled by their own EWM volatility and rescales it:
elementwise in \(i\) for the first line, with \(\hat\sigma_{i,t}=\sqrt{v_{i,t}}\), the seed
\(v_{-1}\) the column’s first finite squared return (X0, so the first volatility of a column is
the absolute value of its first return) and \(\Gamma_{-1}\) given by covar0. The function returns
\((\hat\Sigma_t,\Gamma_t,\hat\sigma_t)\) as tensors of shape \((T,n,n)\), \((T,n,n)\) and \((T,n)\);
with is_corr=True the second output is \(\hat\rho_t\) and the first is unchanged. Every output is
point in time; before this release the volatility seed was the full-sample mean of \(x^2\).
Scaling first prevents a volatile episode or asset from dominating the correlation estimate. It
also caps the normalised returns: at every row \(v_t\ge(1-\lambda)x_t^2\), so
4.30 at \(N=36\). Every \(\hat\Sigma_t\) is positive semidefinite, being a congruence of the correlation matrix of a semidefinite matrix.
Betas and cross moments¶
compute_ewm_cross_xy forms the products after the optional mean adjustment (seeded by
init_type), then runs three recursions:
\(M^{xy}\) is seeded by init_type (default ZERO, so the first product enters with weight
\(1-\lambda\)) and \(M^{xx}\), \(M^{yy}\) by var_init_type (default X0, the first squares). The
first beta is therefore \((1-\lambda)\,y_1/x_1\), shrunk towards zero while \(M^{xy}\) warms up;
var_init_type=InitType.ZERO gives the raw ratio \(y_1/x_1\) instead, which is unstable over the
first rows. warmup_period \(=k\) masks an output until its pair has \(k+1\) joint observations,
counted from the pair’s own first one. Up to qis 5.30.3 the default MEAN seeded the
denominators with full-sample second moments, a look-ahead that damped the ratios over the first
\(1.5N\) rows.
|
Output |
Notes |
|---|---|---|
|
\(M^{xy}_t\) |
A cross moment about zero unless mean-adjusted |
|
\(\hat\beta_t=M^{xy}_t/M^{xx}_t\) |
Missing where \(M^{xx}_t\) is not positive |
|
\(\hat\rho_t=M^{xy}_t/\sqrt{M^{xx}_tM^{yy}_t}\) |
Uncentred unless mean-adjusted; missing where the denominator is not positive |
Both tests are scale free. Before this release they masked any denominator below \(10^{-8}\), which silenced a factor with a daily volatility of one basis point.
On complete data and with the default seeds, \(\lvert\hat\rho_t\rvert\le 1\) by the
Cauchy–Schwarz inequality, because the numerator’s seed is zero and the denominators’ seeds are
non-negative. The function pairs two DataFrames of equal shape column by column, a Series factor
with every column of a DataFrame or with a Series (after an inner join of the indices), and two
ndarrays of the same shape, one- or two-dimensional; a DataFrame factor with a Series asset
raises TypeError.
compute_one_factor_ewm_betas(x, y) is the one-factor beta for a Series factor and a DataFrame of
assets on an identical index. It has no mean adjustment and zero seeds, updates at row 0, and
applies nan_backfill to both moments:
The rows \(t\le\) warmup_period (20 by default, so 21 rows) are missing to suppress the unstable
start, and the beta is missing where \(M^{xx}_t\) is not positive. The multi-factor kernel
qis.compute_ewm_xy_beta_tensor solves \(\hat B_t=(M^{xx}_t)^{-1}M^{xy}_t\) with a scale-free
test: a factor whose second moment is not positive gets missing betas and the others are solved
from the reduced system, and when the reduced matrix, rescaled to unit diagonal, has its smallest
eigenvalue below \(10^{-12}\) times its largest, every beta is missing. Before this release a
factor second moment below \(10^{-8}\), in the units of \(x^2\), replaced the inverse by the identity
for every factor and reported the raw cross moment as the beta.
compute_ewm_beta_alpha_forecast(x_data, y_data, span) adds an intercept and a forecast to the
one-factor beta. With seeds X0 by default, it returns \(\hat\beta_t=M^{xy}_t/M^{xx}_t\), the
alpha \(\hat\alpha_t\) as the EWM of the fitted residual \(y_t-\hat\beta_t x_t\), the one-step-ahead
prediction \(\hat\beta_{t-1}x_t+\hat\alpha_{t-1}\), which is missing on the first row, and in-sample
residual variance and \(R^2\) of \(y_t-\hat\beta_t x_t-\hat\alpha_t\). A prior beta_init_value
\(=\beta_0\) replaces the first informative pair \((x_f,y_f)\) by \((x_f,\beta_0x_f)\) and seeds both
moments with it, so the first beta is \(\beta_0\) and the prior keeps weight \(\lambda^{t-f}\).
Before this release the default seed was MEAN, the prediction used \(\hat\beta_t\) and
\(\hat\alpha_t\), and the beta moments ignored nan_backfill.
ewm_xy_convolution(returns, freq, signals, convolution_type) measures lagged dependence at a
horizon of \(h\) rows, \(h\) being get_annualization_factor(freq) as a whole number: 252 for
'B', 52 for 'W-WED', 12 for 'ME', 4 for 'QE'. Every frequency with a whole-number factor
runs; one without raises ValueError. The input is assumed daily, so freq='ME' means 12 rows,
not one month. The decay is
\(\lambda=1-2/(h+1)\) (0.2 when \(h=1\)). Returns are optionally divided by their compute_ewm_vol
volatility at decay 0.94 lagged one row (is_ra_returns=True) and summed over \(h\) rows,
\(R_t=\sum_{j=0}^{h-1}r_{t-j}\). ConvolutionType.AUTO_CORR correlates \(R_{t-h}\) with \(R_t\);
SIGNAL_CORR and SIGNAL_BETA correlate or regress \(R_t\) on the signal (its last value or its
\(h\)-row mean) shifted by \(h\) rows. The call is compute_ewm_cross_xy with the zero seed of the
cross moment and var_init_type=InitType.ZERO for the denominators, so every estimate is point
in time; before this release the denominators carried the full-sample MEAN seed that was then
the default of compute_ewm_cross_xy. Overlapping \(h\)-row sums make consecutive
products strongly dependent; see serial dependence and
signal diagnostics.
Unit-variance scaling¶
Proposition (variance of an EWM of white noise). If \(\xi_t\) are IID with mean zero and unit variance and the seed is zero, then
Proof. Independence makes the variance the sum of squared weights, \((1-\lambda)^2\sum_{k\le t}\lambda^{2k}=(1-\lambda)^2(1-\lambda^{2t+2})/(1-\lambda^2)\); cancel \(1-\lambda\). \(\square\)
The stationary variance \(1/N\) is that of an \(N\)-observation equal-weight mean, which restates
\(N_{\mathrm{eff}}=N\). Multiplying by \(\sqrt{(1+\lambda)/(1-\lambda)}=\sqrt{N}\) therefore gives unit
variance; this is is_unit_vol_scaling=True in ewm_recursion and compute_ewm. With a
non-zero seed the variance approaches the same limit.
compute_ewm_std1_norm(data, span=260) builds a unit-variance smoothed signal in three steps:
\(\tilde x_t=x_t-m_t\) with the same-span EWMA mean (is_demean=True, MeanAdjType.EWMA), a
volatility \(\hat\sigma_t\) from the X0-seeded EWM of \(\tilde x^2\), and the output
\(\eta\sqrt{N}\,\mathcal{E}_\lambda(\tilde x/\hat\sigma)_t\), the last EWM seeded at zero, with
\(\eta=\sqrt{1+\lambda}\) under the same-span EWMA demeaning and \(\eta=1\) otherwise, and missing
values set to zero by default. The ratio is missing where \(\hat\sigma_t\) is below \(10^{-8}\); because
\(v_t\ge(1-\lambda)\tilde x_t^2\) it is bounded by \(1/\sqrt{1-\lambda}\), and it equals that bound
(11.4 at \(N=260\)) at the first update after \(\tilde x_{t_0}=0\), a warm-up transient.
Proposition (variance of the demeaned signal). Let \(x_t\) be IID with variance \(\sigma^2\) and replace \(\hat\sigma_t\) by its stationary level. Then \(\sqrt{N}\,\mathcal{E}_\lambda(\tilde x/\hat\sigma)_t\) with \(\tilde x=x-\mathcal{E}_\lambda(x)\) has stationary variance \(1/(1+\lambda)\), so \(\eta=\sqrt{1+\lambda}\) restores unit variance.
Proof. The map \(x\mapsto\mathcal{E}_\lambda\big(x-\mathcal{E}_\lambda(x)\big)\) has weights \(h_k-(h*h)_k=(1-\lambda)\lambda^k\big(1-(1-\lambda)(k+1)\big)\), with \(h_k=(1-\lambda)\lambda^k\); summing three geometric series, their squares add up to \(2\lambda^2(1-\lambda)/(1+\lambda)^3\). By the shrinkage proposition \(\hat\sigma^2=\mathbb{E}\tilde x_t^2=2\lambda^2\sigma^2/(1+\lambda)\). Hence the variance is \(N\cdot 2\lambda^2(1-\lambda)(1+\lambda)^{-3}\big/\big(2\lambda^2/(1+\lambda) \big)=N(1-\lambda)/(1+\lambda)^2=1/(1+\lambda)\). \(\square\)
The squared weights of the linear map scaled by \(\sqrt{N}\) sum to \(2\lambda^2/(1+\lambda)^2\),
0.496 at \(N=260\), and the output standard deviation without \(\eta\) is \(1/\sqrt{1+\lambda}=0.71\).
Before this release compute_ewm_std1_norm returned that 0.71, although its docstring promised
unit standard deviation, and seeded the last EWM with its first value, a transient of up to
\(\sqrt{N}\) standard deviations.
The two-span long–short filter¶
compute_ewm_long_short_filter(data, long_span=63, short_span=5) combines two EWMs seeded at
zero before the first observation, which enters with weight \(1-\lambda\); a missing row holds
both legs (FFILL). In the code each leg is weight * load * EWM,
with load \(\sqrt{(1+\lambda)/(1-\lambda)}\) and weight
\(1/\big(\sqrt{1-\lambda^2}\,\kappa\big)\). Their product is \(1/\big((1-\lambda)\kappa\big)\),
which turns each leg into the unnormalised sum \(\kappa^{-1}\sum_k\lambda^k x_{t-k}\):
Proposition (unit variance). For IID unit-variance input the stationary variance of \(F_t\) is one.
Proof. The variance is \(\kappa^{-2}\sum_{k\ge 0}(\lambda_L^k-\lambda_S^k)^2\). Expanding the square gives three geometric series, \(\sum\lambda_L^{2k}=1/(1-\lambda_L^2)\), \(\sum\lambda_S^{2k}=1/(1-\lambda_S^2)\) and \(\sum(\lambda_L\lambda_S)^k=1/(1-\lambda_L\lambda_S)\), whose combination is \(\kappa^2\). \(\square\)
With short_span=None the filter is \(\sqrt{(1+\lambda_L)/(1-\lambda_L)}\,m_t\), unit variance by
the previous proposition. With two legs the weight at lag 0 is \(\lambda_L^0-\lambda_S^0=0\): \(F_t\)
does not depend on \(x_t\) and is known one row earlier.
Identity (moving-average crossover). Let \(X_t=\sum_{j=0}^{t}x_j\) with \(X_{-1}=0\) and let \(\mathcal{E}_\lambda(X)\) be the EWM of \(X\) seeded at zero, the state \(X_{-1}\) before row 0. Then for \(t\ge 0\)
Proof. By the unrolled recursion with a zero seed and \(X_t-X_{t-k}=\sum_{j<k}x_{t-j}\), \(X_t-\mathcal{E}_\lambda(X)_t=\lambda^{t+1}X_t+\sum_{k=1}^{t}(1-\lambda)\lambda^k(X_t-X_{t-k}) =\sum_{j=0}^{t}\lambda^{j+1}x_{t-j}\). Subtract this for \(\lambda_S\) from the same for \(\lambda_L\), and compare with \(\kappa F_{t+1}=\sum_{k=1}^{t+1}(\lambda_L^k-\lambda_S^k)x_{t+1-k}\). \(\square\)
With log returns \(X_t\) is the log price relative, so the filter is the fast-minus-slow EWMA crossover of log prices, lagged one row and scaled to unit variance under white noise. The kernel is hump-shaped with its maximum at lag \(\ln(\ln\lambda_S/\ln\lambda_L)/\ln(\lambda_L/\lambda_S)\), 6.8 rows for the defaults, and its response to a permanent unit shift in \(x\) is \(\sum_k(\lambda_L^k-\lambda_S^k)/\kappa=(N_L-N_S)/(2\kappa)\), 8.23 for the defaults (\(\kappa=3.52\)). Signals of this type are smoothed versions of the time-series momentum of Moskowitz, Ooi and Pedersen (2012), which trades on the sign of the past twelve-month return.
The spans are validated: both must be at least 1 (span 1 is a pass-through with \(\lambda=0\)), and
short_span must be strictly below long_span, since equal spans give \(\kappa=0\). The first
warmup_period=21 finite values are set to missing. The filter takes DataFrames, Series and
one- or two-dimensional ndarrays; the numba kernel compute_ewm_long_short does no validation.
Before this release a finite first row was dropped from both legs, and a one-dimensional ndarray
failed in numba.
Newey–West EWM variance¶
Definition. compute_ewm_newey_west_vol(data, num_lags=q) corrects the EWM variance of
compute_ewm_vol with Bartlett-weighted EWM autocovariances of the mean-adjusted series:
\(v_t\) is seeded with \(\tilde x_{t_0}^2\) under X0, as in compute_ewm_vol, so num_lags=0
reproduces the EWM variance exactly; every lag term uses the same decay (given by span or
ewm_lambda). In \(c_{k,t}\) the product \(\tilde x_s\tilde x_{s-k}\) carries the weight
\((1-\lambda)\lambda^{t-s}\) of its later date; the factor \(\lambda^{k/2}\) turns it into
\((1-\lambda)\lambda^{t-s+k/2}\), the geometric mean of the weights of its two dates. The Bartlett
weights are those of Newey and West (1987); the regression
use of the same kernel is in regression and HAC inference.
Proposition (non-negativity). For a seed \(v_{-1}\ge 0\), \(v^{\mathrm{NW}}_t\ge 0\) at every row.
Proof. Put \(g_s=\sqrt{(1-\lambda)\lambda^{t-s}}\,\tilde x_s\) for \(t_0\le s\le t\) and \(g_s=0\) otherwise. With the geometric-mean weights, \(v^{\mathrm{NW}}_t=\lambda^{t-t_0+1}v_{-1}+\sum_{s,s'}g_sg_{s'}K(s-s')\). The number of integers \(j\) with both \(j-s\) and \(j-s'\) in \(\{0,\dots,q\}\) is \((q+1)K(s-s')\), so \(\sum_{s,s'}g_sg_{s'}K(s-s')=(q+1)^{-1}\sum_j\big(\sum_{i=0}^{q}g_{j-i}\big)^2\ge 0\). \(\square\)
The same argument covers the missing-data policies, each of which is the quadratic form of a
modified sequence: FFILL holds the estimate at a gap and pairs each observation with the
previous observed ones, the estimator on the observed rows; DEFLATED_FFILL makes the gap a
zero observation; ZERO_FILL and NAN_FILL restart the estimator after the gap, NAN_FILL
reporting the gap as missing. The second output is the ratio \(v^{\mathrm{NW}}_t/v_t\), missing
where \(v_t\) is not positive. Annualisation multiplies \(v^{\mathrm{NW}}_t\) by \(\mathrm{AN}\), and
the square root is taken last. compute_ewm_covar_newey_west is the matrix analogue, with the
same factor \(\lambda^{k/2}\), and is positive semidefinite for complete data and
DEFLATED_FFILL gaps by the same argument applied to \(a^{\top}x_s\).
For \(x_t=(-1)^t\lambda^{t/2}\), \(q=1\) and the X0 seed, \(v_t=\lambda^t\big(1+(1-\lambda)t\big)\)
and \(c_{1,t}=-(1-\lambda)\,t\,\lambda^{t-1/2}\), so \(v^{\mathrm{NW}}_t=\lambda^t\): the quadratic
form contributes \((1-\lambda)\lambda^t\) and the seed \(\lambda^{t+1}\). Without the factor
\(\lambda^{1/2}\), as before this release, the estimator was
\(\lambda^t\big(1-(1-\lambda)t(\lambda^{-1/2}-1)\big)\), negative from row 531 at \(\lambda=0.94\),
and qis reported a missing volatility; the lag recursions also ignored nan_backfill and, in
the matrix version, ewm_lambda. The factor shrinks the lag-\(k\) term by 3% per lag at
\(\lambda=0.94\) relative to that estimator.
EWM Sharpe ratios¶
compute_ewm_sharpe(returns, span=260, norm_type=1) fills missing returns with zero, infers
\(\mathrm{AN}\) from the index and runs \(m_t\) and a second-moment recursion from zero seeds, the
states before row 0, so the first return enters with weight \(1-\lambda\) (before this release it
was discarded). With initial_sharpes \(=\mathrm{SR}_0\) the seeds are instead a prior with 10%
annual volatility, \(m_{-1}=0.1\,\mathrm{SR}_0/\mathrm{AN}\) and \(v_{-1}=0.01/\mathrm{AN}\).
|
Output \(\mathrm{SR}^{(n)}_t\) |
Reading |
|---|---|---|
0 |
\(\mathrm{AN}\,m_t\) |
Annualised EWM mean return, not a ratio |
1 (default) |
\(\sqrt{\mathrm{AN}}\,m_t\big/\sqrt{\mathcal{E}_\lambda(x^2)_t}\) |
Mean over root mean square |
2 |
\(\sqrt{\mathrm{AN}}\,m_t\big/\sqrt{\mathcal{E}_\lambda\big((x-m)^2\big)_t}\) |
Mean over the root EWM of squared deviations from the running mean \(m_s\) |
Norms 1 and 2 are missing where the denominator is zero, which happens only while every return
so far is zero. Norm 1 divides by the root mean square, the second moment about zero of the
MeanAdjType.NONE convention; norm 2 divides by the EWM deviation from the running mean.
Proposition (EWM Sharpe biases). With zero seeds, \(\lvert\mathrm{SR}^{(1)}_t\rvert\le\sqrt{\mathrm{AN}}\). For IID returns with per-period Sharpe ratio \(s=\mu/\sigma\), the ratio of stationary expectations is \(\sqrt{\mathrm{AN}}\,s/\sqrt{1+s^2}\) for norm 1 and \(\sqrt{\mathrm{AN}}\,s\,\sqrt{N(N+1)}/(N-1)\) for norm 2.
Proof. The weights of \(m_t\) sum to \(1-\lambda^{t+1}\le 1\), so by Cauchy–Schwarz \(m_t^2\le\mathcal{E}_\lambda(x^2)_t\). The stationary expectations are \(\mathbb{E}m_t=\mu\) and \(\mathbb{E}x^2=\mu^2+\sigma^2\), which gives norm 1; for norm 2 use the shrinkage proposition, \(\mathbb{E}(x_t-m_t)^2=\sigma^2(N-1)^2/(N(N+1))\). \(\square\)
Norm 1 compresses the annualised Sharpe ratio \(\mathrm{SR}\) by the factor
\(1/\sqrt{1+\mathrm{SR}^2/\mathrm{AN}}\): by 4% for \(\mathrm{SR}=1\) on monthly data and by 0.2%
on daily data. Norm 2 inflates it by 4.3% at \(N=36\) and 0.6% at \(N=260\). Both statements are
about ratios of expectations; the ratio of two noisy EWMs has additional small-sample bias.
compute_ewm_sharpe_from_prices resamples prices to freq (default 'QE'), takes log returns
and calls norm 2 with span 40.
EWM score and outlier filtering¶
Definition. compute_ewm_score(data, ewm_lambda=0.94) returns \((m_t,z_t)\) with
where \(m_t\) is the X0-seeded EWM mean, \(\hat\sigma_t\) the compute_ewm_vol volatility about
zero and \(c\) the full-sample clip_quantile (0.16) quantile of the column’s own \(\hat\sigma\)
(is_clip=True; before this release the quantile was pooled over all columns). The score is
missing where \(x_t\) is. Both \(m_t\) and \(\hat\sigma_t\) include \(x_t\), and \(c\) uses the whole
sample.
Proposition (score bound). At every row with \(x_t\) observed,
3.96 at \(\lambda=0.94\), and the bound is sharp.
Proof. \(m_t\) and \(v_t\) are the mean and second moment of one discrete distribution: the seed \(x_{t_0}\) and the observations, with the same weights, of which \(x_t\) has weight \(p=1-\lambda\). Write \(m_t=p\,x_t+(1-p)\mu'\), with \(\mu'\) the mean of the other mass. The variance of the distribution is at least its between-group part, \(p(x_t-m_t)^2+(1-p)(\mu'-m_t)^2=p(x_t-m_t)^2/(1-p)\). Hence \((x_t-m_t)^2\le\frac{1-p}{p}(v_t-m_t^2)\le\frac{\lambda}{1-\lambda}v_t\); clipping only raises the denominator. Equality holds when the other mass sits at one point \(\mu'\) and \(m_t=0\). \(\square\)
Insight
Because the score is measured against estimates that already contain the observation, a single observation can never score above \(\sqrt{(N-1)/2}\), however extreme. A threshold can bind only if \(\lambda>c^2/(1+c^2)\) for a cut at \(c\): \(\lambda>0.990\) for \(c=10\). From a zero-mean state a move of \(k\) standard deviations scores \(\lambda k/\sqrt{\lambda+(1-\lambda)k^2}\), which rises to \(\lambda/\sqrt{1-\lambda}=3.84\) at \(\lambda=0.94\): 3.01 for \(k=5\) and 3.57 for \(k=10\).
The preset policies SOFT_RANGE_CEIL_POLICY and SOFT_POSITIVE_LOG_POLICY of the internal enum
qis.models.linear.ewm_winsor_outliers.OutlierPolicyTypes cut the score at 3.57, the score of a
10-standard-deviation move, computed by the internal helper score_of_move. Before this release
they cut at 10, which the score can never reach at \(\lambda=0.94\).
filter_outliers(data, outlier_policy) applies a qis.OutlierPolicy in this order, setting
rejected points to missing: the absolute ceiling and floor; the optional log transform (which
requires abs_floor); a cut at the full-sample nanmean plus std_abs_ceil (or
std_abs_floor) times the full-sample nanstd with ddof=0; a cut of the EWM score of the
cleaned data at std_ewm_ceil and std_ewm_floor; and the inverse log. With
nan_replacement_type=ReplacementType.EWMA_MEAN every non-finite value, including the input’s
own missing values, is then replaced by the EWM mean of the cleaned data; otherwise rejected and
missing points stay missing. Invalid-value warnings are silenced for the duration of the call
only (before this release the call changed numpy’s error state for the rest of the process).
ewm_insample_winsorising(data, ewm_lambda=0.94, quantile_cut=0.025) computes \(z_t\), then the
full-sample quantile_cut and 1 - quantile_cut quantiles of \(z\) per column. Points outside are
replaced by \(m_t\) (EWMA_MEAN, the default), set missing (NAN), or, for QUANTILES, replaced
by the corresponding full-sample quantile of the data, not of the score. The quantiles ignore
missing values, so a column with gaps is winsorised like any other (before this release one
missing value left the whole column unchanged). All three functions use the full sample and
belong in descriptive cleaning, not in a backtest. The non-anticipating variant, the internal
qis.models.linear.ewm_winsor_outliers.ewm_winsdor_markovian_score, scores \(x_t\) against the
state at \(t-1\) and leaves the state unchanged at an outlier or a missing value, for one series
or a panel alike.
Worked example¶
The first block runs the recursion by hand on three numbers with \(\lambda=0.5\), the decay of span
3. With the X0 seed the outputs are 2, then \(0.5\cdot 2+0.5\cdot 4=3\), then
\(0.5\cdot 3+0.5\cdot 0=1.5\), which pandas reproduces with adjust=False. With the ZERO seed
the first number enters with weight one half: 1, 2.5, 1.25. The same numbers after two missing
rows give the same path, two rows later. On a series with two interior missing rows the four
NanBackfill policies give the values in the table of the missing-data section: FFILL equals
pandas with ignore_na=True, DEFLATED_FFILL equals the recursion with the gaps filled by zeros,
and before the first observation every policy returns missing values.
import numpy as np
import pandas as pd
import qis
x = pd.Series([2.0, 4.0, 0.0])
m_x0 = qis.compute_ewm(x, ewm_lambda=0.5)
np.testing.assert_allclose(m_x0, [2.0, 3.0, 1.5])
np.testing.assert_allclose(qis.compute_ewm(x, span=3), m_x0)
np.testing.assert_allclose(x.ewm(span=3, adjust=False).mean(), m_x0)
np.testing.assert_allclose(qis.compute_ewm(x, ewm_lambda=0.5, init_type=qis.InitType.ZERO),
[1.0, 2.5, 1.25])
late = pd.Series([np.nan, np.nan, 2.0, 4.0, 0.0])
np.testing.assert_allclose(qis.compute_ewm(late, ewm_lambda=0.5), [np.nan, np.nan, 2.0, 3.0, 1.5])
gappy = pd.Series([1.0, 2.0, np.nan, np.nan, 4.0])
expected = {qis.NanBackfill.FFILL: [1.0, 1.5, 1.5, 1.5, 2.75],
qis.NanBackfill.DEFLATED_FFILL: [1.0, 1.5, 0.75, 0.375, 2.1875],
qis.NanBackfill.ZERO_FILL: [1.0, 1.5, 0.0, 0.0, 2.0],
qis.NanBackfill.NAN_FILL: [1.0, 1.5, np.nan, np.nan, 2.0]}
for policy, values in expected.items():
np.testing.assert_allclose(qis.compute_ewm(gappy, ewm_lambda=0.5, nan_backfill=policy), values)
leading = qis.compute_ewm(pd.concat([pd.Series([np.nan]), gappy], ignore_index=True),
ewm_lambda=0.5, nan_backfill=policy)
np.testing.assert_allclose(leading, [np.nan] + values)
np.testing.assert_allclose(qis.compute_ewm(gappy.fillna(0.0), ewm_lambda=0.5),
expected[qis.NanBackfill.DEFLATED_FFILL])
np.testing.assert_allclose(gappy.ewm(alpha=0.5, adjust=False, ignore_na=True).mean(),
expected[qis.NanBackfill.FFILL])
The second block checks the span table against the weights themselves: the mean lag and the Kish size are computed by summing the weight series, and the half-life is checked by counting how many lags hold half the weight.
lags = np.arange(20_000)
rows = []
for span in [12, 36, 52, 260]:
decay = 1.0 - 2.0 / (span + 1.0)
omega = (1.0 - decay) * decay ** lags
mean_lag = np.sum(lags * omega)
n_eff = omega.sum() ** 2 / np.sum(omega ** 2)
half_life = np.log(2.0) / -np.log(decay)
assert np.isclose(mean_lag, (span - 1) / 2) and np.isclose(n_eff, span)
assert omega[:int(half_life)].sum() < 0.5 < omega[:int(half_life) + 1].sum()
tail_lag = np.log(20.0) / -np.log(decay)
assert np.isclose(omega[lags >= tail_lag].sum(), 0.05, atol=(1 - decay) * 0.05)
rows.append([decay, half_life, mean_lag, n_eff, tail_lag])
table = pd.DataFrame(rows, index=[12, 36, 52, 260],
columns=['lambda', 'half-life', 'mean lag', 'effective N', '5% tail lag'])
np.testing.assert_allclose(table['lambda'], [0.8462, 0.9459, 0.9623, 0.9923], atol=5e-5)
np.testing.assert_allclose(table['half-life'], [4.15, 12.47, 18.02, 90.11], atol=5e-3)
np.testing.assert_allclose(table['5% tail lag'], [17.9, 53.9, 77.9, 389.4], atol=0.05)
np.testing.assert_allclose(table['half-life'], np.log(2.0) / 2 * table.index, atol=0.12 / 12)
The third block uses 119 month-end simple returns (February 2015 to December 2024) of the
synthetic US equity and Treasury series. compute_ewm with span 36 equals pandas
ewm(span=36, adjust=False), and the annualised compute_ewm_vol equals
\(\sqrt{12\,v_t}\) from an independent loop seeded with \(x_0^2\). The index is month-end, so qis
infers \(\mathrm{AN}=12\). The last EWM volatilities are 19.3% for equities and 5.9% for
Treasuries, against full-sample volatilities of 18.5% and 5.8%. With the leading missing row that
qis.to_returns keeps by default, the first volatility is the absolute first return, and the
rest of the path is unchanged. InitType.VAR seeds the variance with the sample variance of the
returns, and compute_ewm rejects it.
from qis.datasets import generate_synthetic_universe
universe = generate_synthetic_universe(start='2015-01-01', end='2024-12-31', seed=20260725,
apply_quirks=False)
prices = universe.prices[['SEQ_US', 'SBD_TSY']].resample('ME').last()
returns = qis.to_returns(prices, is_log_returns=False, drop_first=True)
span = 36
lam = 1.0 - 2.0 / (span + 1.0)
assert len(returns) == 119 and qis.infer_annualisation_factor_from_df(returns) == 12.0
np.testing.assert_allclose(qis.compute_ewm(returns, span=span),
returns.ewm(span=span, adjust=False).mean(), atol=1e-15)
r = returns.to_numpy()
v = np.empty_like(r)
v[0] = r[0] ** 2
for t in range(1, len(r)):
v[t] = lam * v[t - 1] + (1.0 - lam) * r[t] ** 2
vol = qis.compute_ewm_vol(returns, span=span, annualize=True)
np.testing.assert_allclose(vol, np.sqrt(12.0 * v), rtol=1e-12)
np.testing.assert_allclose(vol.iloc[-1], [0.193, 0.059], atol=5e-4)
np.testing.assert_allclose(returns.std() * np.sqrt(12.0), [0.185, 0.058], atol=5e-4)
with_leading_row = qis.to_returns(prices, is_log_returns=False)
assert with_leading_row.iloc[0].isna().all()
vol_leading = qis.compute_ewm_vol(with_leading_row, span=span, annualize=True)
np.testing.assert_allclose(vol_leading.iloc[1:], vol, rtol=1e-12)
np.testing.assert_allclose(vol_leading.iloc[1], np.sqrt(12.0) * np.abs(r[0]), rtol=1e-12)
var_seeded = qis.compute_ewm_vol(returns, span=span, init_type=qis.InitType.VAR, apply_sqrt=False)
np.testing.assert_allclose(var_seeded.iloc[0], lam * r.var(axis=0) + (1.0 - lam) * r[0] ** 2)
try:
qis.compute_ewm(returns, init_type=qis.InitType.VAR)
raise AssertionError('a mean cannot take a variance seed')
except ValueError:
pass
The fourth block checks the covariance results. On complete data the final matrix is
semidefinite and its diagonal differs from the X0 variance by \(\lambda^{T}x_0^2\). With the
counterexample of the proof and an explicit FFILL, the smallest eigenvalue is −0.020; the
default DEFLATED_FFILL returns \(0.94\) times the seed, which is positive definite. A
one-dimensional input with is_corr=True returns a correlation matrix.
cov = qis.compute_ewm_covar(r, span=span)
assert np.linalg.eigvalsh(cov).min() >= 0.0
np.testing.assert_allclose(np.diag(cov), v[-1] - lam ** len(r) * r[0] ** 2, rtol=1e-10)
seed = np.array([[1.0, 0.99], [0.99, 1.0]])
one_gap = np.array([[np.nan, 0.0]])
held = qis.compute_ewm_covar(one_gap, ewm_lambda=0.94, covar0=seed,
nan_backfill=qis.NanBackfill.FFILL)
np.testing.assert_allclose(held, [[1.0, 0.99], [0.99, 0.94]])
np.testing.assert_allclose(np.linalg.eigvalsh(held).min(), -0.0205, atol=5e-5)
deflated = qis.compute_ewm_covar(one_gap, ewm_lambda=0.94, covar0=seed)
np.testing.assert_allclose(deflated, 0.94 * seed)
assert np.linalg.eigvalsh(deflated).min() > 0.0
np.testing.assert_allclose(qis.compute_ewm_covar(r[0], span=span, is_corr=True),
[[1.0, np.sign(r[0, 0] * r[0, 1])], [np.sign(r[0, 0] * r[0, 1]), 1.0]])
The fifth block builds the covariance tensors. Every matrix of the path is semidefinite up to rounding. The vol-normalised construction starts its volatility at the absolute first return, is point in time (the first 60 matrices do not depend on the later rows), rebuilds the covariance as \(\operatorname{diag}(\hat\sigma)\hat\rho\operatorname{diag}(\hat\sigma)\), and keeps the normalised returns below the cap \(\sqrt{37/2}\approx 4.30\); the largest here is 4.22.
tensor = qis.compute_ewm_covar_tensor(r, span=span)
for matrix in tensor:
eigenvalues = np.linalg.eigvalsh(matrix)
assert eigenvalues.min() >= -1e-12 * eigenvalues.max()
cov_t, norm_t, vols = qis.compute_ewm_covar_tensor_vol_norm_returns(r, span=span)
np.testing.assert_allclose(vols[0], np.abs(r[0]))
prefix = qis.compute_ewm_covar_tensor_vol_norm_returns(r[:60], span=span)
for whole, part in zip((cov_t, norm_t, vols), prefix):
np.testing.assert_allclose(whole[:60], part, rtol=1e-12)
gamma = norm_t[-1]
rho = gamma / np.sqrt(np.outer(np.diag(gamma), np.diag(gamma)))
np.testing.assert_allclose(cov_t[-1], np.outer(vols[-1], vols[-1]) * rho, rtol=1e-12)
z = np.abs(r / vols)
assert z.max() <= np.sqrt((span + 1) / 2) and np.isclose(z.max(), 4.22, atol=5e-3)
The sixth block checks the betas and cross moments. compute_one_factor_ewm_betas equals a
zero-seeded loop and masks the first 21 rows. compute_ewm_cross_xy with BETA seeds its
denominator with the first squared factor return, which is visible in the value at row 1, and
warmup_period=21 masks the same rows as compute_one_factor_ewm_betas; a Series factor, two
Series and one-dimensional arrays give the same path, and so does the same data in units ten
thousand times smaller. The uncentred EWM correlation of the two series at the last date is −0.091;
with EWMA mean adjustment it is −0.050. compute_ewm_beta_alpha_forecast starts at \(y_0/x_0\) and
predicts \(y_t\) with the beta and alpha of \(t-1\).
bench, asset = returns['SEQ_US'], returns[['SBD_TSY']]
betas = qis.compute_one_factor_ewm_betas(x=bench, y=asset, span=span)
mxy = mxx = 0.0
loop_betas = []
for xb, ya in zip(bench, asset['SBD_TSY']):
mxy = lam * mxy + (1.0 - lam) * xb * ya
mxx = lam * mxx + (1.0 - lam) * xb * xb
loop_betas.append(mxy / mxx)
assert betas.iloc[:21].isna().all().all()
np.testing.assert_allclose(betas['SBD_TSY'].iloc[21:], loop_betas[21:], rtol=1e-10, atol=1e-15)
x_frame, y_frame = returns[['SEQ_US']], returns[['SBD_TSY']]
beta_xy = qis.compute_ewm_cross_xy(x_frame, y_frame, span=span,
cross_xy_type=qis.CrossXyType.BETA)
(x0, y0), (x1, y1) = r[0], r[1]
m_xx = lam * x0 ** 2 + (1 - lam) * x1 ** 2 # the X0 seed x0^2 is also the row-0 state
m_xy = lam * (1 - lam) * x0 * y0 + (1 - lam) * x1 * y1
assert np.isclose(beta_xy.iloc[1, 0], m_xy / m_xx)
masked = qis.compute_ewm_cross_xy(x_frame, y_frame, span=span,
cross_xy_type=qis.CrossXyType.BETA, warmup_period=21)
assert masked.iloc[:21].isna().all().all()
pd.testing.assert_frame_equal(masked.iloc[21:], beta_xy.iloc[21:])
for x_in, y_in in [(bench, y_frame), (bench, asset['SBD_TSY']), (r[:, 0], r[:, 1]),
(x_frame / 1e4, y_frame / 1e4)]:
other = qis.compute_ewm_cross_xy(x_in, y_in, span=span, cross_xy_type=qis.CrossXyType.BETA)
np.testing.assert_allclose(np.asarray(other).reshape(-1), beta_xy.to_numpy().reshape(-1),
rtol=1e-9)
corr = qis.compute_ewm_cross_xy(x_frame, y_frame, span=span, cross_xy_type=qis.CrossXyType.CORR)
centred = qis.compute_ewm_cross_xy(x_frame, y_frame, span=span, cross_xy_type=qis.CrossXyType.CORR,
mean_adj_type=qis.MeanAdjType.EWMA)
assert corr.abs().max().iloc[0] <= 1.0
np.testing.assert_allclose([corr.iloc[-1, 0], centred.iloc[-1, 0]], [-0.091, -0.050], atol=5e-4)
beta_f, alpha_f, prediction, *_ = qis.compute_ewm_beta_alpha_forecast(bench, asset, span=span)
np.testing.assert_allclose(beta_f.iloc[0, 0], y0 / x0)
np.testing.assert_allclose(prediction, beta_f.shift(1).multiply(bench, axis=0) + alpha_f.shift(1))
The seventh block checks the variance results through impulse responses, which are the weights
themselves. The EWM of a unit impulse has squared weights summing to \(1/N\), and to one after
unit-variance scaling. The same-span demeaned map of compute_ewm_std1_norm retains
\(2\lambda^2/(1+\lambda)^2=0.496\) at \(N=260\), and on 40,000 IID normal draws in four columns the
function, with its factor \(\eta=\sqrt{1+\lambda}\), has a root mean square of 1.005 after its
warm-up.
The two-span filter with the default spans 63 and 5 has weights \((\lambda_L^k-\lambda_S^k)/\kappa\),
zero at lag 0, a peak at lag 7, squared weights summing to one and a sum of 8.23. On 2,608 daily
log returns of the synthetic US equity series, \(\kappa\) times the filter equals the fast-minus-slow
EWMA crossover of the cumulative log return one row earlier.
impulse = pd.Series(np.zeros(6000))
impulse.iloc[1] = 1.0
response = qis.compute_ewm(impulse, span=span, init_type=qis.InitType.ZERO)
assert np.isclose(np.sum(response ** 2), 1.0 / span)
scaled = qis.compute_ewm(impulse, span=span, init_type=qis.InitType.ZERO, is_unit_vol_scaling=True)
assert np.isclose(np.sum(scaled ** 2), 1.0)
demeaned = impulse - qis.compute_ewm(impulse, span=260, init_type=qis.InitType.ZERO)
linear_part = np.sqrt(260) * qis.compute_ewm(demeaned, span=260, init_type=qis.InitType.ZERO)
lam_260 = 1.0 - 2.0 / 261.0
assert np.isclose(np.sum(linear_part ** 2), 2 * lam_260 ** 2 / (1 + lam_260) ** 2, rtol=1e-6)
assert np.isclose(np.sum(linear_part ** 2), 0.496, atol=5e-4)
draws = pd.DataFrame(np.random.default_rng(7).standard_normal((40_000, 4)))
normalised = qis.compute_ewm_std1_norm(draws, span=260)
rms = np.sqrt(np.mean(normalised.iloc[2_000:].to_numpy() ** 2))
assert np.isclose(rms, 1.005, atol=5e-4) and abs(rms / np.sqrt(1 + lam_260) - 0.71) < 0.005
ls = qis.compute_ewm_long_short_filter(impulse, long_span=63, short_span=5, warmup_period=None)
lam_l, lam_s = 1.0 - 2.0 / 64, 1.0 - 2.0 / 6
kappa = np.sqrt(1 / (1 - lam_l ** 2) + 1 / (1 - lam_s ** 2) - 2 / (1 - lam_l * lam_s))
k = np.arange(len(impulse) - 1)
np.testing.assert_allclose(ls.iloc[1:], (lam_l ** k - lam_s ** k) / kappa, atol=1e-15)
assert abs(ls.iloc[1]) < 1e-15 and int(np.argmax(ls.to_numpy())) - 1 == 7
assert np.isclose(np.sum(ls ** 2), 1.0)
assert np.isclose(ls.sum(), (63 - 5) / (2 * kappa)) and np.isclose(ls.sum(), 8.23, atol=5e-3)
daily = qis.to_returns(universe.prices['SEQ_US'], is_log_returns=True, drop_first=True)
assert len(daily) == 2608
cumulative = daily.cumsum()
crossover = (qis.compute_ewm(cumulative, span=5, init_type=qis.InitType.ZERO)
- qis.compute_ewm(cumulative, span=63, init_type=qis.InitType.ZERO))
trend = qis.compute_ewm_long_short_filter(daily, long_span=63, short_span=5, warmup_period=None)
np.testing.assert_allclose(kappa * trend.iloc[1:].to_numpy(), crossover.iloc[:-1].to_numpy(),
atol=1e-12)
The eighth block checks the Newey–West variance against an independent loop with \(q=2\) and the geometric-mean lag weights, then the alternating sequence: qis gives \(\lambda^t\), a positive variance at every row, while the unweighted lags of earlier releases, computed here by hand, turn negative at row 531. At the last date of the monthly sample the Newey–West variance is 1.21 times the EWM variance for equities and 1.10 times for Treasuries.
nw_var, nw_ratio = qis.compute_ewm_newey_west_vol(returns, num_lags=2, span=span,
apply_sqrt=False)
adjustment = np.zeros_like(r)
for lag in (1, 2):
c = np.zeros_like(r)
for t in range(1, len(r)):
c[t] = lam * c[t - 1] + (1.0 - lam) * (r[t] * r[t - lag] if t >= lag else 0.0)
adjustment += (1.0 - lag / 3.0) * 2.0 * lam ** (lag / 2) * c
np.testing.assert_allclose(nw_var, v + adjustment, rtol=1e-10)
np.testing.assert_allclose(nw_ratio.iloc[-1], [1.21, 1.10], atol=5e-3)
rows_idx = np.arange(1000)
alternating = pd.Series((-1.0) ** rows_idx * 0.94 ** (rows_idx / 2))
alt_var, alt_ratio = qis.compute_ewm_newey_west_vol(alternating, num_lags=1, ewm_lambda=0.94,
apply_sqrt=False)
np.testing.assert_allclose(alt_var, 0.94 ** rows_idx, rtol=1e-8)
np.testing.assert_allclose(alt_ratio, 1.0 / (1.0 + 0.06 * rows_idx), rtol=1e-8)
unweighted = 0.94 ** rows_idx * (1 - 0.06 * rows_idx * (0.94 ** -0.5 - 1))
assert unweighted[530] > 0 > unweighted[531]
alt_vol, _ = qis.compute_ewm_newey_west_vol(alternating, num_lags=1, ewm_lambda=0.94)
assert alt_vol.notna().all()
The last block checks the Sharpe definitions and the score bound. Norm 0 is twelve times the EWM mean; norm 1 is \(\sqrt{12}\) times the mean over the root mean square, stays within \(\sqrt{12}\), and uses the first return. A 50% one-day return injected into the daily equity returns scores 3.82 at \(\lambda=0.94\), below the bound 3.96, so a filter at ten scores removes nothing; the preset cut at 3.57, the score of a 10-standard-deviation move, removes it, and the call leaves numpy’s error state as it found it.
from qis.models.linear.ewm_winsor_outliers import OutlierPolicyTypes, score_of_move
mean_loop = np.zeros_like(r)
square_loop = np.zeros_like(r)
m_prev = s_prev = np.zeros(2)
for t in range(len(r)):
mean_loop[t] = m_prev = lam * m_prev + (1.0 - lam) * r[t]
square_loop[t] = s_prev = lam * s_prev + (1.0 - lam) * r[t] ** 2
sharpe_0 = qis.compute_ewm_sharpe(returns, span=span, norm_type=0)
sharpe_1 = qis.compute_ewm_sharpe(returns, span=span, norm_type=1)
np.testing.assert_allclose(sharpe_0, 12.0 * mean_loop, atol=1e-15)
np.testing.assert_allclose(sharpe_1, np.sqrt(12.0) * mean_loop / np.sqrt(square_loop))
assert (sharpe_1.abs() <= np.sqrt(12.0)).all().all()
shocked = qis.to_returns(universe.prices, drop_first=True).to_numpy().copy()
shocked[1000, 0] = 0.5
_, score = qis.compute_ewm_score(shocked, ewm_lambda=0.94, is_clip=False)
bound = np.sqrt(0.94 / 0.06)
assert np.nanmax(np.abs(score)) <= bound and np.isclose(bound, 3.96, atol=5e-3)
assert np.isclose(score[1000, 0], 3.82, atol=5e-3)
error_state = np.geterr()
cleaned = qis.filter_outliers(shocked, qis.OutlierPolicy(std_ewm_ceil=10.0, std_ewm_floor=-10.0))
assert np.array_equal(np.isnan(cleaned), np.isnan(shocked)) and np.geterr() == error_state
preset_cut = OutlierPolicyTypes.SOFT_RANGE_CEIL_POLICY.value.std_ewm_ceil
assert np.isclose(preset_cut, score_of_move(10.0, 0.94)) and np.isclose(preset_cut, 3.57, atol=5e-3)
assert np.isnan(qis.filter_outliers(shocked, qis.OutlierPolicy(std_ewm_ceil=preset_cut))[1000, 0])
Implementation in qis¶
Quantity |
Formula |
qis entry point |
|---|---|---|
EWM recursion (numba, ndarray only) |
\(m_t=\lambda m_{t-1}+(1-\lambda)x_t\), seed before the first observation |
|
EWM mean |
the recursion with |
|
Unit-variance EWM |
\(\sqrt{N}\,m_t\) |
|
EWM volatility |
\(\sqrt{\mathrm{AN}\max(v_t,\underline v_t)}\) |
|
Newey–West EWM volatility |
\(v_t+\sum_k(1-k/(q+1))\,2\lambda^{k/2}c_{k,t}\) |
|
Newey–West EWM covariance |
\(\hat\Sigma_{T-1}+\sum_k(1-k/(q+1))\lambda^{k/2}(C_k+C_k^{\top})\) |
|
Covariance at the last date |
\(\hat\Sigma_{T-1}\) |
|
Covariance path |
\(\hat\Sigma_t\) for all \(t\) |
|
Vol-normalised covariance path |
\(\operatorname{diag}(\hat\sigma_t)\hat\rho_t\operatorname{diag}(\hat\sigma_t)\) |
|
Cross moment, beta, correlation |
\(M^{xy}_t\), \(M^{xy}_t/M^{xx}_t\), \(M^{xy}_t/\sqrt{M^{xx}_tM^{yy}_t}\) |
|
One-factor beta |
\(M^{xy}_t/M^{xx}_t\), zero seeds, rows \(t\le 20\) missing |
|
Multi-factor betas |
\((M^{xx}_t)^{-1}M^{xy}_t\), missing where singular |
|
Beta, alpha and forecast |
\(\hat\beta_t\), \(\hat\alpha_t\), \(\hat\beta_{t-1}x_t+\hat\alpha_{t-1}\) |
|
Lagged cross dependence |
CORR or BETA of \(h\)-row sums |
|
Two-span filter |
\(\kappa^{-1}\sum_k(\lambda_L^k-\lambda_S^k)x_{t-k}\) |
|
Unit-variance normalised signal |
\(\eta\sqrt{N}\,\mathcal{E}_\lambda(\tilde x/\hat\sigma)_t\), \(\eta=\sqrt{1+\lambda}\) under EWMA demeaning |
|
EWM Sharpe ratio |
\(\mathrm{AN}\,m_t\) or \(\sqrt{\mathrm{AN}}\,m_t/\sqrt{\cdot}\) |
|
EWM score |
\((x_t-m_t)/\max(\hat\sigma_t,c)\), \(c\) per column |
|
Outlier filter |
policy steps, then optional EWM-mean fill |
|
Score winsorising |
full-sample score quantiles |
|
Mean path |
\(\hat\mu_t\) |
|
Mean-adjusted data |
\(x_t-\hat\mu_t\) |
|
Conventions |
seed, mean, gaps, cross type |
|
The implementations are in ewm.py, ewm_winsor_outliers.py and ewm_convolution.py. API pages: compute_ewm, compute_ewm_vol, compute_ewm_newey_west_vol, compute_ewm_cross_xy, compute_ewm_long_short_filter, compute_ewm_sharpe and NanBackfill.
Contracts worth knowing at the call site:
Containers.
compute_ewm,compute_ewm_vol,compute_ewm_newey_west_vol,compute_roll_mean,compute_rolling_mean_adjandcompute_ewm_long_short_filterreturn the container they receive. The covariance functions,ewm_recursionandcompute_ewm_long_shorttake ndarrays only; pass.to_numpy()and rebuild the frame.compute_ewm_scoreis typed for ndarrays.Decay.
spanoverridesewm_lambda. The column-wise functions accept a vector of decays, one per column; the covariance kernels take a scalar.Infinite values. The pandas-facing wrappers convert infinite values to missing values, and the kernels treat any non-finite update as missing, so both follow the
NanBackfillpolicy.One-dimensional ndarrays. Every wrapper of the chapter accepts a one-dimensional ndarray, including
compute_ewm_long_short_filter,compute_ewm_cross_xyand the volatility floor ofcompute_ewm_vol.Compilation. The numba kernels
ewm_recursion,compute_ewm_long_shortand the internal Newey–West and matrix-update kernels are cached on disk, so a new process does not recompile them; where no cache location is writable they compile in memory.Related public functions not derived here:
qis.compute_ewm_alpha_r2_given_prediction(EWM alpha and in-sample \(R^2\) of a given prediction) andqis.compute_ewm_sharpe_from_prices.
Interpretation and limitations¶
Point in time or not. The recursion itself is point in time, and so is every default seed. Look-ahead enters only through
InitType.MEANandInitType.VAR,MeanAdjType.INSAMPLE, the clip quantile of the score, and the full-sample cuts of the outlier functions. The seed’s weight decays as \(\lambda^{t-t_0+1}\), so a full-sample seed matters for about \(1.5N\) rows; the mean adjustment and the outlier cuts matter at every row.Contemporaneous estimates. Every estimate dated \(t\) contains \(x_t\). Inside a backtest lag it one period. Contemporaneous estimates also shrink what they measure: EWMA-centred residuals by \(\lambda\), normalised returns to at most \(\sqrt{(N+1)/2}\), scores to at most \(\sqrt{(N-1)/2}\).
Second moments about zero. The default volatility, covariance, beta and correlation are uncentred. For daily returns this matters little; for monthly returns, spreads, signals and factor returns with a large mean it does.
Span choice. A span trades noise against lag. The mean lag of \((N-1)/2\) rows is the delay with which a step change in volatility is absorbed; the effective size \(N\) sets the sampling error, roughly \(1/\sqrt{2N}\) relative error for a Gaussian volatility estimate.
Missing data. The column-wise default
FFILLtreats a gap as stopped time. The covariance functions default toDEFLATED_FFILL, which treats a gap as a zero observation and keeps every matrix positive semidefinite; an explicitFFILLcan break that with asynchronous gaps;ZERO_FILLerases history, andNAN_FILLerases it and reports the gap.Newey–West. The EWM version is a heuristic correction for serial correlation, not a consistent HAC estimator. Its geometric-mean lag weights make it a Bartlett quadratic form, so it is never negative.
Early rows. The first-observation rule of this release changed early estimates wherever a column starts after missing rows or a seed was given explicitly: the difference decays as \(\lambda^{t-t_0}\), below 5% after about \(1.5N\) rows, and leaves later estimates unchanged.
See also¶
References¶
J.P. Morgan and Reuters (1996). RiskMetrics — Technical Document, 4th edition. J.P. Morgan. The source of the 0.94 daily decay used as the qis default.
Kish, L. (1965). Survey Sampling. Wiley. Defines the effective sample size of a weighted mean.
Newey, W. K., and West, K. D. (1987). A Simple, Positive Semi-Definite, Heteroskedasticity and Autocorrelation Consistent Covariance Matrix. Econometrica, 55(3), 703–708. Working paper and published-version record. The Bartlett weights of the Newey–West EWM variance.
Moskowitz, T. J., Ooi, Y. H., and Pedersen, L. H. (2012). Time Series Momentum. Journal of Financial Economics, 104(2), 228–250. DOI: 10.1016/j.jfineco.2011.11.003. Context for trend signals such as the two-span filter.
Sepp, A. qis: Performance analytics, portfolio backtesting, risk analysis, and factsheet reporting in Python. Software citation metadata.