Building on the foundation that technical indicators work better as continuous alpha factors than binary trading signals, let’s explore some enhancement strategies for Bitcoin return prediction. We test cross-sectional ranking, volatility regime adjustments, Kalman filtering, and factor combinations on a library of technical indicators.
We already established that technical indicators like RSI and MACD, when used as continuous alpha factors rather than binary trading signals, demonstrate significant predictive power for Bitcoin returns. However, raw indicator values suffer from regime changes, non-stationarity, and noise that can be addressed through quantitative enhancement techniques.
This study systematically evaluates five enhancement strategies commonly used in quantitative finance to determine which methods best improve Bitcoin alpha factor performance.
import pandas as pd
import yfinance as yf
import talib
from scipy.stats import spearmanr
# Load Bitcoin data
df = yf.download('BTC-USD', start='2020-01-01', end='2024-01-01')
price = df['Adj Close']
returns = price.pct_change()
# Create forward return targets
forward_returns = pd.DataFrame(index=price.index)
for h in [7, 14, 30]:
forward_returns[f'fwd_{h}d'] = price.pct_change(h).shift(-h)We construct continuous alpha factors from classic technical indicators:
factors = pd.DataFrame(index=price.index)
# RSI as continuous factor (not binary threshold)
factors['rsi_14'] = talib.RSI(price.values, 14)
# MACD components
macd, macd_signal, _ = talib.MACD(price.values)
factors['macd'] = macd
factors['macd_signal'] = macd_signal
# Bollinger Band position
bb_upper, bb_middle, bb_lower = talib.BBANDS(price.values)
factors['bb_position'] = (price - bb_lower) / (bb_upper - bb_lower)
# Momentum and volatility
factors['momentum_20'] = talib.MOM(price.values, 20) / price
factors['atr_ratio'] = talib.ATR(df['High'], df['Low'], price, 14) / priceWe measure predictive power using Information Coefficient—the Spearman correlation between factor values today and future returns:
def calculate_ic(factor_series, forward_returns_series):
combined = pd.concat([factor_series, forward_returns_series], axis=1).dropna()
if len(combined) < 30:
return np.nan
ic, _ = spearmanr(combined.iloc[:, 0], combined.iloc[:, 1])
return ic
# Example: Raw RSI IC
ic_rsi_raw = calculate_ic(factors['rsi_14'], forward_returns['fwd_30d'])
print(f"Raw RSI IC: {ic_rsi_raw:.3f}")Concept: Convert raw indicator values to rolling percentile ranks, removing level effects and creating stationary time series.
# Cross-sectional ranking enhancement
window = 252 # 1-year rolling window
enhanced_factors = pd.DataFrame(index=price.index)
for factor_name in factors.columns:
factor = factors[factor_name]
# Rolling percentile rank (0-1)
rank_factor = factor.rolling(window).rank(pct=True)
enhanced_factors[f'{factor_name}_rank'] = rank_factor
# Rolling z-score normalization
mean_rolling = factor.rolling(window).mean()
std_rolling = factor.rolling(window).std()
zscore_factor = (factor - mean_rolling) / std_rolling
enhanced_factors[f'{factor_name}_zscore'] = zscore_factorResults: This became our dominant strategy with 35% improvement in mean IC.
Concept: Scale factor strength based on Bitcoin’s volatility clustering patterns.
# Volatility regime enhancement
vol_20 = returns.rolling(20).std() * np.sqrt(365)
vol_regime = vol_20 / vol_20.rolling(60).mean()
for factor_name in factors.columns:
factor = factors[factor_name]
# Scale by inverse volatility (stronger signals in low vol)
enhanced_factors[f'{factor_name}_vol_adj'] = factor / vol_regime
# Regime-dependent scaling
regime_factor = factor.copy()
high_vol_mask = vol_regime > 1.2
low_vol_mask = vol_regime < 0.8
regime_factor[high_vol_mask] *= 1.5 # Amplify in high vol
regime_factor[low_vol_mask] *= 0.8 # Dampen in low vol
enhanced_factors[f'{factor_name}_regime'] = regime_factorRationale: Bitcoin’s extreme volatility clustering means factor signals should be interpreted differently in high vs low volatility periods.
Concept: Apply state-space modeling to denoise indicators while preserving signal.
from pykalman import KalmanFilter
def kalman_denoise(series, transition_cov=0.01, observation_cov=1.0):
clean_series = series.dropna()
if len(clean_series) < 10:
return pd.Series(index=series.index, dtype=float)
kf = KalmanFilter(
transition_matrices=[1],
observation_matrices=[1],
initial_state_mean=clean_series.iloc[0],
initial_state_covariance=1,
observation_covariance=observation_cov,
transition_covariance=transition_cov
)
state_means, _ = kf.filter(clean_series.values)
result = pd.Series(index=series.index, dtype=float)
result.loc[clean_series.index] = state_means.flatten()
return result
# Apply different Kalman configurations
for factor_name in factors.columns:
# Conservative smoothing
enhanced_factors[f'{factor_name}_kalman'] = kalman_denoise(
factors[factor_name], transition_cov=0.05, observation_cov=1.0
)Unexpected Result: Kalman filtering generally degraded performance, highlighting that Bitcoin’s “noise” may actually contain predictive information.
Concept: Create new factors by combining existing indicators in theoretically motivated ways.
# Multi-timeframe RSI
enhanced_factors['rsi_multi_tf'] = (
factors['rsi_7'] * 0.3 + factors['rsi_14'] * 0.7
)
# RSI-momentum divergence
enhanced_factors['rsi_momentum_divergence'] = (
factors['rsi_14'].pct_change(14) - factors['momentum_20']
)
# Volatility-momentum interaction
enhanced_factors['vol_momentum_interaction'] = (
factors['momentum_20'] * (1 / vol_20)
)
# MACD acceleration
enhanced_factors['macd_acceleration'] = factors['macd'].diff(5)# Evaluate all enhancement strategies
strategies = {
'Raw': factors.columns,
'Cross-Sectional': [c for c in enhanced_factors.columns if '_rank' in c or '_zscore' in c],
'Volatility-Regime': [c for c in enhanced_factors.columns if '_regime' in c or '_vol_adj' in c],
'Kalman': [c for c in enhanced_factors.columns if '_kalman' in c],
'Combinations': ['rsi_multi_tf', 'rsi_momentum_divergence', 'vol_momentum_interaction']
}
strategy_performance = {}
for strategy_name, factor_list in strategies.items():
ics = []
for factor in factor_list:
for horizon in [7, 14, 30]:
if strategy_name == 'Raw':
ic = calculate_ic(factors[factor], forward_returns[f'fwd_{horizon}d'])
else:
ic = calculate_ic(enhanced_factors[factor], forward_returns[f'fwd_{horizon}d'])
if not np.isnan(ic):
ics.append(abs(ic))
strategy_performance[strategy_name] = np.mean(ics) if ics else 0
print("Strategy Performance (Mean |IC|):")
for strategy, performance in sorted(strategy_performance.items(),
key=lambda x: x[1], reverse=True):
improvement = (performance / strategy_performance['Raw'] - 1) * 100
print(f"{strategy:>18}: {performance:.4f} ({improvement:+.1f}%)")| Factor | Strategy | IC (30d) | Improvement |
|---|---|---|---|
| macd_rank | Cross-Sectional | 0.271 | +93% |
| macd_signal_zscore | Cross-Sectional | 0.254 | +112% |
| atr_ratio_rank | Cross-Sectional | 0.162 | +62% |
| rsi_14_regime | Volatility-Regime | 0.205 | +86% |
This systematic evaluation of alpha factor enhancement strategies yields clear actionable insights for Bitcoin quantitative research:
The 35% improvement in predictive power from cross-sectional ranking represents a substantial advance in Bitcoin alpha factor research, transforming good factors into genuinely powerful predictive signals.
For practitioners, these results suggest that relative positioning and regime awareness matter more than absolute indicator values in cryptocurrency markets. The failure of Kalman filtering serves as a reminder that sophisticated techniques must be validated empirically rather than assumed to improve performance.