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
= yf.download('BTC-USD', start='2020-01-01', end='2024-01-01')
df = df['Adj Close']
price = price.pct_change()
returns
# Create forward return targets
= pd.DataFrame(index=price.index)
forward_returns for h in [7, 14, 30]:
f'fwd_{h}d'] = price.pct_change(h).shift(-h) forward_returns[
We construct continuous alpha factors from classic technical indicators:
= pd.DataFrame(index=price.index)
factors
# RSI as continuous factor (not binary threshold)
'rsi_14'] = talib.RSI(price.values, 14)
factors[
# MACD components
= talib.MACD(price.values)
macd, macd_signal, _ 'macd'] = macd
factors['macd_signal'] = macd_signal
factors[
# Bollinger Band position
= talib.BBANDS(price.values)
bb_upper, bb_middle, bb_lower 'bb_position'] = (price - bb_lower) / (bb_upper - bb_lower)
factors[
# Momentum and volatility
'momentum_20'] = talib.MOM(price.values, 20) / price
factors['atr_ratio'] = talib.ATR(df['High'], df['Low'], price, 14) / price factors[
We measure predictive power using Information Coefficient—the Spearman correlation between factor values today and future returns:
def calculate_ic(factor_series, forward_returns_series):
= pd.concat([factor_series, forward_returns_series], axis=1).dropna()
combined if len(combined) < 30:
return np.nan
= spearmanr(combined.iloc[:, 0], combined.iloc[:, 1])
ic, _ return ic
# Example: Raw RSI IC
= calculate_ic(factors['rsi_14'], forward_returns['fwd_30d'])
ic_rsi_raw 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
= 252 # 1-year rolling window
window
= pd.DataFrame(index=price.index)
enhanced_factors
for factor_name in factors.columns:
= factors[factor_name]
factor
# Rolling percentile rank (0-1)
= factor.rolling(window).rank(pct=True)
rank_factor f'{factor_name}_rank'] = rank_factor
enhanced_factors[
# Rolling z-score normalization
= factor.rolling(window).mean()
mean_rolling = factor.rolling(window).std()
std_rolling = (factor - mean_rolling) / std_rolling
zscore_factor f'{factor_name}_zscore'] = zscore_factor enhanced_factors[
Results: 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
= returns.rolling(20).std() * np.sqrt(365)
vol_20 = vol_20 / vol_20.rolling(60).mean()
vol_regime
for factor_name in factors.columns:
= factors[factor_name]
factor
# Scale by inverse volatility (stronger signals in low vol)
f'{factor_name}_vol_adj'] = factor / vol_regime
enhanced_factors[
# Regime-dependent scaling
= factor.copy()
regime_factor = vol_regime > 1.2
high_vol_mask = vol_regime < 0.8
low_vol_mask
*= 1.5 # Amplify in high vol
regime_factor[high_vol_mask] *= 0.8 # Dampen in low vol
regime_factor[low_vol_mask] f'{factor_name}_regime'] = regime_factor enhanced_factors[
Rationale: 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):
= series.dropna()
clean_series if len(clean_series) < 10:
return pd.Series(index=series.index, dtype=float)
= KalmanFilter(
kf =[1],
transition_matrices=[1],
observation_matrices=clean_series.iloc[0],
initial_state_mean=1,
initial_state_covariance=observation_cov,
observation_covariance=transition_cov
transition_covariance
)
= kf.filter(clean_series.values)
state_means, _ = pd.Series(index=series.index, dtype=float)
result = state_means.flatten()
result.loc[clean_series.index] return result
# Apply different Kalman configurations
for factor_name in factors.columns:
# Conservative smoothing
f'{factor_name}_kalman'] = kalman_denoise(
enhanced_factors[=0.05, observation_cov=1.0
factors[factor_name], transition_cov )
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
'rsi_multi_tf'] = (
enhanced_factors['rsi_7'] * 0.3 + factors['rsi_14'] * 0.7
factors[
)
# RSI-momentum divergence
'rsi_momentum_divergence'] = (
enhanced_factors['rsi_14'].pct_change(14) - factors['momentum_20']
factors[
)
# Volatility-momentum interaction
'vol_momentum_interaction'] = (
enhanced_factors['momentum_20'] * (1 / vol_20)
factors[
)
# MACD acceleration
'macd_acceleration'] = factors['macd'].diff(5) enhanced_factors[
# 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':
= calculate_ic(factors[factor], forward_returns[f'fwd_{horizon}d'])
ic else:
= calculate_ic(enhanced_factors[factor], forward_returns[f'fwd_{horizon}d'])
ic
if not np.isnan(ic):
abs(ic))
ics.append(
= np.mean(ics) if ics else 0
strategy_performance[strategy_name]
print("Strategy Performance (Mean |IC|):")
for strategy, performance in sorted(strategy_performance.items(),
=lambda x: x[1], reverse=True):
key= (performance / strategy_performance['Raw'] - 1) * 100
improvement 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.