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.
LevyFlightConfig
This 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.LevyFlightAnalyzer
This 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)
= np.clip(2.0 - np.abs(stats.kurtosis(returns_clean)) / 10, 1.1, 1.99)
alpha_init # ... (beta_init, gamma_init, delta_init) ...
def levy_loglike(params):
= params
alpha, beta, gamma, delta # ... (bounds check) ...
try:
# Approximate log-likelihood
= (returns_clean - delta) / gamma
centered_returns if alpha == 2: # Gaussian case
= -0.5 * np.sum(centered_returns**2) - len(returns_clean) * np.log(gamma * np.sqrt(2*np.pi))
loglike else: # Approximation for general Levy case
= -np.sum(np.abs(centered_returns)**alpha) - len(returns_clean) * np.log(gamma)
loglike return -loglike
except: return 1e10
= minimize(
result
levy_loglike,
[alpha_init, beta_init, gamma_init, delta_init],=[(1.1, 1.99), (-0.99, 0.99), (1e-6, None), (None, None)],
bounds='L-BFGS-B'
method
)# ... (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) ...
= returns / (gamma + 1e-10) # Standardize by Levy scale 'gamma'
std_returns = self.config.jump_threshold
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
= np.std(returns[max(0, i-5):i+6])
local_vol if np.abs(returns[i]) > threshold * local_vol: # Compare raw return to local vol
= 1
jumps[i] = returns[i]
jump_magnitudes[i] return jumps, jump_magnitudes
gamma
.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) ...
= np.sum(jumps[-10:]) / 10 # Proportion of jumps in last 10 periods
jump_intensity
if jump_intensity > self.config.diffusion_threshold:
= 'jump'
new_regime else:
= 'diffusion'
new_regime
# 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):
= self.regime_state # Keep old regime with some probability
new_regime
self.regime_state = new_regime
return new_regime
jump_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
= 0.0
jump_momentum if np.sum(recent_jumps) > 0:
= recent_jump_mags[recent_jumps == 1]
jump_returns if len(jump_returns) > 0:
= np.array([self.config.jump_signal_decay**i for i in range(len(jump_returns))])
weights = weights[::-1] # More recent jumps get higher weight
weights = np.average(jump_returns, weights=weights)
jump_momentum
# Diffusion momentum: simple mean of recent non-jump returns
= recent_returns[recent_jumps == 0]
diffusion_returns = 0.0
diffusion_momentum if len(diffusion_returns) > 0:
= np.mean(diffusion_returns)
diffusion_momentum
return jump_momentum, diffusion_momentum
momentum_lookback
window. Recent jumps are given higher
weight due to jump_signal_decay
.LevyMomentumStrategy
This 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) ...
= np.zeros(len(prices))
signals # ... (levy_data dictionary for storing analysis outputs) ...
for i in range(self.config.levy_window, len(returns)): # Rolling window
= returns.iloc[i-self.config.levy_window:i].values
window_returns
= self.analyzer.estimate_levy_parameters(window_returns)
alpha, beta, gamma, delta = self.analyzer.detect_jumps(window_returns, alpha, gamma)
jumps, jump_magnitudes = self.analyzer.identify_regime(window_returns, jumps)
regime = self.analyzer.calculate_levy_momentum(
jump_momentum, diffusion_momentum
window_returns, jumps, jump_magnitudes, regime
)
# Combine signals based on regime
if regime == 'jump':
= (jump_momentum * self.config.jump_momentum_weight +
signal * self.config.diffusion_momentum_weight)
diffusion_momentum else: # diffusion regime
= (diffusion_momentum * self.config.diffusion_momentum_weight +
signal * self.config.jump_momentum_weight * 0.5) # Jumps have less weight in diffusion
jump_momentum
# Apply trend filter (boost signal if aligned with EMA trend)
= prices.iloc[max(0, i-self.config.trend_ema_span):i+1]
trend_prices if len(trend_prices) > 5:
= 1 if trend_prices.iloc[-1] > trend_prices.ewm(span=self.config.trend_ema_span).mean().iloc[-1] else -1
trend_direction *= (1 + 0.3 * trend_direction)
signal
# Normalize signal by volatility using tanh (to bound between -1 and 1)
= np.std(window_returns[-self.config.volatility_window:])
volatility = np.tanh(signal / (volatility + 1e-6))
signal
= signal # Store signal for the current point in time 'i' in the returns array
signals[i] # 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:
= pd.Series(0.0, index=prices.index)
final_signals if len(returns) > 0: # Ensure returns is not empty
self.config.levy_window+1 : len(returns)+1] = signals[self.config.levy_window : len(returns)]
final_signals.iloc[
# Store analysis data
if len(levy_data['alpha']) > 0:
= self.config.levy_window + 1 # +1 because returns are used
analysis_index_start = analysis_index_start + len(levy_data['alpha'])
analysis_index_end 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 index
i
, 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:
= (price - self.entry_price) / self.entry_price * self.position
pnl_pct 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):
= signals.iloc[i] # Use signal for current day i
signal_value
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) ...
# Store actual return for the day based on position
daily_returns.append(daily_return_for_day) # 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 series
config.signal_threshold
and no conflicting position is
open.LevyWalkForwardBacktest
This 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
= current_date
train_start = current_date + timedelta(days=self.config.train_period)
train_end = train_end
test_start = train_end + timedelta(days=self.config.test_period)
test_end # ... (break if test_end > end_date) ...
= self.get_data(train_start, train_end) # Prices for training (parameter estimation)
train_data = self.get_data(test_start, test_end) # Prices for testing (trading)
test_data # ... (data validation) ...
= self.run_strategy_period(train_data, test_data, iteration)
result # ... (store result, increment current_date) ...
return self.compile_results()
def run_strategy_period(self, train_data, test_data, iteration):
try:
= LevyMomentumStrategy(self.config)
strategy
# 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
= pd.concat([train_data.tail(self.config.levy_window), test_data])
combined_data = strategy.generate_signals(combined_data)
signals_on_combined # Extract signals relevant only to the test_data period
= signals_on_combined.tail(len(test_data))
test_signals
= strategy.execute_trades(test_data, test_signals)
trades, daily_returns, final_capital # ... (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.