Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 5

Stationarity Testing for Crypto Returns

Recommended resources

Hello! Welcome to your fifth lesson.

In our last two lessons, we established a data pipeline: we fetched historical OHLCV data and then designed robust storage solutions using either partitioned Parquet files or a TimescaleDB database. We now have the raw material for our trading strategies.

Before we can build models or signals, we must understand the fundamental statistical nature of this data. This brings us to today's topic, which addresses the learning outcome: Analyze stationarity of crypto asset returns using statistical tests (e.g., ADF, KPSS) and interpret the results for strategy selection.

This concept is a cornerstone of quantitative finance. For a momentum strategy, we are fundamentally betting that a trend will continue. This implies that the price series is not stationary. Conversely, many statistical models and mean-reversion strategies rely on the assumption that a series is stationary. Understanding how to test for this property is therefore not just a theoretical exercise—it directly informs which types of strategies are viable.

Given your background in theoretical physics and data engineering, we will explore this from a rigorous, first-principles perspective, connecting the mathematical definitions to their practical implications for algorithmic trading.

1. What is Stationarity?

At its core, a time series is stationary if its statistical properties do not change over time. This is a crucial assumption for many time-series forecasting models, as they assume that the statistical patterns observed in the past will hold in the future.

Let's start with a formal definition and some visual intuition.

Integration, Cointegration, and Stationarity

This video from Quantopian provides a clear and concise introduction to the concept of wide-sense stationarity and its importance in time-series analysis.

Please watch the first five and a half minutes of the video (00:34 - 05:31). Focus on: The definition of wide-sense stationarity: constant mean and constant variance. The reason it's important: statistical models are fitted to a probability distribution, and we need that distribution's parameters to be stable over time. The visual difference between a stationary series (white noise) and a non-stationary series (a process with a trend).

To be more precise, a time series is weakly stationary (or wide-sense stationary) if it satisfies three conditions:

  1. Constant Mean: The expected value is constant for all .
  2. Constant Variance: The variance is finite and constant for all .
  3. Constant Autocovariance: The covariance between any two points in the series depends only on the time difference (the lag ) between them, not on their absolute position in time.

As the video highlights, descriptive statistics like the overall mean are meaningless for a non-stationary series because they don't represent a stable underlying property. This is why we can't apply simple statistical models directly to asset prices, which typically exhibit strong trends.


2. From Non-Stationary Prices to Stationary Returns

Asset prices are classic examples of non-stationary time series. They tend to drift over time, a behavior often modeled as a random walk. A simple random walk can be expressed as:

where is a white noise term. This process has a "unit root" and is non-stationary; its variance grows with time.

To build valid statistical models, we need to transform this non-stationary price series into a stationary one. The most common method is differencing.

Integration, Cointegration, and Stationarity

Let's return to the Quantopian video, which explains the concepts of integration and differencing to achieve stationarity.

Watch the segment from 07:59 to 11:47. It covers: Orders of Integration (I(n)): A series that becomes stationary after being differenced n times is said to be integrated of order n, or I(n). Price series are often I(1). First-Order Differencing: How taking the difference between consecutive data points can transform an I(1) series into a stationary I(0) series. Application to Stock Data: The video demonstrates that while stock prices are clearly non-stationary, their returns (both arithmetic and logarithmic) often appear stationary.

As we discussed in a previous lesson, calculating logarithmic returns is a standard practice:

This is the first difference of the log-price series. If log-prices follow a random walk (are I(1)), their first difference—the log returns—should be stationary (I(0)). This is a foundational assumption in financial econometrics.

Our goal now is to move beyond visual inspection and formally test this hypothesis on our crypto data.


3. Statistical Tests for Stationarity

Visual inspection can be misleading. We need objective statistical tests to determine if a series is stationary. We will focus on two of the most widely used tests: the Augmented Dickey-Fuller (ADF) test and the Kwiatkowski-Phillips-Schmidt-Shin (KPSS) test. They are powerful when used in combination because they have opposing null hypotheses.

Understanding Stationary Time Series Analysis

This article from Analytics Vidhya provides an excellent overview of both the ADF and KPSS tests, including their hypotheses, interpretation, and Python implementation.

