Financial markets are complex systems, often exhibiting behaviors that traditional models based on normal distributions struggle to capture. One such characteristic is the presence of “heavy tails” in return distributions and sudden, large movements or “jumps.” Levy flights, and the associated Levy stable distributions, provide a mathematical framework to model these phenomena. This article explores a Python-based trading strategy that attempts to detect and leverage market momentum by analyzing Levy flight characteristics, identifying jump and diffusion regimes, and applying this understanding to generate trading signals.
In contrast to Brownian motion (which underlies models assuming normal distributions and constant volatility), Levy flights are a type of random walk where the step lengths are not constant but are drawn from a probability distribution with heavy tails. This means that while most price changes (steps) might be small, there’s a non-negligible probability of very large price changes occurring.
Key Characteristics of Levy Stable Distributions:
Levy stable distributions are uniquely characterized by their characteristic function:
where:
and
is defined as:
Why are they relevant? Financial asset returns often display: 1. Leptokurtosis (Fat Tails): More extreme returns (both positive and negative) than predicted by a normal distribution. 2. Jumps: Sudden, large price movements that are hard to explain with continuous diffusion models.
By estimating the parameters of a Levy stable distribution from
historical returns, we can gain insights into the underlying market
dynamics, such as the propensity for jumps () and
any inherent biases (
). This
strategy uses these insights to differentiate between jump
regimes (dominated by large, sudden moves) and
diffusion regimes (more random, smaller price changes),
and then calculates momentum differently for each.
The provided Python script implements a sophisticated Levy flight momentum strategy, structured into several classes:
LevyFlightConfig: Manages all
parameters for the analysis, strategy, and backtest.LevyFlightAnalyzer: Handles the core
analysis – estimating Levy parameters, detecting jumps, identifying
market regimes, and calculating Levy-based momentum.LevyMomentumStrategy: Uses the
analyzer’s output to generate trading signals and execute trades with
risk management.LevyWalkForwardBacktest: Implements a
walk-forward methodology to test the strategy over time.Let’s delve into each.
LevyFlightConfigThis class centralizes all the tunable parameters of the strategy. This is excellent practice for managing complexity and facilitating experimentation.
class LevyFlightConfig:
"""Configuration class for Levy Flight Momentum Detection"""
def __init__(self):
# Levy Flight Parameters
self.levy_window = 50 # Window for Levy parameter estimation
self.alpha_bounds = (1.1, 2.0) # Stability parameter bounds (1 < α ≤ 2)
self.beta_bounds = (-1.0, 1.0) # Skewness parameter bounds
self.jump_threshold = 2.0 # Standard deviations for jump detection
self.diffusion_threshold = 0.5 # Threshold for diffusive vs jump regime
# Momentum Detection Parameters
self.momentum_lookback = 20 # Lookback for momentum calculation
self.jump_momentum_weight = 2.0 # Weight for jump-driven momentum
self.diffusion_momentum_weight = 1.0 # Weight for diffusion momentum
self.regime_persistence = 0.8 # Regime switching persistence
# Signal Processing
self.jump_signal_decay = 0.9 # Exponential decay for jump signals
self.trend_ema_span = 12 # EMA span for trend extraction
self.volatility_window = 20 # Window for volatility estimation
self.signal_threshold = 0.02 # Trading signal threshold
# Risk Management
self.position_sizing = 1.0
self.stop_loss = 0.04 # 4% stop loss
self.take_profit = 0.08 # 8% take profit
self.max_position_hold = 10 # Max days to hold position
# Backtest Parameters
self.train_period = 90
self.test_period = 30
self.transaction_cost = 0.0015
self.initial_capital = 10000
def display_parameters(self):
# ... (displays parameters neatly) ...Key Parameters Explained:
levy_window: The number of past return observations
used to estimate the Levy distribution parameters.alpha_bounds, beta_bounds: Constraints for
the stability and skewness parameters during estimation. jump_threshold: How many standard deviations (or scaled
by gamma) a return must be to be considered a potential
jump.diffusion_threshold: A threshold on jump intensity to
switch between “jump” and “diffusion” regimes.momentum_lookback: Window for calculating short-term
momentum.jump_momentum_weight,
diffusion_momentum_weight: Different weights applied to
momentum calculated from jumps versus normal diffusion, allowing the
strategy to react differently based on the nature of price moves.LevyFlightAnalyzerThis class is the heart of the Levy flight detection mechanism.
2.1. Estimating Levy Parameters
(estimate_levy_parameters)
This method attempts to fit a Levy stable distribution to a window of returns.
def estimate_levy_parameters(self, returns):
"""Estimate Levy stable distribution parameters using MLE"""
try:
# ... (input validation and data cleaning) ...
# Initial parameter guesses based on moments (kurtosis, skewness)
alpha_init = np.clip(2.0 - np.abs(stats.kurtosis(returns_clean)) / 10, 1.1, 1.99)
# ... (beta_init, gamma_init, delta_init) ...
def levy_loglike(params):
alpha, beta, gamma, delta = params
# ... (bounds check) ...
try:
# Approximate log-likelihood
centered_returns = (returns_clean - delta) / gamma
if alpha == 2: # Gaussian case
loglike = -0.5 * np.sum(centered_returns**2) - len(returns_clean) * np.log(gamma * np.sqrt(2*np.pi))
else: # Approximation for general Levy case
loglike = -np.sum(np.abs(centered_returns)**alpha) - len(returns_clean) * np.log(gamma)
return -loglike
except: return 1e10
result = minimize(
levy_loglike,
[alpha_init, beta_init, gamma_init, delta_init],
bounds=[(1.1, 1.99), (-0.99, 0.99), (1e-6, None), (None, None)],
method='L-BFGS-B'
)
# ... (return results or initial guesses on failure) ...
except Exception as e:
# Fallback to simpler moment-based estimates if optimization fails
# ... (fallback calculations) ...abs(centered_returns)**alpha.
This is a practical heuristic but not the exact Levy log-likelihood
(which often involves numerical integration of the characteristic
function).scipy.optimize.minimize with the L-BFGS-B
method is used to find parameters that maximize the (approximate)
log-likelihood.2.2. Detecting Jumps (detect_jumps)
This method identifies returns that are likely “jumps” rather than part of normal diffusive price movements, given the estimated Levy parameters.
def detect_jumps(self, returns, alpha, gamma):
"""Detect jump events in the time series"""
# ... (initialization) ...
std_returns = returns / (gamma + 1e-10) # Standardize by Levy scale 'gamma'
threshold = self.config.jump_threshold
for i in range(len(returns)):
if np.abs(std_returns[i]) > threshold:
# Check if it's a true jump or just high volatility in a local window
local_vol = np.std(returns[max(0, i-5):i+6])
if np.abs(returns[i]) > threshold * local_vol: # Compare raw return to local vol
jumps[i] = 1
jump_magnitudes[i] = returns[i]
return jumps, jump_magnitudesgamma.config.jump_threshold.threshold * local_vol (standard deviation in a small
rolling window) to further distinguish jumps from periods of generally
high (but perhaps not Levy-jump-like) volatility.2.3. Identifying Market Regime
(identify_regime)
This method classifies the recent market behavior as either “jump-dominated” or “diffusion-dominated.”
def identify_regime(self, returns, jumps):
# ... (input validation) ...
jump_intensity = np.sum(jumps[-10:]) / 10 # Proportion of jumps in last 10 periods
if jump_intensity > self.config.diffusion_threshold:
new_regime = 'jump'
else:
new_regime = 'diffusion'
# Apply persistence to avoid rapid switching
if hasattr(self, 'regime_state') and self.regime_state != new_regime:
if np.random.random() > (1 - self.config.regime_persistence):
new_regime = self.regime_state # Keep old regime with some probability
self.regime_state = new_regime
return new_regimejump_intensity as the proportion of
detected jumps in a recent window (last 10 periods).config.diffusion_threshold,
the regime is classified as ‘jump’.config.regime_persistence factor. This prevents overly
frequent regime changes.2.4. Calculating Levy Momentum
(calculate_levy_momentum)
This is where distinct momentum measures for jumps and diffusion are computed.
def calculate_levy_momentum(self, returns, jumps, jump_magnitudes, regime):
# ... (input validation and slicing) ...
# Jump-driven momentum: exponentially weighted average of recent jump magnitudes
jump_momentum = 0.0
if np.sum(recent_jumps) > 0:
jump_returns = recent_jump_mags[recent_jumps == 1]
if len(jump_returns) > 0:
weights = np.array([self.config.jump_signal_decay**i for i in range(len(jump_returns))])
weights = weights[::-1] # More recent jumps get higher weight
jump_momentum = np.average(jump_returns, weights=weights)
# Diffusion momentum: simple mean of recent non-jump returns
diffusion_returns = recent_returns[recent_jumps == 0]
diffusion_momentum = 0.0
if len(diffusion_returns) > 0:
diffusion_momentum = np.mean(diffusion_returns)
return jump_momentum, diffusion_momentummomentum_lookback window. Recent jumps are given higher
weight due to jump_signal_decay.LevyMomentumStrategyThis class brings together the analysis from
LevyFlightAnalyzer to make trading decisions.
3.1. Generating Signals
(generate_signals)
This method performs a rolling analysis to generate a raw signal series.
def generate_signals(self, prices):
# ... (initialization, pct_change) ...
signals = np.zeros(len(prices))
# ... (levy_data dictionary for storing analysis outputs) ...
for i in range(self.config.levy_window, len(returns)): # Rolling window
window_returns = returns.iloc[i-self.config.levy_window:i].values
alpha, beta, gamma, delta = self.analyzer.estimate_levy_parameters(window_returns)
jumps, jump_magnitudes = self.analyzer.detect_jumps(window_returns, alpha, gamma)
regime = self.analyzer.identify_regime(window_returns, jumps)
jump_momentum, diffusion_momentum = self.analyzer.calculate_levy_momentum(
window_returns, jumps, jump_magnitudes, regime
)
# Combine signals based on regime
if regime == 'jump':
signal = (jump_momentum * self.config.jump_momentum_weight +
diffusion_momentum * self.config.diffusion_momentum_weight)
else: # diffusion regime
signal = (diffusion_momentum * self.config.diffusion_momentum_weight +
jump_momentum * self.config.jump_momentum_weight * 0.5) # Jumps have less weight in diffusion
# Apply trend filter (boost signal if aligned with EMA trend)
trend_prices = prices.iloc[max(0, i-self.config.trend_ema_span):i+1]
if len(trend_prices) > 5:
trend_direction = 1 if trend_prices.iloc[-1] > trend_prices.ewm(span=self.config.trend_ema_span).mean().iloc[-1] else -1
signal *= (1 + 0.3 * trend_direction)
# Normalize signal by volatility using tanh (to bound between -1 and 1)
volatility = np.std(window_returns[-self.config.volatility_window:])
signal = np.tanh(signal / (volatility + 1e-6))
signals[i] = signal # Store signal for the current point in time 'i' in the returns array
# Note: returns array is 1 shorter than prices array.
# This signal corresponds to prices.index[i+1]
# Adjust signals index to align with prices
# The loop goes up to len(returns)-1. `returns` starts from prices.index[1].
# So, the last signal signals[len(returns)-1] is for returns.index[len(returns)-1], which is prices.index[len(prices)-1].
# The signals array should be shifted to align with the price series for trading.
# Current code creates pd.Series(signals, index=prices.index) -> this makes signals of length prices,
# but the loop for calculation only fills up to len(returns) which is len(prices)-1.
# A more robust way to align:
final_signals = pd.Series(0.0, index=prices.index)
if len(returns) > 0: # Ensure returns is not empty
final_signals.iloc[self.config.levy_window+1 : len(returns)+1] = signals[self.config.levy_window : len(returns)]
# Store analysis data
if len(levy_data['alpha']) > 0:
analysis_index_start = self.config.levy_window + 1 # +1 because returns are used
analysis_index_end = analysis_index_start + len(levy_data['alpha'])
self.levy_analysis = pd.DataFrame(levy_data, index=prices.index[analysis_index_start:analysis_index_end])
return final_signals # Return pd.Series aligned with price indexi, the last levy_window returns are
analyzed.regime.
In a ‘jump’ regime, jump momentum is more influential. In ‘diffusion’,
its influence is halved.np.tanh to bound it between -1 and 1.
This makes the signal strength adaptive to volatility.signals and levy_analysis
needs care to ensure correct alignment with the original
prices index. The corrected final_signals part
aims to address this.3.2. Executing Trades
(execute_trades)
This method simulates trade execution based on the generated signals, incorporating risk management rules.
def execute_trades(self, prices, signals, verbose=False):
# ... (initialization of capital, positions list, trades list, daily_returns list) ...
for i, (date, price) in enumerate(prices.items()):
# ... (skip first day, calculate daily_return holder, update position_days) ...
# Risk management checks (Stop Loss, Take Profit, Max Hold)
if self.position != 0 and self.entry_price > 0:
pnl_pct = (price - self.entry_price) / self.entry_price * self.position
if self.config.stop_loss and pnl_pct < -self.config.stop_loss:
# ... (execute stop loss, update capital with transaction cost) ...
elif self.config.take_profit and pnl_pct > self.config.take_profit:
# ... (execute take profit) ...
elif self.position_days >= self.config.max_position_hold:
# ... (execute max hold exit) ...
# Trading signals
if i < len(signals):
signal_value = signals.iloc[i] # Use signal for current day i
if signal_value > self.config.signal_threshold and self.position <= 0: # Buy
# ... (handle existing short, enter long, record trade, apply transaction cost) ...
elif signal_value < -self.config.signal_threshold and self.position >= 0: # Sell
# ... (handle existing long, enter short, record trade, apply transaction cost) ...
# Calculate daily P&L based on holding position
# ... (calculate P&L if self.position is 1 or -1) ...
daily_returns.append(daily_return_for_day) # Store actual return for the day based on position
# Capital update should be based on actual P&L, not just raw daily_return if no position.
# The current script's capital update: capital *= (1 + daily_return) where daily_return is portfolio return.
return trades, daily_returns_series, capital # daily_returns should be a seriesconfig.signal_threshold and no conflicting position is
open.LevyWalkForwardBacktestThis class orchestrates the walk-forward backtesting process.
class LevyWalkForwardBacktest:
# ... (__init__, get_data) ...
def run_backtest(self, start_date=None, end_date=None):
# ... (date handling) ...
while current_date < end_date - timedelta(days=self.config.test_period):
# Define train and test periods
train_start = current_date
train_end = current_date + timedelta(days=self.config.train_period)
test_start = train_end
test_end = train_end + timedelta(days=self.config.test_period)
# ... (break if test_end > end_date) ...
train_data = self.get_data(train_start, train_end) # Prices for training (parameter estimation)
test_data = self.get_data(test_start, test_end) # Prices for testing (trading)
# ... (data validation) ...
result = self.run_strategy_period(train_data, test_data, iteration)
# ... (store result, increment current_date) ...
return self.compile_results()
def run_strategy_period(self, train_data, test_data, iteration):
try:
strategy = LevyMomentumStrategy(self.config)
# Generate signals: Use part of train_data for history/warm-up for signals on test_data
# This ensures the signal generation has enough historical context for the start of test_data
combined_data = pd.concat([train_data.tail(self.config.levy_window), test_data])
signals_on_combined = strategy.generate_signals(combined_data)
# Extract signals relevant only to the test_data period
test_signals = signals_on_combined.tail(len(test_data))
trades, daily_returns, final_capital = strategy.execute_trades(test_data, test_signals)
# ... (calculate metrics for this period and return) ...
# ... (error handling) ...
def compile_results(self):
# ... (aggregates results from all walk-forward periods) ...
def plot_results(self):
# ... (plots various performance charts based on compiled results) ...run_backtest
method iterates, creating rolling training and testing periods.run_strategy_period,
combined_data = pd.concat([train_data.tail(self.config.levy_window), test_data])
is crucial. It takes the last levy_window portion of the
training data and appends the test data. Signals are then generated on
this combined_data. This allows the
generate_signals method (which itself uses a rolling window
of levy_window) to have sufficient historical context when
generating signals for the very beginning of the test_data.
The test_signals are then sliced out for actual trading
simulation on the test_data. This is a sound approach to
avoid lookahead bias while ensuring signal stability.The script also includes:
test_levy_configurations(): A function
to quickly test different sets of Levy parameters on a short period.
This is useful for sensitivity analysis.analyze_levy_characteristics(): A
standalone function to download data for a symbol, estimate its overall
Levy parameters, analyze jump frequency, and plot diagnostic charts
(price, returns with jumps, return distribution vs. normal, Q-Q plot).
This helps in understanding the baseline characteristics of an asset
before applying the strategy.Main Execution Block
(if __name__ == "__main__":)
LevyFlightConfig.analyze_levy_characteristics for the
target symbol (BTC-USD) and can even dynamically adjust some
configuration parameters based on the analysis (e.g., if heavy
tails are detected).LevyWalkForwardBacktest.yfinance.generate_signals to establish historical context.LevyMomentumStrategy.generate_signals on combined
train-tail + test data):
levy_window of returns using
LevyFlightAnalyzer.estimate_levy_parameters.LevyFlightAnalyzer.detect_jumps.LevyFlightAnalyzer.identify_regime.LevyFlightAnalyzer.calculate_levy_momentum.tanh.LevyMomentumStrategy.execute_trades on test data with its
signals):
signal_threshold.This Python script presents a sophisticated and well-structured approach to developing a trading strategy based on Levy flight theory. It correctly identifies that financial markets often deviate from Gaussian assumptions and attempts to model and exploit the characteristics of jumps and heavy tails.
Key Strengths:
Potential Areas for Exploration/Refinement:
Overall, this script provides an excellent foundation for exploring advanced quantitative trading strategies that go beyond traditional mean-variance frameworks by incorporating the unique statistical properties often observed in financial markets.