← Back to Home
A Multi-Factor ADX–RSI–VWAP Trend Strategy with ATR-Based Position Sizing and Risk Parity for Crypto Portfolios

A Multi-Factor ADX–RSI–VWAP Trend Strategy with ATR-Based Position Sizing and Risk Parity for Crypto Portfolios

In the volatile world of cryptocurrency, “HODLing” is often not enough. To survive bear markets and capitalize on bull runs without emotional bias, we need a systematic approach.

This article walks through the construction of a robust Python backtesting script using Backtrader. We will build a strategy that combines Multi-Factor Trend Following (to know what to buy) with Risk Parity & Volatility Sizing (to know how much to buy).

The Architecture

The system is built on three pillars:

  1. Data Ingestion: Fetching clean history from Binance via CCXT.

  2. The Alpha Engine: A composite scoring system using ADX, RSI, and VWAP.

  3. The Risk Engine: Position sizing based on Volatility (ATR) and Risk Parity.


Part 1: The Data Layer

Before we trade, we need data. We use CCXT to fetch data because it handles the API complexity of hundreds of exchanges.1

The Problem

Exchanges often limit how many data points (candles) you can get in one request (e.g., Binance limits to 1000 candles).

The Solution: Pagination

Our BinanceDataFetcher class loops through time to stitch together a complete dataset.

Python

def fetch_ohlcv(self, symbol: str, timeframe: str = '1d', days_back: int = 365):
    # Calculate start time
    start_time = datetime.now() - timedelta(days=days_back)
    since = self.exchange.parse8601(start_time.isoformat())
    
    all_ohlcv = []
    
    # Pagination Loop
    while True:
        ohlcv = self.exchange.fetch_ohlcv(
            symbol, timeframe=timeframe, since=since, limit=1000
        )
        if not ohlcv: break
            
        all_ohlcv.extend(ohlcv)
        # Move the 'since' cursor to the next millisecond
        since = ohlcv[-1][0] + 1
        
        if len(ohlcv) < 1000: break # We reached the end

Key Takeaway: Always handle pagination when fetching historical data, or your backtest will be missing recent price action.


Part 2: The Alpha Engine (Trading Strategy)

Instead of relying on a single indicator (which generates many false signals), we use a Weighted Composite Score. We assign points to different market factors.

The Factors

  1. Trend Strength (ADX): Is the market actually moving?

  2. Direction (+DI/-DI): Is it moving up or down?

  3. Fair Value (VWAP): Are we paying a premium or discount relative to volume?

  4. Momentum (RSI): Is the trend healthy or overextended?

The Code

Here is how we calculate the score for every asset, every day:

Python

def calculate_position_score(self, symbol: str) -> float:
    ind = self.indicators[symbol]
    score = 0
    
    # 1. Trend Strength (40% Weight)
    # ADX measures intensity. We scale the score by how strong the ADX is.
    if ind['adx'][0] > self.p.adx_threshold:
        score += 0.4 * (ind['adx'][0] / 100)
    
    # 2. Direction (30% Weight)
    # Using Directional Indicators to determine Bull vs Bear
    if ind['plus_di'][0] > ind['minus_di'][0]:
        score += 0.3  # Bullish
    else:
        score -= 0.3  # Bearish
        
    # 3. Price vs VWAP (20% Weight)
    # Buying above VWAP signifies strong demand
    if self.symbol_data[symbol].close[0] > ind['vwap'][0]:
        score += 0.2
    else:
        score -= 0.2
        
    return score

The Logic:


Part 3: Position Sizing (The Risk Engine)

This is where amateurs lose money and pros manage risk. We don’t just put “10% in everything.” We use a dual-constraint method.

Image of risk parity portfolio allocation chart

Getty Images

Constraint A: Risk Parity

We want to divide our exposure equally among the assets we are currently trading.

Constraint B: Volatility Sizing (ATR)

We calculate the Average True Range (ATR). We limit our position size so that if the price moves 2 ATRs against us, we only lose 2% of our total capital.

The Code

Python