Please read the sections covering the definitions and mechanics of the ADF and KPSS tests. Specifically focus on: 'What Is a Stationary Time Series?' and 'How to Check Stationarity?' to reinforce the concepts. 'Augmented Dickey-Fuller Test': Understand that its null hypothesis (H0) is that the series is non-stationary (has a unit root). 'How Does the Kwiatkowski-Phillips-Schmidt-Shin (KPSS) Test Work?': Understand that its null hypothesis (H0) is that the series is stationary (or trend-stationary). Pay close attention to the hypotheses—they are the key to interpreting the results.

Here's a summary of the tests:

Test Null Hypothesis () Alternative Hypothesis () Interpretation of p-value < 0.05
ADF The series has a unit root (is non-stationary). The series is stationary or trend-stationary. Reject . The series is likely stationary.
KPSS The series is stationary around a mean or trend. The series has a unit root (is non-stationary). Reject . The series is likely non-stationary.

Notice the reversal. A low p-value in an ADF test suggests stationarity, while a low p-value in a KPSS test suggests non-stationarity. This is why using them together is so effective.


4. Interpreting Results for Strategy Selection

By running both tests, we can diagnose the nature of a time series with more confidence. This directly impacts our strategy choice. For instance, a momentum strategy is predicated on the existence of a trend, which is a feature of a non-stationary series.

Understanding Stationary Time Series Analysis

Let's revisit the Analytics Vidhya article for a crucial section that explains how to combine the results of the ADF and KPSS tests to reach a conclusion.

Read the section 'When to Choose ADF or KPSS Test?'. It outlines the four possible outcomes when you apply both tests.

Here is the interpretation matrix based on the article:

  1. ADF: Stationary & KPSS: Stationary

    • Conclusion: The series is stationary.
    • Strategy Implication: The series is mean-reverting. This is a good candidate for strategies that trade reversals (e.g., buy dips, sell rips). We don't expect to find this in raw price data but might find it in the returns of a stable asset or the spread between two cointegrated assets.
  2. ADF: Non-stationary & KPSS: Non-stationary

    • Conclusion: The series is non-stationary.
    • Strategy Implication: The series is not mean-reverting and likely contains a stochastic trend (like a random walk). This is the behavior we expect from asset prices and is the domain of momentum and trend-following strategies.
  3. ADF: Non-stationary & KPSS: Stationary

    • Conclusion: The series is trend-stationary. It has a deterministic trend. If you were to subtract this trend, the remaining series would be stationary.
    • Strategy Implication: This is also a candidate for trend-following, but the nature of the trend is more predictable than a purely stochastic one.
  4. ADF: Stationary & KPSS: Non-stationary

    • Conclusion: The series is difference-stationary. It is I(1).
    • Strategy Implication: This confirms the random walk-like behavior. Differencing the series once will make it stationary. This is the classic signature of asset prices, where the price itself is unpredictable, but the returns are stationary and more amenable to modeling.

5. Practical Implementation with Python

Let's apply these tests to real crypto data. We will test both the closing prices and the log returns of a crypto asset. We expect to see prices being non-stationary (Case 2 or 4) and returns being stationary (Case 1).

The statsmodels library in Python provides implementations for both tests.

import pandas as pd
import numpy as np
from statsmodels.tsa.stattools import adfuller, kpss

