Hurst Exponent
Calculate the Hurst exponent using multiple estimation methodologies including rescaled range analysis and detrended fluctuation analysis to measure the long-range dependence and fractal properties of financial time series for regime classification and model selection.
Statistical Analysis: Hurst Exponent Mean Reversion Test
This notebook explores the Hurst exponent, a measure used in statistical analysis to characterize the long-term memory of a time series. It is particularly useful in finance and hydrology for identifying whether a series is mean-reverting, trending, or a pure random walk.
Introduction to the Hurst Exponent
The Hurst exponent ($H$) quantifies the long-term memory of a time series. Its value ranges from 0 to 1, with different ranges indicating different behaviors:
- $H < 0.5$ (Anti-persistent/Mean-reverting): The series tends to revert to its mean. A decrease in the past implies an increase in the future, and vice versa. The closer to 0, the stronger the mean reversion.
- $H = 0.5$ (Random Walk/Brownian Motion): The series has no long-term memory. Future changes are independent of past changes. This is characteristic of efficient markets.
- $H > 0.5$ (Persistent/Trending): The series exhibits trending behavior. An increase in the past implies an increase in the future. The closer to 1, the stronger the trending behavior.
Key Concepts
| Concept | Description |
|---|---|
| Hurst Exponent ($H$) | A measure of long-term memory in a time series, indicating persistence or mean reversion. |
| Mean Reversion | A phenomenon where a variable tends to return to its long-term average level after deviations. Characterized by $H < 0.5$. |
| Random Walk | A series where future steps are independent of past steps, implying no memory. Characterized by $H = 0.5$. |
| Persistence/Trending | A phenomenon where a series tends to continue in the same direction. Past increases suggest future increases. Characterized by $H > 0.5$. |
| Rescaled Range (R/S) Analysis | A method used to estimate the Hurst exponent by analyzing the range of deviations from the mean, rescaled by the standard deviation, over various time intervals. |
Dependency Installation
# Install necessary libraries
!pip install nolds numpy pandas matplotlib seabornRequirement already satisfied: nolds in /usr/local/lib/python3.12/dist-packages (0.6.3) Requirement already satisfied: numpy in /usr/local/lib/python3.12/dist-packages (2.0.2) Requirement already satisfied: pandas in /usr/local/lib/python3.12/dist-packages (2.2.2) Requirement already satisfied: matplotlib in /usr/local/lib/python3.12/dist-packages (3.10.0) Requirement already satisfied: seaborn in /usr/local/lib/python3.12/dist-packages (0.13.2) Requirement already satisfied: future in /usr/local/lib/python3.12/dist-packages (from nolds) (1.0.0) Requirement already satisfied: setuptools in /usr/local/lib/python3.12/dist-packages (from nolds) (75.2.0) Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.9.0.post0) Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2) Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas) (2026.2) Requirement already satisfied: contourpy>=1.0.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.3.3) Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (0.12.1) Requirement already satisfied: fonttools>=4.22.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (4.63.0) Requirement already satisfied: kiwisolver>=1.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.5.0) Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (26.2) Requirement already satisfied: pillow>=8 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (11.3.0) Requirement already satisfied: pyparsing>=2.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (3.3.2) Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)
Library Imports
import logging
import time
import random
from collections import deque
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import nolds
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')import inspect
print(inspect.signature(nolds.hurst_rs))(data, nvals=None, fit='RANSAC', debug_plot=False, debug_data=False, plot_file=None, corrected=True, unbiased=True)
Core Functions
Function Name: create_state
This function initializes the state dictionary for the Hurst exponent analysis. It sets up initial parameters and data structures required for subsequent operations. The state management is done entirely through dictionaries to avoid classes, as specified in the format requirements.
Parameters:
initial_data(Optional[np.ndarray]): An optional initial time series data to include in the state. If None, an empty list is initialized.
Returns:
dict: An initialized state dictionary with keys for data, parameters, and logging.
def create_state(initial_data: np.ndarray = None) -> dict:
"""
Initializes the state dictionary for the Hurst exponent analysis.
Parameters
----------
initial_data : np.ndarray, optional
An optional initial time series data to include in the state.
If None, an empty list is initialized.
Returns
-------
dict
An initialized state dictionary with keys for data, parameters, and logging.
Examples
--------
>>> state = create_state()
>>> 'data' in state and 'parameters' in state
True
>>> sample_data = np.array([1, 2, 3, 4, 5])
>>> state_with_data = create_state(initial_data=sample_data)
>>> np.array_equal(state_with_data['data'], sample_data)
True
"""
logger = logging.getLogger(__name__)
state = {
'data': initial_data if initial_data is not None else np.array([]),
'parameters': {
'min_window_size': 10,
'max_window_size': None, # Will be set based on data length
'n_windows': 100, # Number of window sizes to test for R/S analysis
'log_data_transformed': False # Flag to indicate if data has been transformed (e.g., log-returns)
},
'results': {},
'history': deque(maxlen=100) # For tracking recent operations or data points
}
logger.info("State initialized.")
return stateFunction Name: generate_synthetic_series
This function generates synthetic time series data based on different statistical properties to demonstrate the Hurst exponent. It can create pure random walks (Brownian motion), mean-reverting series, or trending (persistent) series by controlling the correlation structure.
Parameters:
state(dict): The current state dictionary. (Not used for modification here, but included for consistent signature).series_type(str): The type of series to generate. Can be 'random_walk', 'mean_reverting', or 'trending'.length(int): The number of data points in the series.volatility(float): The standard deviation of the daily returns for the series.mean_reversion_strength(float, optional): For 'mean_reverting' series, controls how quickly the series reverts to its mean. Defaults to0.1.trend_strength(float, optional): For 'trending' series, controls the magnitude of the underlying trend. Defaults to0.001.seed(int, optional): Random seed for reproducibility. Defaults toNone.
Returns:
np.ndarray: A NumPy array representing the generated synthetic time series.
def generate_synthetic_series(state: dict, series_type: str, length: int, volatility: float,
mean_reversion_strength: float = 0.1, trend_strength: float = 0.001,
seed: int = None) -> np.ndarray:
"""
Generates synthetic time series data (prices) based on specified characteristics.
Parameters
----------
state : dict
The current state dictionary. (Not used for modification here, but included for consistent signature).
series_type : str
Type of series to generate: 'random_walk', 'mean_reverting', 'trending'.
length : int
The number of data points in the series.
volatility : float
The standard deviation of the daily returns for the series.
mean_reversion_strength : float, optional
For 'mean_reverting' series, controls how quickly the series reverts to its mean.
Defaults to 0.1.
trend_strength : float, optional
For 'trending' series, controls the magnitude of the underlying trend.
Defaults to 0.001.
seed : int, optional
Random seed for reproducibility. Defaults to None.
Returns
-------
np.ndarray
A NumPy array representing the generated synthetic time series.
Raises
------
ValueError
If an unknown series_type is provided.
Examples
--------
>>> state = create_state()
>>> rw = generate_synthetic_series(state, 'random_walk', 100, 0.01, seed=42)
>>> len(rw) == 100
True
>>> mr = generate_synthetic_series(state, 'mean_reverting', 100, 0.01, mean_reversion_strength=0.05, seed=42)
>>> len(mr) == 100
True
"""
logger = logging.getLogger(__name__)
if seed is not None:
np.random.seed(seed)
random.seed(seed)
prices = np.zeros(length)
prices[0] = 100.0 # Starting price
if series_type == 'random_walk':
returns = np.random.normal(0, volatility, length - 1)
prices[1:] = prices[0] * np.exp(np.cumsum(returns))
logger.info(f"Generated a random walk series of length {length}.")
elif series_type == 'mean_reverting':
# Ornstein-Uhlenbeck process approximation for mean reversion
mean_level = prices[0]
dt = 1.0 # Time step
for t in range(1, length):
# dx = alpha * (mu - x) * dt + sigma * dW
# Here, alpha is mean_reversion_strength, mu is mean_level, sigma is volatility
drift = mean_reversion_strength * (mean_level - prices[t-1]) * dt
shock = volatility * np.random.normal(0, 1) * np.sqrt(dt)
prices[t] = prices[t-1] + drift + shock
logger.info(f"Generated a mean-reverting series of length {length} with strength {mean_reversion_strength}.")
elif series_type == 'trending':
# Geometric Brownian Motion with a drift
returns = np.random.normal(0, volatility, length - 1) + trend_strength
prices[1:] = prices[0] * np.exp(np.cumsum(returns))
logger.info(f"Generated a trending series of length {length} with strength {trend_strength}.")
else:
logger.error(f"Unknown series type: {series_type}")
raise ValueError(f"Unknown series type: {series_type}")
return pricesFunction Name: calculate_hurst_exponent
This function calculates the Hurst exponent of a given time series using the Rescaled Range (R/S) analysis method, specifically leveraging the nolds library. The R/S analysis is a robust method for estimating the Hurst exponent and involves dividing the time series into sub-periods and calculating the rescaled range for each. This function can also optionally apply log transformation to the data before calculation, which is common for price series to analyze log-returns.
Parameters:
state(dict): The current state dictionary, which will be updated with the calculated Hurst exponent and potentially the log_data_transformed flag.time_series(np.ndarray): The input time series data (e.g., price series).fit_method(str, optional): The method used bynolds.hurst_exponentto fit the line for R/S analysis. Defaults to 'R/S'.min_window_size(int, optional): The minimum window size for R/S analysis. If not provided, it's taken fromstate['parameters']. Defaults to10.max_window_size(int, optional): The maximum window size for R/S analysis. If not provided, it's taken fromstate['parameters']or defaults to half the series length.n_windows(int, optional): The number of different window sizes to use for R/S analysis. If not provided, it's taken fromstate['parameters']. Defaults to100.use_log_returns(bool, optional): IfTrue, the function will calculate the Hurst exponent on the log-returns of the time series instead of the raw values. Defaults toTrue.
Returns:
dict: The updated state dictionary including the calculated Hurst exponent instate['results']['hurst_exponent'].
def calculate_hurst_exponent(state: dict, time_series: np.ndarray, fit_method: str = 'RANSAC',
min_window_size: int = None, max_window_size: int = None,
n_windows: int = None, use_log_returns: bool = True) -> dict:
"""
Calculates the Hurst exponent of a time series using R/S analysis.
Parameters
----------
state : dict
The current state dictionary, which will be updated with the calculated
Hurst exponent and potentially the log_data_transformed flag.
time_series : np.ndarray
The input time series data (e.g., price series).
fit_method : str, optional
The method used by `nolds.hurst_rs` to fit the line for R/S analysis.
Defaults to 'RANSAC' (can also be 'poly').
min_window_size : int, optional
The minimum window size for R/S analysis. If not provided, it's taken
from `state['parameters']`. Defaults to 10.
max_window_size : int, optional
The maximum window size for R/S analysis. If not provided, it's taken
from `state['parameters']` or defaults to half the series length.
n_windows : int, optional
The number of different window sizes to use for R/S analysis. If not provided,
it's taken from `state['parameters']`. Defaults to 100.
use_log_returns : bool, optional
If `True`, the function will calculate the Hurst exponent on the log-returns
of the time series instead of the raw values. Defaults to True.
Returns
-------
dict
The updated state dictionary including the calculated Hurst exponent in
`state['results']['hurst_exponent']`.
Examples
--------
>>> state = create_state()
>>> prices = np.array([100, 101, 100, 102, 103, 102.5, 104, 103.5, 105, 106])
>>> updated_state = calculate_hurst_exponent(state, prices)
>>> 'hurst_exponent' in updated_state['results']
True
>>> # Example with a known random walk (H=0.5)
>>> np.random.seed(42)
>>> rw_prices = np.cumsum(np.random.randn(1000)) + 100
>>> state_rw = create_state()
>>> updated_state_rw = calculate_hurst_exponent(state_rw, rw_prices)
>>> hurst_rw = updated_state_rw['results']['hurst_exponent']
>>> 0.4 < hurst_rw < 0.6 # Hurst for random walk should be around 0.5
True
"""
logger = logging.getLogger(__name__)
if len(time_series) < state['parameters']['min_window_size'] * 2:
logger.warning(f"Time series length ({len(time_series)}) is too short for Hurst exponent calculation. Min required: {state['parameters']['min_window_size'] * 2}.")
state['results']['hurst_exponent'] = np.nan
state['results']['hurst_calculation_error'] = "Series too short"
return state
data_for_hurst = time_series
if use_log_returns:
# Calculate log returns: log(P_t / P_{t-1})
# Handle potential zero or negative prices by adding a small epsilon or filtering
if np.any(time_series <= 0):
logger.warning("Time series contains non-positive values. Cannot calculate log returns. Using raw series.")
use_log_returns = False
else:
data_for_hurst = np.diff(np.log(time_series))
state['parameters']['log_data_transformed'] = True
logger.info("Calculating Hurst exponent on log returns.")
if len(data_for_hurst) == 0:
logger.warning("Data for Hurst calculation is empty after transformation. Cannot calculate.")
state['results']['hurst_exponent'] = np.nan
state['results']['hurst_calculation_error'] = "Empty data after transformation"
return state
# Determine window parameters, preferring function args, then state, then defaults
min_win = min_window_size if min_window_size is not None else state['parameters'].get('min_window_size', 10)
# Ensure max_win is an integer. Default to half the series length if not provided in args or state.
default_max_win = len(data_for_hurst) // 2
max_win = max_window_size if max_window_size is not None else state['parameters'].get('max_window_size', default_max_win)
# If max_win is still None (e.g., if state['parameters']['max_window_size'] was None), use default_max_win
if max_win is None:
max_win = default_max_win
# Clamp min_win and max_win to valid ranges
min_win = max(10, min_win)
max_win = min(max_win, len(data_for_hurst) // 2)
# Ensure max_win is greater than min_win for logspace to work and nolds requirements
if max_win <= min_win:
logger.warning(f"Adjusting max_window_size. Original: {max_win}, Adjusted: {min_win + 1}")
max_win = min_win + 1
if max_win > len(data_for_hurst):
max_win = len(data_for_hurst) # Ensure it doesn't exceed data length
n_win = n_windows if n_windows is not None else state['parameters'].get('n_windows', 20) # Use 20 as suggested for nvals
# Generate nvals for nolds.hurst_rs
nvals = np.unique(
np.logspace(
np.log10(min_win),
np.log10(max_win),
num=n_win
).astype(int)
)
# Exponential backoff for retries in case of nolds internal issues (e.g. singular matrix)
max_retries = 3
for attempt in range(max_retries):
try:
hurst_exponent = nolds.hurst_rs(
data_for_hurst,
nvals=nvals,
fit=fit_method,
debug_plot=False) # Keep debug_plot False for non-interactive environments
state['results']['hurst_exponent'] = hurst_exponent
logger.info(f"Successfully calculated Hurst exponent: {hurst_exponent:.4f} for series length {len(time_series)}.")
break # Exit loop on success
except Exception as e:
logger.warning(f"Attempt {attempt + 1} failed to calculate Hurst exponent: {e}")
if attempt < max_retries - 1:
sleep_time = 2 ** attempt + random.uniform(0, 1) # Exponential backoff with jitter
logger.info(f"Retrying in {sleep_time:.2f} seconds...")
time.sleep(sleep_time)
else:
logger.error(f"Failed to calculate Hurst exponent after {max_retries} attempts.")
state['results']['hurst_exponent'] = np.nan
state['results']['hurst_calculation_error'] = str(e)
return stateFunction Name: track_metrics
This function is designed to track and summarize key metrics from the analysis. It takes the current state and extracts relevant results, such as the Hurst exponent, to store them in a more organized format, typically a Pandas DataFrame, for easy viewing and comparison. This allows for a structured way to compare results across different synthetic series or parameters.
Parameters:
state(dict): The current state dictionary containing analysis results.metric_name(str): A descriptive name for the metric being tracked (e.g., 'hurst_exponent').value(Any): The value of the metric to track.series_label(str, optional): A label to identify the series from which the metric was derived (e.g., 'Random Walk', 'Mean Reverting'). Defaults to 'default'.
Returns:
dict: The updated state dictionary with the new metric appended tostate['results']['tracked_metrics'].
def track_metrics(state: dict, metric_name: str, value: any, series_label: str = 'default') -> dict:
"""
Tracks and stores key metrics in the state dictionary.
This function is designed to collect various metrics (e.g., Hurst exponent)
along with their associated labels (e.g., series type, parameters) into a
structured format within the state dictionary. It initializes a DataFrame
for tracking if it doesn't exist and appends new metric entries.
Parameters
----------
state : dict
The current state dictionary containing analysis results.
metric_name : str
A descriptive name for the metric being tracked (e.g., 'hurst_exponent').
value : Any
The value of the metric to track.
series_label : str, optional
A label to identify the series from which the metric was derived
(e.g., 'Random Walk', 'Mean Reverting'). Defaults to 'default'.
Returns
-------
dict
The updated state dictionary with the new metric appended to
`state['results']['tracked_metrics']`.
Examples
--------
>>> state = create_state()
>>> state = track_metrics(state, 'hurst_exponent', 0.52, 'Random Walk')
>>> 'tracked_metrics' in state['results']
True
>>> state['results']['tracked_metrics'].shape[0] == 1
True
>>> state = track_metrics(state, 'hurst_exponent', 0.25, 'Mean Reverting')
>>> state['results']['tracked_metrics'].shape[0] == 2
True
"""
logger = logging.getLogger(__name__)
if 'tracked_metrics' not in state['results'] or not isinstance(state['results']['tracked_metrics'], pd.DataFrame):
state['results']['tracked_metrics'] = pd.DataFrame(columns=['Series Type', 'Metric Name', 'Value'])
logger.debug("Initialized 'tracked_metrics' DataFrame.")
new_metric = pd.DataFrame([{'Series Type': series_label, 'Metric Name': metric_name, 'Value': value}])
state['results']['tracked_metrics'] = pd.concat([state['results']['tracked_metrics'], new_metric], ignore_index=True)
logger.info(f"Tracked metric: {metric_name} = {value} for series type: {series_label}.")
return stateFunction Name: calculate_rolling_hurst
This function calculates the Hurst exponent for a time series using a rolling window approach. It processes the series segment by segment, providing insight into how the Hurst exponent changes over time, which can reveal shifts in persistence or mean-reversion characteristics. It utilizes a deque to efficiently manage the rolling window.
Parameters:
state(dict): The current state dictionary. This will be updated with the rolling Hurst exponents.time_series(np.ndarray): The input time series data.window_size(int): The size of the rolling window.step(int, optional): The number of steps to advance the window after each calculation. Defaults to1.use_log_returns(bool, optional): IfTrue, Hurst exponent is calculated on log-returns. Defaults toTrue.
Returns:
dict: The updated state dictionary with a new entrystate['results']['rolling_hurst']containing a Pandas DataFrame of rolling Hurst exponents.
def calculate_rolling_hurst(state: dict, time_series: np.ndarray, window_size: int, step: int = 1,
use_log_returns: bool = True) -> dict:
"""
Calculates the Hurst exponent over a rolling window of the time series.
Parameters
----------
state : dict
The current state dictionary. This will be updated with the rolling Hurst exponents.
time_series : np.ndarray
The input time series data.
window_size : int
The size of the rolling window.
step : int, optional
The number of steps to advance the window after each calculation. Defaults to 1.
use_log_returns : bool, optional
If `True`, Hurst exponent is calculated on log-returns. Defaults to True.
Returns
-------
dict
The updated state dictionary with a new entry `state['results']['rolling_hurst']`
containing a Pandas DataFrame of rolling Hurst exponents.
Examples
--------
>>> state = create_state()
>>> np.random.seed(42)
>>> data = np.cumsum(np.random.randn(500)) + 100
>>> updated_state = calculate_rolling_hurst(state, data, window_size=100, step=10)
>>> 'rolling_hurst' in updated_state['results']
True
>>> isinstance(updated_state['results']['rolling_hurst'], pd.DataFrame)
True
>>> updated_state['results']['rolling_hurst'].shape[0] > 0
True
"""
logger = logging.getLogger(__name__)
if len(time_series) < window_size:
logger.warning(f"Time series length ({len(time_series)}) is less than window size ({window_size}). Cannot calculate rolling Hurst.")
state['results']['rolling_hurst'] = pd.DataFrame(columns=['Index', 'Hurst Exponent'])
return state
if window_size < state['parameters']['min_window_size'] * 2:
logger.warning(f"Window size ({window_size}) is too small for Hurst exponent calculation. Min required: {state['parameters']['min_window_size'] * 2}.")
state['results']['rolling_hurst'] = pd.DataFrame(columns=['Index', 'Hurst Exponent'])
return state
rolling_hurst_results = []
data_deque = deque(maxlen=window_size)
for i in range(len(time_series)):
data_deque.append(time_series[i])
if len(data_deque) == window_size and (i - window_size + 1) % step == 0:
current_window = np.array(data_deque)
temp_state = state.copy() # Use a temporary state for calculation to avoid modifying main state excessively
temp_state = calculate_hurst_exponent(temp_state, current_window, use_log_returns=use_log_returns)
hurst_val = temp_state['results'].get('hurst_exponent')
if not np.isnan(hurst_val):
rolling_hurst_results.append({'Index': i - window_size + 1, 'Hurst Exponent': hurst_val})
logger.debug(f"Calculated rolling Hurst at index {i - window_size + 1}: {hurst_val:.4f}")
else:
logger.warning(f"Could not calculate Hurst for window ending at index {i}. Error: {temp_state['results'].get('hurst_calculation_error', 'Unknown error')}")
state['results']['rolling_hurst'] = pd.DataFrame(rolling_hurst_results)
logger.info(f"Calculated rolling Hurst exponent for {len(rolling_hurst_results)} windows.")
return stateDemonstration/Visualization
This section demonstrates the usage of the core functions by generating synthetic data, calculating Hurst exponents, and visualizing the results. We will create different types of series (random walk, mean-reverting, trending) and compare their Hurst exponents. Additionally, we will show how the Hurst exponent can change over time using a rolling window analysis.
Demonstration: Generating and Visualizing Synthetic Series
# Initialize state
initial_state = create_state()
# --- Generate Synthetic Series ---
series_length = 1000
series_volatility = 0.01
seed = 42
# Random Walk (H ~ 0.5)
rw_series = generate_synthetic_series(initial_state, 'random_walk', series_length, series_volatility, seed=seed)
# Mean Reverting (H < 0.5)
mr_series = generate_synthetic_series(initial_state, 'mean_reverting', series_length, series_volatility, mean_reversion_strength=0.05, seed=seed)
# Trending (H > 0.5)
trend_series = generate_synthetic_series(initial_state, 'trending', series_length, series_volatility, trend_strength=0.0005, seed=seed)
# Store series in a dictionary for easy access and plotting
synthetic_series = {
'Random Walk': rw_series,
'Mean Reverting': mr_series,
'Trending': trend_series
}
# --- Visualize Synthetic Series ---
plt.figure(figsize=(15, 6))
sns.set_style('whitegrid')
for name, series_data in synthetic_series.items():
plt.plot(series_data, label=name, alpha=0.8)
plt.title('Synthetic Time Series: Random Walk, Mean-Reverting, and Trending')
plt.xlabel('Time Step')
plt.ylabel('Price')
plt.legend()
plt.grid(True)
plt.show()
logging.info("Generated and visualized synthetic series.")Demonstration: Calculating and Comparing Hurst Exponents
# Initialize a fresh state for Hurst calculations
hurst_state = create_state()
# Calculate Hurst for each synthetic series
for name, series_data in synthetic_series.items():
logging.info(f"Calculating Hurst for {name} series...")
# For Mean Reverting, consider using raw series as suggested, otherwise use log returns
if name == 'Mean Reverting':
hurst_state = calculate_hurst_exponent(hurst_state, series_data, use_log_returns=False)
else:
hurst_state = calculate_hurst_exponent(hurst_state, series_data, use_log_returns=True)
hurst_value = hurst_state['results'].get('hurst_exponent', np.nan)
hurst_state = track_metrics(hurst_state, 'hurst_exponent', hurst_value, name)
logging.info(f"Hurst Exponent for {name}: {hurst_value:.4f}")
# Display summary of Hurst exponents
print("\n--- Summary of Hurst Exponents ---")
display(hurst_state['results']['tracked_metrics'])
logging.info("Calculated and summarized Hurst exponents for synthetic series.")--- Summary of Hurst Exponents ---
/tmp/ipykernel_620/1479415453.py:48: FutureWarning: The behavior of DataFrame concatenation with empty or all-NA entries is deprecated. In a future version, this will no longer exclude empty or all-NA columns when determining the result dtypes. To retain the old behavior, exclude the relevant entries before the concat operation. state['results']['tracked_metrics'] = pd.concat([state['results']['tracked_metrics'], new_metric], ignore_index=True)
| Series Type | Metric Name | Value | |
|---|---|---|---|
| 0 | Random Walk | hurst_exponent | 0.499003 |
| 1 | Mean Reverting | hurst_exponent | 0.860277 |
| 2 | Trending | hurst_exponent | 0.499838 |
Demonstration: Rolling Hurst Exponent Analysis
# Choose one series for rolling analysis, e.g., the Mean Reverting series
series_to_analyze = synthetic_series['Mean Reverting']
series_label = 'Mean Reverting'
# Parameters for rolling Hurst calculation
rolling_window_size = 250
rolling_step = 50
# Initialize a fresh state for rolling Hurst calculation
rolling_hurst_state = create_state()
logging.info(f"Calculating rolling Hurst for {series_label} series with window size {rolling_window_size} and step {rolling_step}...")
# Calculate rolling Hurst on raw series for Mean Reverting as suggested
rolling_hurst_state = calculate_rolling_hurst(rolling_hurst_state, series_to_analyze, rolling_window_size, rolling_step, use_log_returns=False)
# Get rolling Hurst results
rolling_hurst_df = rolling_hurst_state['results']['rolling_hurst']
if not rolling_hurst_df.empty:
# --- Visualize Rolling Hurst Exponent ---
plt.figure(figsize=(15, 7))
sns.set_style('whitegrid')
# Subplot 1: Original Series
plt.subplot(2, 1, 1)
plt.plot(series_to_analyze, label=f'{series_label} Series', color='cornflowerblue')
plt.title(f'Original {series_label} Series')
plt.xlabel('Time Step')
plt.ylabel('Price')
plt.legend()
plt.grid(True)
# Subplot 2: Rolling Hurst Exponent
plt.subplot(2, 1, 2)
plt.plot(rolling_hurst_df['Index'], rolling_hurst_df['Hurst Exponent'], color='firebrick', marker='o', markersize=4, linestyle='-')
plt.axhline(0.5, color='gray', linestyle='--', label='H = 0.5 (Random Walk)')
plt.fill_between(rolling_hurst_df['Index'], 0, 0.5, color='green', alpha=0.1, label='Mean Reverting (H < 0.5)')
plt.fill_between(rolling_hurst_df['Index'], 0.5, 1, color='red', alpha=0.1, label='Trending (H > 0.5)')
plt.title(f'Rolling Hurst Exponent for {series_label} (Window Size: {rolling_window_size}, Step: {rolling_step})')
plt.xlabel('Starting Index of Window')
plt.ylabel('Hurst Exponent')
plt.ylim(0, 1)
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
logging.info("Visualized rolling Hurst exponent.")
else:
logging.warning(f"No rolling Hurst data to plot for {series_label}.")Production Considerations
When implementing Hurst exponent analysis in a production environment, several best practices should be considered to ensure robustness, efficiency, and maintainability. Key considerations include:
| Consideration | Best Practice |
|---|---|
| Data Validation | Implement checks for data quality, missing values, and stationarity before analysis. |
| Error Handling | Use try-except blocks and retry mechanisms (e.g., exponential backoff) for calculations that might fail. |
| Performance | Optimize calculations for large datasets using efficient libraries (NumPy) and vectorized operations. |
| Logging & Monitoring | Implement comprehensive logging for execution flow, errors, and calculated metrics. Monitor system health. |
| Configuration | Externalize parameters (window sizes, thresholds) for easy adjustment without code changes. |
| Modularity & Reusability | Design functions to be modular, testable, and reusable across different analyses. |
| Scalability | Consider distributed computing frameworks (e.g., Dask, Spark) for processing massive time series data. |
| Version Control | Maintain code in a version control system (e.g., Git) to track changes and facilitate collaboration. |
| Testing | Write unit and integration tests for all core functions to ensure correctness and reliability. |
| Security | Secure data access and API keys, especially when dealing with sensitive financial or proprietary data. |
| Documentation | Provide clear and concise documentation for code, functions, and the overall analysis pipeline. |
| Reproducibility | Ensure analyses are reproducible by setting random seeds and managing dependencies. |
Conclusion
This notebook provided a comprehensive exploration of the Hurst exponent and its application in statistical analysis for understanding the long-term memory of time series. We covered:
- Fundamental Concepts: Defined the Hurst exponent and its interpretations (mean-reverting, random walk, trending).
- Core Functions: Developed modular functions for state management, synthetic data generation, Hurst exponent calculation (using R/S analysis), rolling Hurst exponent, and metric tracking.
- Demonstration & Visualization: Illustrated the concepts with synthetic time series (random walk, mean-reverting, trending), calculated their respective Hurst exponents, and visualized the series along with their rolling Hurst exponents to show how persistence/mean-reversion can evolve over time.
- Production Considerations: Highlighted best practices for deploying such analyses in real-world scenarios, emphasizing data validation, error handling, performance optimization, and robust logging.
By following the structured approach outlined, this notebook serves as a robust framework for analyzing the Hurst exponent in various time series data, enabling deeper insights into their underlying statistical properties and potential applications in fields like finance and hydrology.