def calculate_position_size(self, symbol: str, score: float) -> float:
    capital = self.broker.getvalue()
    atr = self.indicators[symbol]['atr'][0]
    
    # 1. Calculate Risk Parity Size
    active_count = max(len(self.active_positions), 1)
    weight = min(0.5 / active_count, 0.2) # Cap at 20% per asset
    size_parity = (capital * weight) / data.close[0]
    
    # 2. Calculate Volatility Risk Size
    # How much can we buy so that 2*ATR loss = 2% of Account?
    risk_dollars = capital * self.p.risk_per_trade
    size_volatility = risk_dollars / (atr * 2)
    
    # TAKE THE SMALLER OF THE TWO (Conservative Approach)
    return min(size_parity, size_volatility)

Part 4: Execution & Rebalancing

The strategy runs inside the next() method, which Backtrader executes for every new candle.2

1. Stop Loss & Take Profit

We implement “Hard Stops.” If the price moves 10% down, we cut it. If it moves 30% up, we take the profit. This removes the psychological difficulty of selling.

Python

# Inside next() loop
entry = self.entry_prices.get(symbol)
if entry:
    # Stop Loss Logic
    if data.close[0] < entry * (1 - self.p.stop_loss_pct):
        self.close(data=data)
    # Take Profit Logic
    elif data.close[0] > entry * (1 + self.p.take_profit_pct):
        self.close(data=data)

2. Monthly Rebalancing

Crypto markets change fast. Every 30 days, we perform a “Health Check.”

  1. Recalculate scores for all assets.

  2. Sell anything with a negative score.

  3. Buy the top assets with positive scores.


Part 5: Analysis & Metrics

To fix the dependency issues with the library empyrical, we wrote a custom RiskMetrics class. This allows us to calculate institutional-grade metrics without version conflicts.

The Sharpe Ratio Calculation:

The Sharpe ratio tells us if the returns were worth the stress (volatility).

Python

@staticmethod
def sharpe_ratio(returns, risk_free=0.0, periods=365):
    # Calculate excess return over the risk-free rate
    excess_returns = returns - (risk_free / periods)
    std = excess_returns.std()
    
    if std == 0: return 0.0
    
    # Annualize the result
    return np.sqrt(periods) * excess_returns.mean() / std

Performance Analysis

The backtest covered the period of 2025, a year characterized by significant volatility in the crypto markets. The strategy demonstrated its core value proposition: capital preservation during bear trends.

1. Key Metrics Overview

Metric Result Interpretation
Total Return +4.74% Positive profitability in a year where the benchmark (Grey Line) ended negative.
Max Drawdown -19.71% The system hit its risk limits late in the year. This falls just within the 20% max portfolio risk parameter we set.
Sharpe Ratio 0.296 A lower score indicating that for every unit of risk taken, the return was modest. This is typical for trend-following strategies during choppy/sideways years.
Final Value $104,744 The portfolio grew capital while the buy-and-hold benchmark lost value (ending below $90k).

2. Visual Analysis (The Charts)

Pasted image 20251205151020.png

A. The Equity Curve (Capital Preservation)

The most striking feature of the performance is visible in Q1 2025 (January - March).

B. The Catch-Up Phase

From April to September, as the market recovered, the strategy re-entered. You can see the Red line aggressively catching up to the Grey line. The Momentum component worked perfectly here, capturing the bulk of the mid-year rally (July alone returned +8.4%).

C. The Drawdown (The “Whipsaw” Risk)

The strategy struggled in Q4 (October - November).

3. The Verdict

The strategy successfully performed its primary job: it outperformed the benchmark by not losing money when the market crashed.

While a 4.74% return may seem modest compared to a bull market frenzy, it is infinitely better than the significant loss a “Buy and Hold” investor would have suffered in 2025. The system proved it acts as a volatility shield, effectively “flattening” the downside while still attempting to capture the upside. ## Summary

This script provides a professional framework for crypto backtesting. By decoupling the Signal (Composite Score) from the Sizing (ATR/Risk Parity), we create a system that can capture upside trends while surviving the inevitable crypto crashes.