Financial asset prices rarely move purely in a smooth, continuous “diffusion” process. Instead, they often exhibit jumps: sudden, large, and discontinuous movements that can significantly impact returns. These jumps, typically driven by unexpected news or events, often exhibit short-term momentum or mean-reversion characteristics depending on the market and the nature of the shock. This article explores a trading strategy that attempts to capitalize on these jump-diffusion dynamics, combining statistical detection with momentum and trend-volatility filtering.
The strategy’s foundation lies in distinguishing between two primary types of price movements:
This distinction is crucial because the market’s behavior after a jump can be systematically different from its behavior during normal diffusion. A significant price jump might attract follow-through momentum or trigger a short-term reversal.
Jump Detection: The core of jump detection involves
comparing a single period’s return to the asset’s historical volatility.
A common approach uses Z-scores: where
is the current return and
is the estimated
volatility over a lookback period. If
exceeds a predefined
jump_threshold
(e.g., 2.5 standard deviations), combined
with a min_jump_size
(e.g., 2.5% move), a jump is
flagged.
Post-Jump Momentum/Diffusion Analysis: Once a jump is detected, the strategy investigates the subsequent price action.
momentum_period
.Trend & Volatility Filtering (ADX & ATR): To enhance signal quality and avoid trading in unsuitable market conditions, the strategy incorporates filters based on:
min_adx_level
suggests a non-trending
market.rising_lookback
period, ensuring that confirmed trends are
developing alongside supportive volatility.The strategy employs three custom indicators to capture these dynamics:
JumpDiffusionDetector
: This
indicator calculates daily returns, estimates rolling volatility
(standard deviation of returns), and then computes a Z-score to identify
jumps. It also provides a smoothed diffusion_trend
based on
recent returns.
class JumpDiffusionDetector(bt.Indicator):
= ('jump_signal', 'jump_magnitude', 'diffusion_trend')
lines = (('lookback', 20), ('jump_threshold', 2.5), ('min_jump_size', 0.02), ('decay_factor', 0.9),)
params
def __init__(self):
self.returns = bt.indicators.PctChange(self.data.close, period=1)
self.vol_estimator = bt.indicators.StandardDeviation(self.returns, period=self.params.lookback)
self.change_buffer = deque(maxlen=self.params.lookback)
def next(self):
= self.returns[0]
current_return = self.vol_estimator[0]
current_vol # ... (Z-score calculation, jump detection, jump_signal and jump_magnitude assignment)
# Diffusion trend: smoothed recent returns
self.change_buffer.append(current_return)
if len(self.change_buffer) >= 5:
= np.mean(list(self.change_buffer)[-5:])
recent_trend self.lines.diffusion_trend[0] = np.tanh(recent_trend / current_vol) # Normalized diffusion trend
The jump_signal
is for an upward jump,
for a downward jump, and decays over
time if no new jump occurs.
MomentumAfterJump
: This indicator
quantifies the consistency of price movement
(momentum_strength
) and its direction
(momentum_direction
) over a momentum_period
.
It’s designed to confirm whether the market is following through on a
jump or a general directional bias.
class MomentumAfterJump(bt.Indicator):
= ('momentum_strength', 'momentum_direction')
lines = (('momentum_period', 5), ('momentum_threshold', 0.6),)
params
def __init__(self):
self.returns = bt.indicators.PctChange(self.data.close, period=1)
self.momentum_buffer = deque(maxlen=self.params.momentum_period)
def next(self):
self.momentum_buffer.append(self.returns[0] if not np.isnan(self.returns[0]) else 0)
if len(self.momentum_buffer) >= self.params.momentum_period:
= np.array(list(self.momentum_buffer))
returns_array = np.sum(returns_array > 0)
positive_returns = np.sum(returns_array < 0)
negative_returns = len(returns_array)
total_returns
if total_returns > 0:
= max(positive_returns, negative_returns) / total_returns
strength = 1 if positive_returns > negative_returns else -1
direction self.lines.momentum_strength[0] = strength
self.lines.momentum_direction[0] = direction if strength >= self.params.momentum_threshold else 0
TrendVolatilityFilter
: This
indicator measures ADX and ATR, but crucially, it also counts how many
periods they have been rising over a
rising_lookback
window. This provides a “sustained rising”
condition, ensuring that the market is not just trending or volatile,
but that these characteristics are actively strengthening.
class TrendVolatilityFilter(bt.Indicator):
= ('adx_rising', 'atr_rising', 'adx_strength', 'atr_strength', 'adx_rising_count', 'atr_rising_count')
lines = (('adx_period', 7), ('atr_period', 7), ('min_adx_level', 25), ('rising_lookback', 7), ('min_rising_periods', 5),)
params
def __init__(self):
self.adx = bt.indicators.DirectionalMovementIndex(period=self.params.adx_period)
self.atr = bt.indicators.AverageTrueRange(period=self.params.atr_period)
self.adx_buffer = deque(maxlen=self.params.rising_lookback + 1)
self.atr_buffer = deque(maxlen=self.params.rising_lookback + 1)
def next(self):
= self.adx[0]
current_adx = self.atr[0]
current_atr self.adx_buffer.append(current_adx)
self.atr_buffer.append(current_atr)
# Count rising periods
= sum(1 for i in range(1, len(self.adx_buffer)) if self.adx_buffer[i] > self.adx_buffer[i-1])
adx_rising_count = sum(1 for i in range(1, len(self.atr_buffer)) if self.atr_buffer[i] > self.atr_buffer[i-1])
atr_rising_count
# Check for sustained rising and min ADX level
= (adx_rising_count >= self.params.min_rising_periods and current_adx >= self.params.min_adx_level and current_adx > self.adx_buffer[0])
adx_sustained_rising = (atr_rising_count >= self.params.min_rising_periods and current_atr > self.atr_buffer[0])
atr_sustained_rising
self.lines.adx_rising[0] = 1 if adx_sustained_rising else 0
self.lines.atr_rising[0] = 1 if atr_sustained_rising else 0
JumpDiffusionMomentumStrategy
This strategy combines the signals from the three indicators. It seeks to enter trades when a significant jump occurs, followed by or confirmed by positive momentum/diffusion, and only if the underlying market conditions (ADX and ATR) show a sustained increase in trend strength and volatility, respectively. This layered approach aims to filter out false signals and capitalize on strong, confirmed movements.
class JumpDiffusionMomentumStrategy(bt.Strategy):
= (
params 'jump_threshold', 2.0), ('min_jump_size', 0.05), # Jump detection sensitivity
('momentum_threshold', 0.7), # Strength required for momentum confirmation
('diffusion_weight', 0.5), # How much diffusion trend matters
('hold_periods', 7), # Minimum time to hold position
('min_adx_level', 20), # Minimum ADX for trade
('require_adx_rising', True), ('require_atr_rising', True), # Filter conditions
('trailing_stop_pct', 0.05), ('stop_loss_pct', 0.1), # Risk Management
('printlog', False),
(
)
def __init__(self):
self.jump_detector = JumpDiffusionDetector(...) # Initialize with params
self.momentum_detector = MomentumAfterJump(...) # Initialize with params
self.trend_vol_filter = TrendVolatilityFilter(...) # Initialize with params
def next(self):
# ... (Fetch indicator values and check for data validity)
# Entry signals (if no position)
if not self.position:
# Filter condition: ADX & ATR must show sustained rising trend/volatility
= ( (not self.params.require_adx_rising) or bool(self.trend_vol_filter.adx_rising[0])) and \
trend_vol_filters_passed not self.params.require_atr_rising) or bool(self.trend_vol_filter.atr_rising[0]))
( (
# Long entry logic: Strong positive jump + momentum/diffusion + filter confirmation
if (self.jump_detector.jump_signal[0] > 0.8 and # A clear positive jump
self.momentum_detector.momentum_direction[0] > 0 or self.jump_detector.diffusion_trend[0] * self.params.diffusion_weight > 0.1) and # Confirmed by momentum or diffusion
(# Filter conditions met
trend_vol_filters_passed): self.order = self.buy()
# Short entry logic: Strong negative jump + momentum/diffusion + filter confirmation
elif (self.jump_detector.jump_signal[0] < -0.8 and # A clear negative jump
self.momentum_detector.momentum_direction[0] < 0 or self.jump_detector.diffusion_trend[0] * self.params.diffusion_weight < -0.1) and # Confirmed by momentum or diffusion
(# Filter conditions met
trend_vol_filters_passed): self.order = self.sell()
# ... (Position management, trailing stop, and fixed stop-loss logic)
The strategy is deliberately conservative, requiring multiple layers of confirmation. A “jump” alone isn’t enough; it must be followed by momentum (or strong diffusion) and occur within a market environment that shows strengthening trend and volatility.
The strategy is backtested on ETH-USD
data over a 3-year
period. The run_jump_diffusion_strategy()
function sets up
the Backtrader environment, including initial capital, commissions, and
a suite of analyzers (Returns, Trades, Sharpe Ratio, Drawdown, VWR) to
provide a comprehensive performance overview.
def run_jump_diffusion_strategy():
print("Downloading data for ETH-USD...")
= yf.download('ETH-USD', period='3y', auto_adjust=False).droplevel(1, axis=1) # Applying saved instruction
data
= bt.Cerebro()
cerebro # Add strategy with parameters
cerebro.addstrategy(JumpDiffusionMomentumStrategy, ...) =data))
cerebro.adddata(bt.feeds.PandasData(dataname10000.0)
cerebro.broker.setcash(=0.001)
cerebro.broker.setcommission(commission=95)
cerebro.addsizer(bt.sizers.PercentSizer, percents
# Add various analyzers for comprehensive metrics
='returns')
cerebro.addanalyzer(bt.analyzers.Returns, _name='trades')
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='sharpe')
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='drawdown')
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='vwr')
cerebro.addanalyzer(bt.analyzers.VWR, _name
= cerebro.broker.getvalue()
starting_value print(f'Starting Value: ${starting_value:,.2f}')
= cerebro.run()
results = cerebro.broker.getvalue()
final_value
# ... (Print detailed performance summary including total return, trade stats,
# risk metrics, and Buy & Hold comparison.)
# Plot results
'figure.figsize'] = [12, 8]
plt.rcParams[='line', iplot=False, figsize=(12, 6))[0][0]
cerebro.plot(style
plt.tight_layout()
plt.show()
if __name__ == '__main__':
run_jump_diffusion_strategy()
The performance metrics provide a quantitative assessment, while the plot offers a visual depiction of the strategy’s equity curve against the asset’s price.
The Jump-Diffusion Momentum Strategy offers an advanced approach to understanding and reacting to distinct price behaviors in financial markets. By systematically identifying significant price jumps and then confirming subsequent directional bias with momentum and market environment filters, it aims to capture potentially profitable opportunities that arise from these discontinuities. This approach moves beyond simple trend following or mean reversion, seeking to exploit specific market dynamics. As with all complex quantitative strategies, careful parameter calibration, robust out-of-sample testing, and continuous monitoring are essential to ensure its effectiveness in live trading environments.