← Back to Home
Capturing Market Momentum with Levy Flights A Python Implementation

Capturing Market Momentum with Levy Flights A Python Implementation

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.

Understanding Levy Flights in Finance

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:

\phi(t)
= \mathbb{E}\bigl[e^{i t X}\bigr]
= \exp\!\Bigl(
  i\,\delta\,t 
  \;-\; \gamma\,\lvert t\rvert^\alpha\,
  \Bigl[\,1 + i\,\beta\,\operatorname{sign}(t)\,\omega(\alpha,t)\Bigr]
\Bigr)\,.

where:

and \omega(\alpha, t) is defined as:

\omega(\alpha, t) = 
\begin{cases}
\tan\left(\frac{\pi\alpha}{2}\right), & \text{if } \alpha \neq 1 \\
\frac{2}{\pi}\log|t|, & \text{if } \alpha = 1
\end{cases}

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 (\alpha < 2) and any inherent biases (\beta \ne 0). 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.

Python Implementation: A Walkthrough

The provided Python script implements a sophisticated Levy flight momentum strategy, structured into several classes:

  1. LevyFlightConfig: Manages all parameters for the analysis, strategy, and backtest.
  2. LevyFlightAnalyzer: Handles the core analysis – estimating Levy parameters, detecting jumps, identifying market regimes, and calculating Levy-based momentum.
  3. LevyMomentumStrategy: Uses the analyzer’s output to generate trading signals and execute trades with risk management.
  4. LevyWalkForwardBacktest: Implements a walk-forward methodology to test the strategy over time.

Let’s delve into each.

1. Configuration: 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:

2. Core Analysis: 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)
            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) ...

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_magnitudes

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_regime

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_momentum

3. Strategy Logic: 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) ...
        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 index

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 series

4. Backtesting Framework: 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
            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) ...

Supporting Functions and Execution

The script also includes:

Main Execution Block (if __name__ == "__main__":)

  1. Initializes LevyFlightConfig.
  2. Allows for customization of parameters (commented out in the provided code, but available for users).
  3. Optionally runs 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).
  4. Initializes and runs the LevyWalkForwardBacktest.
  5. Prints a detailed summary of performance metrics and generates plots.
  6. Includes robust error handling and troubleshooting tips.
Pasted image 20250605033426.png

How It All Works Together: Strategy Flow

  1. Configuration: Define all parameters (Levy estimation, momentum, risk, backtest).
  2. Data Acquisition: Fetch historical price data using yfinance.
  3. Walk-Forward Loop:
    • For each period:
      • Training Data Slice: Used implicitly by generate_signals to establish historical context.
      • Testing Data Slice: The period on which trading is simulated.
      • Signal Generation (LevyMomentumStrategy.generate_signals on combined train-tail + test data):
        • Calculate rolling returns.
        • For each point in the (combined) returns window:
          • Estimate Levy parameters (\alpha, \beta, \gamma, \delta) from the preceding levy_window of returns using LevyFlightAnalyzer.estimate_levy_parameters.
          • Detect jumps using LevyFlightAnalyzer.detect_jumps.
          • Identify market regime (jump/diffusion) using LevyFlightAnalyzer.identify_regime.
          • Calculate jump momentum and diffusion momentum using LevyFlightAnalyzer.calculate_levy_momentum.
          • Combine these momentums based on the current regime and configured weights.
          • Apply a trend filter (EMA-based).
          • Normalize the signal using volatility and tanh.
      • Trade Execution (LevyMomentumStrategy.execute_trades on test data with its signals):
        • Apply stop-loss, take-profit, and max holding period rules.
        • Enter long/short positions if the signal strength exceeds signal_threshold.
        • Calculate daily portfolio returns and track capital.
  4. Results Aggregation & Reporting: Compile metrics (total return, Sharpe, drawdown, win rate, etc.) across all walk-forward periods and plot the performance.

Conclusion & Potential Enhancements

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.