Statistical Concepts for Trading¶
🎯 Learning Objectives
- Understand hypothesis testing for trading strategies
- Master regression analysis for financial data
- Learn time series statistical concepts
- Apply statistical methods to validate strategies
Statistics is fundamental to quantitative trading. This chapter covers essential statistical concepts you'll use to validate strategies, analyze relationships, and make data-driven trading decisions.
Hypothesis Testing¶
Why Hypothesis Testing?¶
Hypothesis testing helps you determine if your trading strategy's results are statistically significant or just due to chance.
T-Test for Returns¶
Test if mean return is significantly different from zero:
from scipy import stats
import numpy as np
import pandas as pd
import yfinance as yf
# Get returns data
data = yf.download("AAPL", period="1y")['Close']
returns = data.pct_change().dropna()
# Test if mean return is significantly different from zero
t_stat, p_value = stats.ttest_1samp(returns, 0)
print(f"T-statistic: {t_stat:.4f}")
print(f"P-value: {p_value:.4f}")
if p_value < 0.05:
print("Mean return is significantly different from zero (95% confidence)")
else:
print("Cannot reject null hypothesis - mean return may be zero")
Two-Sample T-Test¶
Compare returns of two strategies:
# Strategy A returns
strategy_a = np.array([0.01, 0.02, -0.01, 0.03, 0.01, 0.02])
# Strategy B returns
strategy_b = np.array([0.015, 0.025, -0.005, 0.035, 0.015, 0.025])
# Test if means are significantly different
t_stat, p_value = stats.ttest_ind(strategy_a, strategy_b)
print(f"T-statistic: {t_stat:.4f}, P-value: {p_value:.4f}")
if p_value < 0.05:
print("Strategies have significantly different returns")
Correlation Testing¶
Test if correlation between two stocks is significant:
from scipy.stats import pearsonr
# Get data for two stocks
stock_a = yf.download("AAPL", period="1y")['Close']
stock_b = yf.download("MSFT", period="1y")['Close']
# Calculate returns
returns_a = stock_a.pct_change().dropna()
returns_b = stock_b.pct_change().dropna()
# Test correlation
corr, p_value = pearsonr(returns_a, returns_b)
print(f"Correlation: {corr:.4f}")
print(f"P-value: {p_value:.4f}")
if p_value < 0.05:
print("Correlation is statistically significant")
Chi-Square Test¶
Test for independence (e.g., are winning trades independent?):
from scipy.stats import chi2_contingency
# Contingency table: Win/Loss by Day of Week
observed = np.array([[50, 30, 40, 45, 35], # Wins by day
[20, 30, 25, 20, 30]]) # Losses by day
chi2, p_value, dof, expected = chi2_contingency(observed)
print(f"Chi-square: {chi2:.4f}, P-value: {p_value:.4f}")
if p_value < 0.05:
print("Win/loss is dependent on day of week")
Regression Analysis¶
Simple Linear Regression¶
Model relationship between stock return and market return:
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score
import matplotlib.pyplot as plt
# Get market and stock data
market = yf.download("SPY", period="1y")['Close']
stock = yf.download("AAPL", period="1y")['Close']
# Calculate returns
market_returns = market.pct_change().dropna()
stock_returns = stock.pct_change().dropna()
# Align data
aligned_data = pd.DataFrame({
'Market': market_returns,
'Stock': stock_returns
}).dropna()
# Fit model
model = LinearRegression()
X = aligned_data[['Market']].values
y = aligned_data['Stock'].values
model.fit(X, y)
predictions = model.predict(X)
# Results
beta = model.coef_[0] # Market beta
alpha = model.intercept_ # Alpha (excess return)
r2 = r2_score(y, predictions)
print(f"Beta: {beta:.4f}")
print(f"Alpha: {alpha:.4f}")
print(f"R-squared: {r2:.4f}")
# Interpret
if beta > 1:
print("Stock is more volatile than market")
elif beta < 1:
print("Stock is less volatile than market")
Multiple Linear Regression¶
Use multiple factors to predict returns:
# Prepare factors
factors = pd.DataFrame({
'Market': market_returns,
'Momentum': stock_returns.rolling(10).sum(),
'Volatility': stock_returns.rolling(20).std()
}).dropna()
# Target: next period return
target = stock_returns.shift(-1).dropna()
# Align
aligned = pd.concat([factors, target], axis=1).dropna()
X = aligned[['Market', 'Momentum', 'Volatility']].values
y = aligned.iloc[:, -1].values
# Fit model
model = LinearRegression()
model.fit(X, y)
# Coefficients
print("Factor Loadings:")
for i, factor in enumerate(['Market', 'Momentum', 'Volatility']):
print(f"{factor}: {model.coef_[i]:.4f}")
print(f"Intercept (Alpha): {model.intercept_:.4f}")
Regression Diagnostics¶
Check regression assumptions:
from scipy import stats
# Residuals
residuals = y - model.predict(X)
# Normality test
shapiro_stat, shapiro_p = stats.shapiro(residuals)
print(f"Shapiro-Wilk test: p-value = {shapiro_p:.4f}")
# Autocorrelation of residuals
from statsmodels.tsa.stattools import acf
resid_autocorr = acf(residuals, nlags=5)
print(f"Residual autocorrelation (lag 1): {resid_autocorr[1]:.4f}")
# Heteroscedasticity test
from statsmodels.stats.diagnostic import het_breuschpagan
bp_stat, bp_p, _, _ = het_breuschpagan(residuals, X)
print(f"Breusch-Pagan test: p-value = {bp_p:.4f}")
Time Series Statistics¶
Stationarity¶
Many time series models require stationary data:
from statsmodels.tsa.stattools import adfuller
# Test for stationarity
adf_result = adfuller(returns)
print(f"ADF Statistic: {adf_result[0]:.4f}")
print(f"P-value: {adf_result[1]:.4f}")
print("Critical Values:")
for key, value in adf_result[4].items():
print(f"\t{key}: {value:.4f}")
if adf_result[1] < 0.05:
print("Series is stationary (reject null hypothesis)")
else:
print("Series is not stationary (cannot reject null hypothesis)")
# Make stationary by differencing
returns_diff = returns.diff().dropna()
adf_diff = adfuller(returns_diff)
if adf_diff[1] < 0.05:
print("First difference is stationary")
Autocorrelation¶
Detect patterns and dependencies in returns:
from statsmodels.tsa.stattools import acf, pacf
import matplotlib.pyplot as plt
# Calculate ACF and PACF
autocorr = acf(returns, nlags=20, fft=True)
partial_autocorr = pacf(returns, nlags=20)
# Plot
fig, axes = plt.subplots(2, 1, figsize=(12, 8))
axes[0].stem(range(len(autocorr)), autocorr)
axes[0].axhline(y=0, color='k', linestyle='-')
axes[0].axhline(y=1.96/np.sqrt(len(returns)), color='r', linestyle='--', label='95% confidence')
axes[0].axhline(y=-1.96/np.sqrt(len(returns)), color='r', linestyle='--')
axes[0].set_title('Autocorrelation Function (ACF)')
axes[0].set_xlabel('Lag')
axes[0].legend()
axes[1].stem(range(len(partial_autocorr)), partial_autocorr)
axes[1].axhline(y=0, color='k', linestyle='-')
axes[1].axhline(y=1.96/np.sqrt(len(returns)), color='r', linestyle='--')
axes[1].axhline(y=-1.96/np.sqrt(len(returns)), color='r', linestyle='--')
axes[1].set_title('Partial Autocorrelation Function (PACF)')
axes[1].set_xlabel('Lag')
plt.tight_layout()
plt.show()
# Test for significant autocorrelation
from statsmodels.stats.diagnostic import acorr_ljungbox
lb_stat, lb_pvalue = acorr_ljungbox(returns, lags=10, return_p=True)
print(f"Ljung-Box test p-value: {lb_pvalue[-1]:.4f}")
if lb_pvalue[-1] < 0.05:
print("Significant autocorrelation detected")
Cointegration¶
Test for long-term relationships (important for pairs trading):
from statsmodels.tsa.stattools import coint
# Get two correlated stocks
stock1 = yf.download("AAPL", period="2y")['Close']
stock2 = yf.download("MSFT", period="2y")['Close']
# Test for cointegration
score, pvalue, _ = coint(stock1, stock2)
print(f"Cointegration test statistic: {score:.4f}")
print(f"P-value: {pvalue:.4f}")
if pvalue < 0.05:
print("Stocks are cointegrated - good for pairs trading")
# Calculate spread
spread = stock1 - stock2
print(f"Spread mean: {spread.mean():.2f}")
print(f"Spread std: {spread.std():.2f}")
Distribution Analysis¶
Normality Tests¶
Test if returns follow normal distribution:
from scipy import stats
# Jarque-Bera test
jb_stat, jb_pvalue = stats.jarque_bera(returns)
print(f"Jarque-Bera statistic: {jb_stat:.4f}")
print(f"P-value: {jb_pvalue:.4f}")
if jb_pvalue < 0.05:
print("Returns are NOT normally distributed")
else:
print("Returns may be normally distributed")
# Kolmogorov-Smirnov test
ks_stat, ks_pvalue = stats.kstest(returns, 'norm',
args=(returns.mean(), returns.std()))
print(f"\nKS statistic: {ks_stat:.4f}")
print(f"P-value: {ks_pvalue:.4f}")
Q-Q Plot¶
Visual test for normality:
from scipy import stats
import matplotlib.pyplot as plt
# Q-Q plot
stats.probplot(returns, dist="norm", plot=plt)
plt.title('Q-Q Plot: Returns vs Normal Distribution')
plt.grid(True)
plt.show()
Statistical Significance in Trading¶
Multiple Testing Problem¶
When testing many strategies, adjust for multiple comparisons:
from statsmodels.stats.multitest import multipletests
# P-values from testing 10 strategies
p_values = [0.03, 0.05, 0.01, 0.08, 0.02, 0.04, 0.06, 0.09, 0.01, 0.07]
# Bonferroni correction
rejected, p_corrected, _, _ = multipletests(p_values, alpha=0.05, method='bonferroni')
print("Original p-values:", p_values)
print("Corrected p-values:", p_corrected)
print("Rejected hypotheses:", rejected)
Confidence Intervals¶
Calculate confidence intervals for strategy returns:
from scipy import stats
# 95% confidence interval for mean return
mean_return = returns.mean()
std_error = returns.std() / np.sqrt(len(returns))
confidence = 0.95
alpha = 1 - confidence
t_critical = stats.t.ppf(1 - alpha/2, len(returns) - 1)
margin_error = t_critical * std_error
ci_lower = mean_return - margin_error
ci_upper = mean_return + margin_error
print(f"Mean return: {mean_return:.4f}")
print(f"95% Confidence Interval: [{ci_lower:.4f}, {ci_upper:.4f}]")
Key Takeaways¶
- Hypothesis Testing: Validate if strategy results are statistically significant
- Regression: Model relationships between variables (e.g., stock vs market)
- Stationarity: Required for many time series models - test and transform if needed
- Autocorrelation: Detect patterns and dependencies in returns
- Cointegration: Important for pairs trading strategies
- Multiple Testing: Adjust p-values when testing many strategies
- Confidence Intervals: Quantify uncertainty in estimates
Previous: Technical Analysis | Next: Trading Strategies