def analyze_stationarity(timeseries, series_name=""):
    """
    Performs and interprets ADF and KPSS tests for stationarity.

    Args:
        timeseries (pd.Series): The time series data to analyze.
        series_name (str): A name for the series for printing.
    """
    print(f"--- Stationarity Analysis for {series_name} ---")
    
    # --- ADF Test ---
    # H0: Series is non-stationary
    adf_result = adfuller(timeseries.dropna())
    adf_p_value = adf_result[1]
    
    print(f'ADF Statistic: {adf_result[0]:.4f}')
    print(f'ADF p-value: {adf_p_value:.4f}')
    
    if adf_p_value <= 0.05:
        print("ADF Conclusion: Reject H0. The series is likely stationary.")
        adf_stationary = True
    else:
        print("ADF Conclusion: Fail to reject H0. The series is likely non-stationary.")
        adf_stationary = False

    print("-" * 20)

    # --- KPSS Test ---
    # H0: Series is stationary
    # Using 'c' for constant-only trend. Use 'ct' for constant and trend.
    kpss_result = kpss(timeseries.dropna(), regression='c', nlags='auto')
    kpss_p_value = kpss_result[1]

    print(f'KPSS Statistic: {kpss_result[0]:.4f}')
    print(f'KPSS p-value: {kpss_p_value:.4f}')
    # Note: The p-value can be 0.10 (upper bound) or 0.01 (lower bound).
    # We check against our significance level.
    
    if kpss_p_value <= 0.05:
        print("KPSS Conclusion: Reject H0. The series is likely non-stationary.")
        kpss_stationary = False
    else:
        print("KPSS Conclusion: Fail to reject H0. The series is likely stationary.")
        kpss_stationary = True
        
    print("-" * 20)

    # --- Combined Conclusion ---
    if adf_stationary and kpss_stationary:
        print("Combined Conclusion: The series is stationary (Case 1).")
        print("Implication: Suitable for mean-reversion strategies.")
    elif not adf_stationary and not kpss_stationary:
        print("Combined Conclusion: The series is non-stationary (Case 2).")
        print("Implication: Suitable for trend-following/momentum strategies.")
    elif not adf_stationary and kpss_stationary:
        print("Combined Conclusion: The series is trend-stationary (Case 3).")
        print("Implication: A deterministic trend exists. Suitable for trend-following.")
    elif adf_stationary and not kpss_stationary:
        print("Combined Conclusion: The series is difference-stationary (Case 4).")
        print("Implication: Requires differencing to become stationary. Classic price behavior.")
    
    print("-" * 40 + "\n")


# --- Example Usage ---
# Assume you have a DataFrame 'df' with OHLCV data and a datetime index
# For demonstration, let's create some sample data
np.random.seed(42)
price_drift = 0.0005
price_vol = 0.02
prices = 100 * np.exp(np.cumsum(np.random.normal(price_drift, price_vol, 1000)))
dates = pd.to_datetime(pd.date_range(start='2021-01-01', periods=1000))
df = pd.DataFrame({'close': prices}, index=dates)

# Calculate log returns
df['log_return'] = np.log(df['close']).diff()

# Analyze the closing price series
analyze_stationarity(df['close'], "BTC-USD Closing Price (Simulated)")

# Analyze the log return series
analyze_stationarity(df['log_return'], "BTC-USD Log Returns (Simulated)")

When you run this code, you should see that the price series is identified as non-stationary, justifying the use of momentum strategies. The log return series, in contrast, will be identified as stationary, confirming it's a valid input for statistical models that require this property.



Conclusion

Key Takeaways:

  • Stationarity implies that a time series has a constant mean, variance, and autocovariance structure.
  • Asset prices are typically non-stationary (often I(1) or "difference-stationary"), exhibiting trends that make them unsuitable for many standard statistical models.
  • Asset returns (especially log returns) are often stationary (I(0)), making them the primary object of study for quantitative modeling.
  • The Augmented Dickey-Fuller (ADF) test has a null hypothesis of non-stationarity.
  • The Kwiatkowski-Phillips-Schmidt-Shin (KPSS) test has a null hypothesis of stationarity.
  • By combining the results of both tests, we can confidently diagnose a series and select an appropriate strategy class: momentum for non-stationary series and mean-reversion for stationary series.

You now have the theoretical understanding and practical tools to assess the most fundamental property of your trading data. This is a critical step before we move on to feature engineering and signal generation.

Preview of the Next Lesson:

Our analysis so far has assumed clean, continuous data. However, real-world crypto data is messy. It contains gaps from exchange downtime, sudden anomalous spikes from flash crashes, and periods of zero volume. Before we can reliably calculate signals, we must address these issues. In the next lesson, we will focus on the learning outcome: "Implement robust methods to detect and handle crypto-specific data anomalies, such as exchange downtime, flash crashes, and missing values."