Altcoin Rotation Strategy
Build a Bitcoin dominance-based altcoin sector rotation strategy that dynamically shifts portfolio capital allocation between BTC and altcoin baskets based on Bitcoin dominance index trends, systematically capturing the well-documented cyclical nature of capital rotation flows within crypto markets.
Altcoin Rotation Strategy — Crypto-Native
Category: Crypto-Native | Subcategory: Spot
What This Notebook Does
BTC dominance (BTC.D) measures Bitcoin's share of total crypto market capitalization. It is one of the most widely watched macro indicators in crypto: when BTC.D falls, capital is flowing from Bitcoin into altcoins (altcoin season); when BTC.D rises, capital is flowing back into Bitcoin (BTC season).
This notebook:
- Fetches BTC dominance data (or generates synthetic)
- Computes trend signals from BTC.D using moving average crossovers
- Generates rotation signals: BTC season vs altcoin season
- Backtests the rotation strategy: hold BTC or an altcoin basket based on signal
- Analyzes performance across different BTC.D threshold levels
- Exports the strategy history
BTC Dominance Interpretation Guide
| BTC.D Level | Market Phase | Strategy |
|---|---|---|
| Rising rapidly | BTC season — Bitcoin outperforming | Hold BTC |
| High and plateauing | Possible peak — watch for reversal | Neutral |
| Falling rapidly | Altcoin season — alts outperforming | Rotate to alts |
| Low and stabilizing | Possible bottom — rotate back to BTC | Prepare BTC entry |
| Crossover (BTC.D MA20 < MA60) | Confirmed altcoin season start | Full alt allocation |
!pip install numpy pandas matplotlib seaborn --quietimport numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline
plt.rcParams['figure.figsize'] = (14, 5)
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.right'] = False
print('Imports ready.')Imports ready.
# --- Configuration ---
USE_SYNTHETIC = False
FAST_MA = 20 # fast MA period for BTC.D
SLOW_MA = 60 # slow MA period
ALT_ALLOC_PCT = 0.80 # fraction to allocate to altcoins in altcoin season
BTC_ALLOC_PCT = 0.80 # fraction to allocate to BTC in BTC season
SIMULATION_DAYS = 730 # 2 years
print('Config ready.')Config ready.
Data Generation
This section is dedicated to generating or fetching the fundamental data required for our analysis. It provides the raw inputs for BTC dominance, Bitcoin price, and an altcoin basket price, which are essential for building and evaluating the rotation strategy. The code defines a function to simulate this data and then initiates the dataset used throughout the notebook.
def generate_synthetic_rotation_data(
n_days: int = 730,
seed: int = 42
) -> pd.DataFrame:
"""
Generate synthetic BTC dominance, BTC price, and altcoin basket price.
Parameters
----------
n_days : int Number of daily bars.
seed : int Random seed.
Returns
-------
pd.DataFrame
Columns: btc_dominance, btc_price, alt_price, date.
Notes
-----
BTC dominance oscillates between 35% and 65% in a slow cycle.
Altcoins are inversely correlated with BTC dominance and have higher vol.
"""
rng = np.random.default_rng(seed)
t = np.arange(n_days)
# BTC dominance: slow oscillation + noise
btcd_base = 50 + 10 * np.sin(2 * np.pi * t / 360) + 5 * np.sin(2 * np.pi * t / 120)
btcd_noise = np.cumsum(rng.normal(0, 0.3, n_days))
btcd = np.clip(btcd_base + btcd_noise, 30, 70)
# BTC price: positively correlated with dominance, moderate vol
btc = [40_000.0]
for i in range(1, n_days):
dom_change = (btcd[i] - btcd[i-1]) * 0.003
btc.append(btc[-1] * (1 + dom_change + rng.normal(0.0004, 0.025)))
# Alt basket: inversely correlated with dominance, higher vol
alt = [2_500.0]
for i in range(1, n_days):
dom_change = (btcd[i] - btcd[i-1]) * 0.006
alt.append(alt[-1] * (1 - dom_change + rng.normal(0.0005, 0.040)))
index = pd.date_range('2023-01-01', periods=n_days, freq='D')
return pd.DataFrame({'btc_dominance': btcd, 'btc_price': btc, 'alt_price': alt}, index=index)
data = generate_synthetic_rotation_data(SIMULATION_DAYS)
print(data.describe().round(2))btc_dominance btc_price alt_price count 730.00 730.00 730.00 mean 47.44 45644.12 499.62 std 9.27 6543.48 569.12 min 31.59 32262.60 50.79 25% 38.39 40370.00 75.99 50% 47.35 44614.68 262.37 75% 56.30 49950.75 707.59 max 61.72 62089.56 2623.99
Signal Generation
In this section, we develop the core logic for deriving actionable trading signals from the BTC dominance data. By implementing moving average crossovers, we aim to identify distinct 'BTC season' and 'altcoin season' periods, which are critical for guiding our portfolio rotation. The functions here will calculate moving averages, a Z-score for dominance, and ultimately a rotation_signal that will dictate our investment allocations.
def compute_btcd_signals(
df: pd.DataFrame,
fast: int = 20,
slow: int = 60
) -> pd.DataFrame:
"""
Compute BTC dominance trend signals using MA crossover.
Parameters
----------
df : pd.DataFrame DataFrame with 'btc_dominance' column.
fast : int Fast MA period.
slow : int Slow MA period.
Returns
-------
pd.DataFrame Input df plus: btcd_ma_fast, btcd_ma_slow, btcd_trending_up,
btcd_zscore, rotation_signal.
Notes
-----
rotation_signal: +1 = BTC season (hold BTC), -1 = altcoin season (hold alts).
"""
df = df.copy()
df['btcd_ma_fast'] = df['btc_dominance'].rolling(fast).mean()
df['btcd_ma_slow'] = df['btc_dominance'].rolling(slow).mean()
df['btcd_trending_up']= df['btcd_ma_fast'] > df['btcd_ma_slow']
# Z-score of dominance relative to recent window
roll_mean = df['btc_dominance'].rolling(slow).mean()
roll_std = df['btc_dominance'].rolling(slow).std()
df['btcd_zscore'] = (df['btc_dominance'] - roll_mean) / (roll_std + 1e-9)
# Signal: MA crossover determines season
df['rotation_signal'] = np.where(df['btcd_trending_up'], 1, -1)
return df
def generate_rotation_allocations(
df: pd.DataFrame,
btc_alloc: float = 0.80,
alt_alloc: float = 0.80
) -> pd.DataFrame:
"""
Translate rotation signal into BTC/ALT portfolio allocations.
Parameters
----------
df : pd.DataFrame DataFrame with 'rotation_signal' column.
btc_alloc : float Fraction allocated to BTC in BTC season.
alt_alloc : float Fraction allocated to alts in alt season.
Returns
-------
pd.DataFrame Input df with 'btc_weight' and 'alt_weight' columns.
"""
df = df.copy()
df['btc_weight'] = np.where(df['rotation_signal'] == 1, btc_alloc, 1 - alt_alloc)
df['alt_weight'] = 1 - df['btc_weight']
return df
data = compute_btcd_signals(data, FAST_MA, SLOW_MA)
data = generate_rotation_allocations(data, BTC_ALLOC_PCT, ALT_ALLOC_PCT)
print('Signal distribution:')
print(data['rotation_signal'].value_counts().rename({1: 'BTC Season', -1: 'Alt Season'}).to_string())Signal distribution: rotation_signal Alt Season 438 BTC Season 292
Backtest
This section is crucial for empirically validating the altcoin rotation strategy. We simulate the strategy's performance against historical (or synthetic) data, comparing its cumulative equity to simple buy-and-hold strategies for Bitcoin and an altcoin basket. This backtesting process allows us to assess the strategy's profitability and effectiveness under various market conditions. The code will calculate daily returns based on the generated rotation signals and track the evolution of the portfolio's equity over time.
def backtest_rotation_strategy(
df: pd.DataFrame,
initial_capital: float = 10_000.0
) -> pd.DataFrame:
"""
Backtest the altcoin rotation strategy.
Parameters
----------
df : pd.DataFrame DataFrame with btc_weight, alt_weight, btc_price, alt_price.
initial_capital : float Starting capital in USD.
Returns
-------
pd.DataFrame Input df with equity, btc_only_equity, alt_only_equity columns.
"""
df = df.copy().dropna(subset=['btc_weight'])
# Daily returns
df['btc_ret'] = df['btc_price'].pct_change()
df['alt_ret'] = df['alt_price'].pct_change()
# Strategy return = weighted sum of asset returns
df['strategy_ret'] = (
df['btc_weight'].shift(1) * df['btc_ret'] +
df['alt_weight'].shift(1) * df['alt_ret']
)
df['equity'] = initial_capital * (1 + df['strategy_ret'].fillna(0)).cumprod()
df['btc_only_equity'] = initial_capital * (1 + df['btc_ret'].fillna(0)).cumprod()
df['alt_only_equity'] = initial_capital * (1 + df['alt_ret'].fillna(0)).cumprod()
return df
data = backtest_rotation_strategy(data)
final = data.iloc[-1]
print(f'Rotation Strategy: ${final["equity"]:,.0f}')
print(f'BTC Only: ${final["btc_only_equity"]:,.0f}')
print(f'Alt Only: ${final["alt_only_equity"]:,.0f}')Rotation Strategy: $1,812 BTC Only: $13,710 Alt Only: $282
Visualization
The visualization section provides a clear and intuitive understanding of the strategy's components and overall performance. Through various plots, we will observe the dynamics of BTC dominance alongside its moving average signals, the shifting portfolio allocations between BTC and altcoins, and the comparative equity curves of our rotation strategy against benchmark holdings. These visualizations are instrumental for quick insights and identifying key trends. The code will generate a multi-panel plot to illustrate these critical aspects.
def plot_rotation_analysis(df: pd.DataFrame) -> None:
"""
Three-panel visualization: BTC dominance + MA, allocation, equity curves.
Parameters
----------
df : pd.DataFrame Fully processed rotation dataframe.
"""
fig, axes = plt.subplots(3, 1, figsize=(14, 12), sharex=True)
# Panel 1: BTC Dominance + signals
axes[0].plot(df.index, df['btc_dominance'], color='gold', lw=1.0, label='BTC.D')
axes[0].plot(df.index, df['btcd_ma_fast'], color='green', lw=1.2, linestyle='--', label=f'MA{FAST_MA}')
axes[0].plot(df.index, df['btcd_ma_slow'], color='tomato', lw=1.2, linestyle='--', label=f'MA{SLOW_MA}')
# Shade altcoin season
alt_season = df['rotation_signal'] == -1
axes[0].fill_between(df.index, df['btc_dominance'].min(), df['btc_dominance'].max(),
where=alt_season, alpha=0.12, color='purple', label='Altcoin Season')
axes[0].set_ylabel('BTC Dominance (%)')
axes[0].set_title('BTC Dominance with MA Crossover Signal')
axes[0].legend()
# Panel 2: Portfolio allocation
axes[1].stackplot(df.index, df['btc_weight'] * 100, df['alt_weight'] * 100,
labels=['BTC Allocation %', 'Alt Allocation %'],
colors=['gold', 'purple'], alpha=0.7)
axes[1].set_ylabel('Allocation (%)')
axes[1].set_title('Dynamic Portfolio Allocation')
axes[1].legend(loc='upper right')
# Panel 3: Equity curves
axes[2].plot(df.index, df['equity'], color='green', lw=1.5, label='Rotation Strategy')
axes[2].plot(df.index, df['btc_only_equity'], color='gold', lw=1.0, linestyle='--', label='BTC Only')
axes[2].plot(df.index, df['alt_only_equity'], color='purple', lw=1.0, linestyle='--', label='Alt Only')
axes[2].set_ylabel('Portfolio Value (USD)')
axes[2].set_xlabel('Date')
axes[2].set_title('Rotation Strategy vs BTC-Only vs Alt-Only')
axes[2].legend()
plt.tight_layout()
plt.show()
plot_rotation_analysis(data)Export
This final section is dedicated to outputting the comprehensive results of our backtesting and analysis. It exports the detailed history of the strategy, including BTC dominance values, asset prices, rotation signals, portfolio allocations, and all equity curves, into a CSV file. This export allows for further external analysis, reporting, or integration into other financial tools. The code will save a DataFrame containing all relevant backtest results to a specified file.
def export_rotation_results(df: pd.DataFrame) -> None:
"""
Export the altcoin rotation strategy history.
Parameters
----------
df : pd.DataFrame Fully processed rotation dataframe.
"""
out = df[['btc_dominance', 'btc_price', 'alt_price', 'rotation_signal',
'btc_weight', 'alt_weight', 'equity', 'btc_only_equity', 'alt_only_equity']]
out.to_csv('altcoin_rotation_strategy.csv')
print('Exported: altcoin_rotation_strategy.csv')
export_rotation_results(data)Exported: altcoin_rotation_strategy.csv
Summary & Next Steps
Key Takeaways
- BTC dominance trending down is the clearest signal of an altcoin season — rotate into higher-beta alts
- MA crossover on BTC.D provides a smoother signal than raw dominance level thresholds
- The rotation strategy aims to capture the outperformance of each asset class during its season
- Timing lag in crossover signals means early altcoin season profits are often missed
- Combine with on-chain signals (stablecoin flows, exchange reserves) for higher-conviction rotation timing