Momentum trading, the strategy of buying assets that have been rising and selling those that have been falling, is a popular approach in financial markets. However, a key challenge lies in distinguishing sustainable trends from overextended moves that are ripe for a sharp reversal. What if we could harness the power of momentum but intelligently sidestep entries when the market is exhibiting statistically extreme behavior? This is where Extreme Value Theory (EVT)–Adjusted Momentum comes into play.
This strategy combines a standard momentum indicator with EVT, a branch of statistics focused on modeling the extreme deviations from the median of probability distributions. By understanding the “tail behavior” of asset returns or momentum values, we can adapt our entry thresholds to potentially filter out trades that are initiated when the market is already in an unsustainable frenzy or a deep capitulation.
The core idea is straightforward:
Let’s explore how this can be implemented.
For our momentum indicator, we’ll use the Price Rate of Change (ROC). It measures the percentage change in price between the current price and the price a certain number of periods ago. A positive ROC suggests upward momentum, while a negative ROC indicates downward momentum.
# Momentum Component Parameters
= 30
MOMENTUM_ROC_WINDOW = 0.02 # ROC must be > 2% for a potential long
MOMENTUM_BASE_THRESH_LONG = -0.02 # ROC must be < -2% for a potential short
MOMENTUM_BASE_THRESH_SHORT # ...
# In calculate_indicators_and_evt function:
# df_calc['ROC'] = (df_calc['Close'] / df_calc['Close'].shift(MOMENTUM_ROC_WINDOW)) - 1
In our strategy, we’ll look for ROC values that exceed a baseline threshold (e.g., a 2% move over 30 days) to consider it a valid initial momentum signal.
Financial returns are notorious for their “fat tails” – extreme events occur more frequently than a normal distribution would suggest. EVT provides tools to specifically model these tails. We’ll use the Peaks Over Threshold (POT) method, which focuses on observations that exceed a certain high threshold. The excesses over this threshold are then modeled using the Generalized Pareto Distribution (GPD).
Key EVT parameters in our strategy:
EVT_ROLLING_WINDOW
: The lookback period (e.g., 30 days
in the provided code, though typically longer like 252 days might be
used for more stable EVT estimates) used to gather data for fitting the
GPD. This window rolls forward, allowing the model to adapt.EVT_EXCESS_PERCENTILE_U
: This percentile (e.g., 80th or
90th) is used on the ROC values within the rolling window to determine
the threshold u
. Only ROC values exceeding u
(the “excesses”) are used to fit the GPD.EVT_FILTER_QUANTILE_GPD
: Once the GPD is fitted to the
excesses, we calculate a specific quantile of this distribution (e.g.,
the 90th or 95th percentile). This gives us a level beyond which ROC
values are considered “too extreme” by our EVT model.The following snippet from the
calculate_indicators_and_evt
function illustrates the core
GPD fitting for the upper tail (positive ROCs). A similar process is
applied to the lower tail (negative ROCs).
# --- Upper Tail (Positive ROCs) ---
= roc_window_data[roc_window_data > 0]
positive_rocs = np.inf # Default if EVT fit fails or not enough data
current_evt_upper_boundary
# Ensure enough positive observations before attempting to find quantile for 'u'
if len(positive_rocs) > min_obs_for_evt * 2 : # min_obs_for_evt is derived from EVT_EXCESS_PERCENTILE_U
= positive_rocs.quantile(EVT_EXCESS_PERCENTILE_U)
u_pos = positive_rocs[positive_rocs > u_pos] - u_pos # Calculate excesses over threshold u
excesses_pos
# Ensure enough excesses for a stable GPD fit
if len(excesses_pos) >= min_obs_for_evt:
try:
# Fit GPD to excesses. floc=0 means location parameter is fixed at 0 for excesses.
# c_pos is the shape parameter (xi), scale_pos is the scale parameter (sigma).
= genpareto.fit(excesses_pos, floc=0)
c_pos, _, scale_pos
if scale_pos > 0: # Ensure valid scale parameter
# Calculate the value at the specified quantile of the fitted GPD of excesses
= genpareto.ppf(EVT_FILTER_QUANTILE_GPD, c_pos, loc=0, scale=scale_pos)
gpd_quantile_val_pos # The EVT filter boundary is the threshold 'u' plus the GPD quantile value
= u_pos + gpd_quantile_val_pos
current_evt_upper_boundary except (RuntimeError, ValueError) as e:
# print(f"EVT GPD fit warning (upper tail): {e}")
pass # Keep default np.inf if fit fails
evt_upper_boundaries.append(current_evt_upper_boundary)
In this snippet:
genpareto.fit(excesses_pos, floc=0)
fits the GPD to the
ROC values that exceeded our threshold u_pos
.genpareto.ppf(EVT_FILTER_QUANTILE_GPD, ...)
calculates
the inverse of the cumulative distribution function (CDF), effectively
giving us the ROC value at our specified high quantile
(EVT_FILTER_QUANTILE_GPD
) of the fitted tail
distribution.current_evt_upper_boundary
is this calculated
extreme level.Once we have our ROC momentum signal and our dynamically calculated EVT filter boundaries, the trading logic is as follows:
roc_prev
) must be greater
than our MOMENTUM_BASE_THRESH_LONG
(e.g., > 2%).roc_prev
must be less than the
EVT_Upper_Filter
boundary calculated for the previous day.
This means the momentum is strong, but not yet in the “too extreme” zone
identified by EVT.roc_prev
must be less than
MOMENTUM_BASE_THRESH_SHORT
(e.g., < -2%).roc_prev
must be greater than the
EVT_Lower_Filter
boundary.The following code snippet from the run_backtest
function shows this entry logic:
# Inside the backtest loop, after fetching previous day's values:
# roc_prev = df['ROC'].iloc[i-1]
# evt_upper_prev = df['EVT_Upper_Filter'].iloc[i-1]
# evt_lower_prev = df['EVT_Lower_Filter'].iloc[i-1]
# Long Entry Condition
if roc_prev > MOMENTUM_BASE_THRESH_LONG and roc_prev < evt_upper_prev:
'Signal'] = 1 # Signal for current day, entry at Open
df.loc[df.index[i],
# Short Entry Condition
elif roc_prev < MOMENTUM_BASE_THRESH_SHORT and roc_prev > evt_lower_prev:
'Signal'] = -1 # Signal for current day, entry at Open df.loc[df.index[i],
This ensures that we only act on momentum signals that pass through our EVT-based “sanity check,” aiming to avoid entries at points of potential exhaustion.
The rationale behind filtering out EVT-defined extreme momentum signals is rooted in the observation that exceptionally large price movements (the “fat tails”) can sometimes be followed by corrections or mean reversion, rather than continued acceleration. By setting an upper (and lower) bound based on a dynamic model of these extremes, the strategy attempts to:
While intellectually appealing, implementing EVT-based strategies comes with its own set of challenges:
EVT_ROLLING_WINDOW
, EVT_EXCESS_PERCENTILE_U
(for threshold selection), and EVT_FILTER_QUANTILE_GPD
is
crucial and can significantly impact results. The provided code uses a
very short EVT_ROLLING_WINDOW
of 30 days and a relatively
low EVT_EXCESS_PERCENTILE_U
of 80%, which might lead to
less stable EVT parameter estimates. Typically, longer periods are
preferred for more robust tail modeling.u
), can be challenging. The estimated parameters (shape and
scale) might be unstable or sensitive.The EVT-Adjusted Momentum strategy offers a sophisticated approach to momentum trading by incorporating a statistical understanding of extreme price movements. By attempting to filter out entries when momentum signals appear “too extreme” according to a dynamic tail model, it aims to enhance the robustness of a traditional momentum approach. However, its complexity means that careful parameterization, robust implementation of the EVT component, and thorough testing are paramount for anyone looking to explore such strategies. It’s a fascinating intersection of statistical theory and practical trading, pushing the boundaries of quantitative strategy design.