The Hurst exponent \(H\) quantifies the long-term memory or persistence of a time series.
\(H=0.5\) indicates a pure random walk (no autocorrelation).
\(H>0.5\) signals persistence (trending behavior).
\(H<0.5\) signals anti-persistence (mean-reverting behavior).
One classic method to estimate \(H\) is Rescaled-Range Analysis. Given a time series \(\{X_t\}\), for a window of length \(n\):
Compute the mean: \[\bar{X}_n \;=\; \frac{1}{n}\sum_{t=1}^n X_t.\]
Form the mean‐adjusted cumulative deviate series: \[Y_k \;=\; \sum_{t=1}^k (X_t - \bar{X}_n), \quad k = 1,2,\dots,n\]
Compute the range: \[R(n) \;=\; \max_{1 \le k \le n} Y_k \;-\; \min_{1 \le k \le n} Y_k\]
Compute the standard deviation: \[S(n) \;=\; \sqrt{\frac{1}{n}\sum_{t=1}^n (X_t - \bar{X}_n)^2}\]
Form the rescaled range: \[\frac{R(n)}{S(n)}\]
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.}\]
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).
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:
HurstExponentIndicator
calculates H over a rolling window
(using the hurst
library).hurst_threshold
(e.g., 0.7 in the
example run).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),
)
hurst_window
& hurst_threshold
: Define
the filter. Longer windows give smoother but laggier H values. The
threshold determines how strong the persistence must be. (Note: Your
example run used hurst_window=180
,
hurst_threshold=0.7
).sma_fast
& sma_slow
: Define the
periods for the simple moving average crossover signal. (Note: Your
example run used sma_fast=30
,
sma_slow=50
).trail_percent
: The trailing stop percentage. (Note:
Your example run used trail_percent=0.05
).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:
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.