Crypto-Native·Spot Trading Mechanics·Intermediate

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.

cryptospot-tradingtrading-strategies

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:

  1. Fetches BTC dominance data (or generates synthetic)
  2. Computes trend signals from BTC.D using moving average crossovers
  3. Generates rotation signals: BTC season vs altcoin season
  4. Backtests the rotation strategy: hold BTC or an altcoin basket based on signal
  5. Analyzes performance across different BTC.D threshold levels
  6. Exports the strategy history

BTC Dominance Interpretation Guide

BTC.D LevelMarket PhaseStrategy
Rising rapidlyBTC season — Bitcoin outperformingHold BTC
High and plateauingPossible peak — watch for reversalNeutral
Falling rapidlyAltcoin season — alts outperformingRotate to alts
Low and stabilizingPossible bottom — rotate back to BTCPrepare BTC entry
Crossover (BTC.D MA20 < MA60)Confirmed altcoin season startFull alt allocation
[ ]
!pip install numpy pandas matplotlib seaborn --quiet
[ ]
import 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)
cell output

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