← Back to Home
Can the Hurst Exponent Reliably Identify Trends

Can the Hurst Exponent Reliably Identify Trends

1. Regime Detection via the Hurst Exponent

1.1. Concept

The Hurst exponent \(H\) quantifies the long-term memory or persistence of a time series.

1.2. R/S Analysis

One classic method to estimate \(H\) is Rescaled-Range Analysis. Given a time series \(\{X_t\}\), for a window of length \(n\):

  1. Compute the mean: \[\bar{X}_n \;=\; \frac{1}{n}\sum_{t=1}^n X_t.\]

  2. Form the mean‐adjusted cumulative deviate series: \[Y_k \;=\; \sum_{t=1}^k (X_t - \bar{X}_n), \quad k = 1,2,\dots,n\]

  3. Compute the range: \[R(n) \;=\; \max_{1 \le k \le n} Y_k \;-\; \min_{1 \le k \le n} Y_k\]

  4. Compute the standard deviation: \[S(n) \;=\; \sqrt{\frac{1}{n}\sum_{t=1}^n (X_t - \bar{X}_n)^2}\]

  5. Form the rescaled range: \[\frac{R(n)}{S(n)}\]

  6. Repeat for multiple window sizes \(n\), then fit a power‐law: \[E\!\Bigl[\tfrac{R(n)}{S(n)}\Bigr] \;\propto\; n^H\]

    Taking logs and performing a linear regression:\[\log\bigl(R(n)/S(n)\bigr) \;=\; H\,\log(n) \;+\;\text{const.}\]

2. Trend Signal: SMA Crossover

2.1. Simple Moving Averages

Define two SMAs on price \(P_t\):

\[\text{SMA}_{\text{fast},t} =\frac{1}{N_f}\sum_{i=0}^{N_f-1}P_{t-i}, \quad \text{SMA}_{\text{slow},t} =\frac{1}{N_s}\sum_{i=0}^{N_s-1}P_{t-i}\]

with \(N_f < N_s\) (e.g. 20 vs 50, or 30 vs 50).

2.2. Crossover Signal

The crossover indicator is

\[\text{Cross}_t = \begin{cases} +1, & \text{if }\text{SMA}_{\text{fast},t} > \text{SMA}_{\text{slow},t} \text{ and } \text{SMA}_{\text{fast},t-1}\le \text{SMA}_{\text{slow},t-1},\\ -1, & \text{if }\text{SMA}_{\text{fast},t} < \text{SMA}_{\text{slow},t} \text{ and } \text{SMA}_{\text{fast},t-1}\ge \text{SMA}_{\text{slow},t-1},\\ 0, & \text{otherwise}. \end{cases}\]

Can we improve trend-following strategies by only trading when the market exhibits strong trending characteristics? The Hurst exponent (H) offers a potential way to quantify this. It measures the long-term memory or persistence of a time series:

This article explores a backtrader strategy that calculates the Hurst exponent and uses it as a regime filter. It only activates a simple trend-following system (an SMA Crossover) when H indicates a trending regime (H > threshold). Exits are managed using a trailing stop-loss.

Strategy Logic Overview:

  1. Hurst Calculation: A custom HurstExponentIndicator calculates H over a rolling window (using the hurst library).
  2. Regime Filter: The strategy checks if the current H is above a defined hurst_threshold (e.g., 0.7 in the example run).
  3. Entry Signal: If the market is deemed “trending” (H > threshold) and the strategy is flat, it checks for an SMA crossover signal to enter long (fast SMA > slow SMA) or short (fast SMA < slow SMA).
  4. Exit Signal: Once in a position, a percentage-based trailing stop-loss (trail_percent) handles the exit exclusively.

The Supporting Indicator: HurstExponentIndicator

(The code for HurstExponentIndicator, corrected for the AttributeError, is assumed here. It calculates hurst over a window and plots it in a separate panel).

The Strategy Class: HurstFilteredTrendStrategy

Let’s examine the strategy class implementing this logic.

1. Parameters (params)

These allow configuration of the Hurst filter, the trend system, and the exit mechanism.

Python

# --- Inside HurstFilteredTrendStrategy class ---
    params = (
        # Hurst parameters
        ('hurst_window', 100),       # Window for Hurst calculation
        ('hurst_threshold', 0.55),   # Hurst value above which trend signals are enabled

        # Trend parameters (SMA Crossover example)
        ('sma_fast', 20),            # Fast SMA period
        ('sma_slow', 50),            # Slow SMA period

        # Exit parameter
        ('trail_percent', 0.05),     # Trailing stop percentage

        # Logging
        ('printlog', True),
    )

