DEX & Algo Trading

Crypto Momentum Strategy Backtest: From Signal Generation to Risk Control

ClawDUX TeamApril 5, 20268 min read5 views

Crypto Momentum Strategy Backtest: From Signal Generation to Risk Control

Momentum strategies exploit the tendency of winning assets to keep winning. In crypto, where trends can persist for months, momentum effects are particularly strong.

The Momentum Framework

python
import pandas as pd
import numpy as np
from typing import List, Dict

class MomentumStrategy:
    def __init__(
        self,
        lookback: int = 30,       # Signal lookback (days)
        holding: int = 7,          # Holding period (days)
        top_n: int = 3,            # Long top N assets
        stop_loss_pct: float = 0.10,  # 10% stop loss
        position_size_method: str = "equal",  # or "vol_parity"
    ):
        self.lookback = lookback
        self.holding = holding
        self.top_n = top_n
        self.stop_loss_pct = stop_loss_pct
        self.position_size_method = position_size_method

    def generate_signals(
        self, prices: pd.DataFrame
    ) -> pd.DataFrame:
        """
        Generate momentum signals for a universe of assets.
        prices: DataFrame with columns = asset names,
                index = dates, values = prices
        """
        # Calculate momentum score (return over lookback)
        momentum = prices.pct_change(self.lookback)

        # Rank assets by momentum each day
        ranks = momentum.rank(axis=1, ascending=False)

        # Signal: 1 for top_n, 0 otherwise
        signals = (ranks <= self.top_n).astype(int)

        return signals

    def apply_position_sizing(
        self, signals: pd.DataFrame, prices: pd.DataFrame
    ) -> pd.DataFrame:
        """Convert signals to position weights."""
        if self.position_size_method == "equal":
            # Equal weight across selected assets
            n_selected = signals.sum(axis=1).replace(0, 1)
            weights = signals.div(n_selected, axis=0)

        elif self.position_size_method == "vol_parity":
            # Inverse volatility weighting
            vol = prices.pct_change().rolling(20).std()
            inv_vol = (1 / vol).replace(
                [np.inf, -np.inf], 0
            ).fillna(0)
            weighted = signals * inv_vol
            total = weighted.sum(axis=1).replace(0, 1)
            weights = weighted.div(total, axis=0)

        return weights

    def backtest(
        self, prices: pd.DataFrame
    ) -> Dict:
        """Run full backtest with risk management."""
        signals = self.generate_signals(prices)
        weights = self.apply_position_sizing(signals, prices)

        # Calculate returns
        asset_returns = prices.pct_change()
        strategy_returns = (weights.shift(1) * asset_returns).sum(
            axis=1
        )

        # Apply stop-loss
        cumulative = (1 + strategy_returns).cumprod()
        peak = cumulative.cummax()
        drawdown = (cumulative - peak) / peak

        # When drawdown exceeds stop_loss, go flat
        stopped = drawdown < -self.stop_loss_pct
        strategy_returns[stopped] = 0

        # Rebalance signal (every holding period)
        rebal_mask = np.arange(len(strategy_returns)) % self.holding == 0
        # (Simplified: in production, hold positions for full period)

        # Performance metrics
        total_return = (
            (1 + strategy_returns).prod() - 1
        ) * 100
        annual_return = (
            (1 + strategy_returns).prod()
            ** (365 / len(strategy_returns)) - 1
        ) * 100
        volatility = strategy_returns.std() * np.sqrt(365) * 100
        sharpe = (
            annual_return / volatility
        ) if volatility > 0 else 0
        max_dd = drawdown.min() * 100
        win_rate = (
            (strategy_returns > 0).sum()
            / (strategy_returns != 0).sum()
        ) * 100

        return {
            'total_return_pct': round(total_return, 2),
            'annual_return_pct': round(annual_return, 2),
            'volatility_pct': round(volatility, 2),
            'sharpe_ratio': round(sharpe, 2),
            'max_drawdown_pct': round(max_dd, 2),
            'win_rate_pct': round(win_rate, 1),
            'total_trades': int(
                signals.diff().abs().sum().sum() / 2
            ),
            'stop_loss_triggers': int(stopped.sum()),
        }

# Usage
# prices = pd.DataFrame with BTC, ETH, SOL, etc. as columns
# strategy = MomentumStrategy(
#     lookback=14, holding=7, top_n=3,
#     stop_loss_pct=0.15, position_size_method="vol_parity"
# )
# results = strategy.backtest(prices)

Walk-Forward Optimization

python
def walk_forward_test(
    prices: pd.DataFrame,
    train_window: int = 180,
    test_window: int = 30,
) -> List[Dict]:
    """Walk-forward optimization to avoid overfitting."""
    results = []
    total_days = len(prices)

    for start in range(0, total_days - train_window - test_window,
                       test_window):
        train = prices.iloc[start:start + train_window]
        test = prices.iloc[
            start + train_window:
            start + train_window + test_window
        ]

        # Optimize on training data
        best_sharpe = -np.inf
        best_params = {}
        for lookback in [7, 14, 30, 60]:
            for top_n in [1, 3, 5]:
                strat = MomentumStrategy(
                    lookback=lookback, top_n=top_n
                )
                train_result = strat.backtest(train)
                if train_result['sharpe_ratio'] > best_sharpe:
                    best_sharpe = train_result['sharpe_ratio']
                    best_params = {
                        'lookback': lookback, 'top_n': top_n
                    }

        # Test with best params
        test_strat = MomentumStrategy(**best_params)
        test_result = test_strat.backtest(test)
        test_result['period'] = f"{test.index[0]} to {test.index[-1]}"
        test_result['params'] = best_params
        results.append(test_result)

    return results

This momentum framework is the type of strategy that trades well on ClawDUX — the platform's AI verification engine can independently reproduce backtest results to validate the Sharpe ratio and drawdown claims before listing.

The core logic discussed in this article has been integrated into the ClawDUX API. Access ClawDUX-core for full permissions, or browse the marketplace to discover verified trading strategies.

#momentum#backtesting#crypto#risk-management#strategy

Related Articles