Traditional moving averages, while foundational, often face a
trade-off: responsiveness versus smoothness. A Triple Exponential Moving
Average (TEMA) aims to reduce the lag inherent in simpler MAs. But can
we push adaptivity further? This article investigates a strategy that
modulates the TEMA’s underlying smoothing based on the
co-movement between price and volume. The hypothesis is
that when price changes are strongly supported by corresponding volume
changes (high price-volume covariance), the prevailing move has
conviction, and our TEMA should react faster. Conversely, if price moves
on weak or contradictory volume, the TEMA should be more cautious and
smooth out potential noise. We’ll explore the construction of this
“CATEMA,” its trading logic, and analyze a backtest performed on
ETH-USD
from 2020 to 2024.
Triple Exponential Moving Average (TEMA):
TEMA was designed by Patrick Mulloy to offer a moving average with less lag than traditional EMAs. It achieves this by applying multiple layers of exponential smoothing and then adjusting the result:
period
is used for all three underlying EMAs. Our strategy will make this
period
adaptive.Price-Volume Covariance: Measuring Co-movement:
The strength and conviction behind a price move are often gauged by accompanying volume.
Close.diff()
) and volume percentage changes
(Volume.pct_change()
) over a lookback window (e.g., 30
days).The Adaptive Mechanism: Modulating TEMA’s Speed:
The core innovation is to use the normalized price-volume covariance to adjust the effective period of the TEMA’s underlying EMAs.
period_tema_min
and
period_tema_max
.Let’s look at key code segments that bring this adaptive mechanism to life.
Snippet 1: Calculating and Normalizing Price-Volume Covariance
This shows how daily price changes and volume percentage changes are used to compute a rolling covariance, which is then normalized to a 0-1 scale.
Python
# --- Parameters from the script for ETH-USD ---
# cov_window = 30
# cov_norm_window = 90
# --- Column Names ---
# price_change_cov_col = "Price_Change_For_Cov"
# volume_change_cov_col = "Volume_Change_For_Cov"
# price_volume_cov_col = f"PriceVol_Cov_{cov_window}d"
# normalized_cov_col = f"Normalized_Cov_{cov_norm_window}d"
# --- Indicator Calculation (within pandas DataFrame 'df') ---
# Inputs for Covariance
df[price_change_cov_col] = df['Close'].diff() # Simple price change
df[volume_change_cov_col] = df['Volume'].pct_change() # Percentage volume change
df.fillna({price_change_cov_col: 0, volume_change_cov_col: 0}, inplace=True)
# Rolling Price-Volume Covariance
df[price_volume_cov_col] = df[price_change_cov_col].rolling(window=cov_window).cov(df[volume_change_cov_col])
# Normalize Covariance (0-1 range, higher means stronger positive co-movement)
rolling_min_cov = df[price_volume_cov_col].rolling(window=cov_norm_window).min()
rolling_max_cov = df[price_volume_cov_col].rolling(window=cov_norm_window).max()
range_cov = rolling_max_cov - rolling_min_cov
# Handle division by zero if range_cov is 0, then fillna with neutral 0.5
df[normalized_cov_col] = ((df[price_volume_cov_col] - rolling_min_cov) / range_cov.replace(0, np.nan)).fillna(0.5)
df[normalized_cov_col] = np.clip(df[normalized_cov_col], 0, 1) # Ensure strictly 0-1
The normalized_cov_col
gives us a bounded (0 to 1)
measure of recent price-volume co-movement strength, where 1 indicates
the strongest positive covariance in the normalization window.
Snippet 2: Determining the Adaptive TEMA Period and Iterative CATEMA Calculation
The normalized covariance (from the previous day) dictates the EMA period for the current day’s TEMA calculation. High covariance leads to a shorter period, low covariance to a longer one. The TEMA itself (CATEMA) is then built iteratively.
Python
# --- Parameters from the script ---
# period_tema_min = 7
# period_tema_max = 90
# --- Column Names ---
# adaptive_tema_period_col = "Adaptive_TEMA_Period"
# catema_col = "CATEMA"
# normalized_cov_col = ... (from previous snippet)
# 1. Map Normalized Covariance to Adaptive TEMA Period (using lagged covariance)
# High normalized_cov (strong co-movement) -> period_tema_min (faster)
# Low normalized_cov (weak co-movement) -> period_tema_max (slower)
df[adaptive_tema_period_col] = period_tema_max - df[normalized_cov_col].shift(1) * (period_tema_max - period_tema_min)
df[adaptive_tema_period_col] = np.round(df[adaptive_tema_period_col]).fillna( (period_tema_min + period_tema_max) / 2 ).astype(int)
df[adaptive_tema_period_col] = np.clip(df[adaptive_tema_period_col], period_tema_min, period_tema_max)
# 2. Iteratively Calculate Covariance-Adjusted TEMA (CATEMA)
# (Conceptual structure; full loop is in the main script)
df['EMA1_cat'] = np.nan; df['EMA2_cat'] = np.nan; df['EMA3_cat'] = np.nan
df[catema_col] = np.nan
# Seed initial EMAs and CATEMA (e.g., at first valid adaptive_period_col)
# ... (seeding logic as in the full script) ...
# Example of one iteration within the loop (from the full script):
# for k_idx in range(start_loc + 1, len(df)):
# idx_today = df.index[k_idx]
# idx_prev = df.index[k_idx-1]
# current_period = df.loc[idx_today, adaptive_tema_period_col] # Based on prev day's cov
# alpha = 2 / (current_period + 1)
# close_today = df.loc[idx_today, 'Close']
# prev_ema1 = df.loc[idx_prev, 'EMA1_cat'] # ... and for EMA2, EMA3
#
# ema1_today = alpha * close_today + (1 - alpha) * prev_ema1
# ema2_today = alpha * ema1_today + (1 - alpha) * prev_ema2
# ema3_today = alpha * ema2_today + (1 - alpha) * prev_ema3
# catema_today = (3 * ema1_today) - (3 * ema2_today) + ema3_today
#
# df.loc[idx_today, 'EMA1_cat'] = ema1_today # ... and for EMA2, EMA3, CATEMA
# print("# Full iterative CATEMA calculation is performed in the main script's loop.")
This iterative calculation ensures that each day’s TEMA uses an α
(smoothing constant) derived from the
adaptive_tema_period_col
, which in turn is based on the
prior day’s price-volume covariance.
The trading signals are generated by the price crossing the CATEMA:
Close
>
previous day’s CATEMA
. Enter long at current day’s
Open
.Close
<
previous day’s CATEMA
. Enter short at current day’s
Open
.Risk Management: An ATR-based trailing stop
loss is employed. The script uses a 14-period ATR and a tight
multiplier of 1.0
. This stop trails the price to protect
profits and limit losses.
The backtest results provided for ETH-USD
(January 2020
- December 2024) are striking:
Interpreting These Results – A Researcher’s Perspective:
cov_window
, cov_norm_window
,
period_tema_min
, period_tema_max
, ATR
settings), the risk of finding a combination that performed
exceptionally well historically by chance is significant.The CATEMA concept – dynamically adjusting a TEMA’s responsiveness using price-volume covariance – is an intuitive attempt to align the indicator with perceived market conviction.
Potential Strengths:
Critical Areas for Research & Due Diligence:
Close.diff()
for price change and
Volume.pct_change()
for volume change in the covariance
calculation is one of many possibilities. Others (e.g., log returns, raw
volume, detrended price/volume) could be explored.cov_norm_window
. Other methods like Z-scoring or rank-based
normalization could be investigated.cov_window
s, TEMA period ranges, and especially
the atr_multiplier_sl
? A multiplier of 1.0 is unusual and
its impact needs careful study.The Covariance-Adjusted Triple EMA (CATEMA) strategy offers an advanced, adaptive approach to trend following. By modulating the TEMA’s speed based on the strength of price-volume co-movement, it seeks to enhance signal quality. The backtest results on ETH-USD for the 2020-2024 period are, at face value, extraordinarily strong. However, any researcher or practitioner would treat such figures as a starting point for a much deeper investigation. The path from a compelling backtest to a consistently profitable live strategy requires thorough validation, an understanding of all assumptions (especially regarding execution and costs), and a healthy skepticism towards parameters that seem “too good to be true,” like a 1.0x ATR multiplier often is. This adaptive framework nonetheless provides a rich field for further quantitative exploration.