2. Initialization (__init__)

Sets up the required indicators.

Python

# --- Inside HurstFilteredTrendStrategy class ---
    def __init__(self):
        # Instantiate the Hurst Exponent Indicator
        self.hurst = HurstExponentIndicator(
            self.data.close, # Pass the close price series
            window=self.p.hurst_window
        )

        # Instantiate the trend indicators (SMA Crossover)
        sma_fast = bt.indicators.SimpleMovingAverage(self.data.close, period=self.p.sma_fast)
        sma_slow = bt.indicators.SimpleMovingAverage(self.data.close, period=self.p.sma_slow)
        self.sma_cross = bt.indicators.CrossOver(sma_fast, sma_slow) # 1 for cross up, -1 for cross down

        # Order trackers
        self.order = None
        self.stop_order = None

        # (Parameter logging code omitted for brevity)
        ...

3. Core Logic (next)

This method executes on each bar, checks the regime, and looks for entry signals if applicable.

Python

# --- Inside HurstFilteredTrendStrategy class ---
    def next(self):
        # Check if indicators/orders are ready
        if self.order or len(self.hurst) == 0 or len(self.sma_cross) == 0:
            return

        current_hurst = self.hurst.hurst[0]
        current_cross = self.sma_cross[0]
        current_close = self.data.close[0]
        current_position_size = self.position.size

        # Check if Hurst calculation failed
        if np.isnan(current_hurst):
            return # Skip bar if Hurst is invalid

        # --- Regime Filter ---
        # Check if Hurst value is above the threshold
        is_trending_regime = current_hurst > self.p.hurst_threshold

        # --- Trading Logic ---
        if current_position_size == 0: # If FLAT
            # (Safety check for stray stop orders omitted)
            ...
            # --- Entry Check ---
            # Only consider entries if the regime filter allows it
            if is_trending_regime:
                if current_cross > 0: # Buy signal: Fast SMA crosses above Slow SMA
                    self.log(f'BUY CREATE (Hurst Trend Regime & SMA Cross > 0), H={current_hurst:.3f}, Close={current_close:.2f}', doprint=True)
                    self.order = self.buy()
                elif current_cross < 0: # Sell signal: Fast SMA crosses below Slow SMA
                    self.log(f'SELL CREATE (Hurst Trend Regime & SMA Cross < 0), H={current_hurst:.3f}, Close={current_close:.2f}', doprint=True)
                    self.order = self.sell()
            # If not in a trending regime, do nothing.

        else: # If IN A POSITION
            # Exits are handled by the trailing stop, so no action needed here.
            pass

The key is the if is_trending_regime: check, which gates the SMA crossover signal.

4. Exit Logic (notify_order)

Exits are managed by placing a StopTrail order after an entry fills.

Python

# --- Inside HurstFilteredTrendStrategy class ---
    def notify_order(self, order):
        # (Initial checks for Submitted/Accepted omitted)
        ...
        if order.status == order.Completed:
            # If it was our entry order completing...
            if self.order and order.ref == self.order.ref:
                entry_type = "BUY" if order.isbuy() else "SELL"
                exit_func = self.sell if order.isbuy() else self.buy # Function to place opposite order

                # (Log entry execution omitted)
                ...

                # Place the TRAILING STOP order
                if self.p.trail_percent and self.p.trail_percent > 0.0:
                    self.stop_order = exit_func(exectype=bt.Order.StopTrail,
                                                trailpercent=self.p.trail_percent)
                    # (Log stop placement omitted)
                    ...
                self.order = None # Reset entry order tracker

            # If it was our stop order completing...
            elif self.stop_order and order.ref == self.stop_order.ref:
                 # (Log stop execution omitted)
                 ...
                 self.stop_order = None # Reset stop order tracker
        # (Handle Failed orders omitted)
        ...

Performance Example & Considerations

Running this strategy (with parameters hurst_window=180, hurst_threshold=0.7, sma_fast=30, sma_slow=50, trail_percent=0.05 on BTC-USD from 2021-01-01 to 2023-01-01, as per your example setup) yielded the following results in one test:

Pasted image 20250424193637.png
Starting Portfolio Value: 10,000.00

2021-11-29 SELL CREATE (Hurst Trend Regime & SMA Cross < 0), H=0.740, Close=57806.57
2021-11-30 SELL EXECUTED @ 57830.11, Size: -0.1643, Cost: -9503.87, Comm: 9.50
2021-11-30 Trailing Stop Placed for SELL order ref 8475 at 5.00% trail
2021-12-07 STOP BUY (Cover) EXECUTED @ 51660.74, Size: 0.1643, Cost: -9503.87, Comm: 8.49
2021-12-07 OPERATION PROFIT, GROSS 1013.88, NET 995.89
2022-04-28 SELL CREATE (Hurst Trend Regime & SMA Cross < 0), H=0.712, Close=39773.83
2022-04-29 SELL EXECUTED @ 39768.62, Size: -0.2626, Cost: -10444.73, Comm: 10.44
2022-04-29 Trailing Stop Placed for SELL order ref 8477 at 5.00% trail
2022-05-04 STOP BUY (Cover) EXECUTED @ 39600.62, Size: 0.2626, Cost: -10444.73, Comm: 10.40
2022-05-04 OPERATION PROFIT, GROSS 44.12, NET 23.28
2022-07-31 BUY CREATE (Hurst Trend Regime & SMA Cross > 0), H=0.771, Close=23336.90
2022-08-01 BUY EXECUTED @ 23336.72, Size: 0.4486, Cost: 10468.13, Comm: 10.47
2022-08-01 Trailing Stop Placed for BUY order ref 8479 at 5.00% trail
2022-08-18 STOP SELL (Exit Long) EXECUTED @ 23202.86, Size: -0.4486, Cost: 10468.13, Comm: 10.41
2022-08-18 OPERATION PROFIT, GROSS -60.04, NET -80.92
2022-08-31 SELL CREATE (Hurst Trend Regime & SMA Cross < 0), H=0.830, Close=20049.76
2022-09-01 SELL EXECUTED @ 20050.50, Size: -0.5183, Cost: -10391.72, Comm: 10.39
2022-09-01 Trailing Stop Placed for SELL order ref 8481 at 5.00% trail
2022-09-09 STOP BUY (Cover) EXECUTED @ 19779.55, Size: 0.5183, Cost: -10391.72, Comm: 10.25
2022-09-09 OPERATION PROFIT, GROSS 140.43, NET 119.78
2022-11-14 SELL CREATE (Hurst Trend Regime & SMA Cross < 0), H=0.702, Close=16618.20
2022-11-15 SELL EXECUTED @ 16617.48, Size: -0.6321, Cost: -10504.68, Comm: 10.50
2022-11-15 Trailing Stop Placed for SELL order ref 8483 at 5.00% trail
2022-11-23 STOP BUY (Cover) EXECUTED @ 16576.65, Size: 0.6321, Cost: -10504.68, Comm: 10.48
2022-11-23 OPERATION PROFIT, GROSS 25.81, NET 4.83
Final Portfolio Value:  11,062.86
Total Return: 10.63%

--- Performance Analysis ---
Sharpe Ratio: 0.028
Max Drawdown: 7.23%
Max Money Drawdown: 832.72
System Quality Number (SQN): 1.20
SQN Trades: 5

--- Trade Analysis ---
Total Closed Trades: 5
Total Open Trades: 0

Win Rate: 80.00% (4 wins)
Loss Rate: 20.00% (1 losses)

Total Net Profit/Loss: 1,062.86
Average Net Profit/Loss per Trade: 212.57

Average Winning Trade: 285.94
Average Losing Trade: -80.92
Max Winning Trade: 995.89
Max Losing Trade: -80.92

Profit Factor: 14.13

Average Bars in Trade: 9.00
Max Bars in Winning Trade: 8
Max Bars in Losing Trade: 17

Interpretation: In this specific run, the Hurst filter drastically reduced the number of trades and helped achieve excellent drawdown control compared to unfiltered strategies. However, the risk-adjusted returns (Sharpe) and system quality (SQN) remained very low. The very short average trade duration suggests the 5% trailing stop might have been too tight, cutting off winners before they could significantly contribute to profit, thus limiting the overall return despite the decent win rate and profit factor.

Conclusion:

Using the Hurst exponent as a regime filter shows potential for improving selectivity and managing risk in trend-following systems. This backtrader example provides a framework. However, the results highlight the critical need for extensive parameter tuning (hurst_window, hurst_threshold, trend indicator periods, trail_percent) through optimization. Finding the right balance to control risk while allowing profitable trends to run is key to developing this concept into a potentially viable strategy.