Skip to content

Risk Management Guide

This guide covers risk management principles, position sizing strategies, and risk controls for the DSTA trading system.

Overview

Risk management is the most important factor in trading success. Even the best strategy will fail without proper risk controls.

Core Principles

  1. Capital Preservation: Protecting capital is more important than making profits
  2. Consistent Sizing: Position sizes should be systematic, not emotional
  3. Limit Losses: Cut losses quickly, let winners run
  4. Diversification: Don't put all eggs in one basket
  5. Know Your Risk: Always know maximum potential loss

Risk Hierarchy

1. Account Risk    → Total portfolio value at risk
2. Position Risk   → Risk per individual position  
3. Trade Risk      → Risk per individual trade
4. Execution Risk  → Slippage, gaps, liquidity

Position Sizing Strategies

Position sizing determines how much capital to allocate to each trade. DSTA supports multiple methods.

1. Fixed Dollar Amount

Method: Allocate fixed dollar amount per trade.

When to Use: - Simple, easy to understand - Good for beginners - Stable account size

Formula: Position Size = Fixed Amount

Example:

from backtesting.portfolio import BacktestPortfolio, PositionSizingMethod
from decimal import Decimal

portfolio = BacktestPortfolio(
    bars=bars,
    events_queue=events_queue,
    start_date=start_date,
    initial_capital=Decimal('100000'),
    position_sizing=PositionSizingMethod.FIXED,
    position_size_value=Decimal('5000')  # $5,000 per trade
)

Pros: - Simple to implement - Predictable risk per trade - Easy to track

Cons: - Doesn't adjust for account growth - May be too conservative when account grows - May be too aggressive when account shrinks

2. Fixed Percentage of Capital

Method: Risk a fixed percentage of total capital per trade.

When to Use: - Most common method - Adjusts automatically with account size - Good balance of growth and safety

Formula: Position Size = Total Capital × Percentage

Example:

portfolio = BacktestPortfolio(
    bars=bars,
    events_queue=events_queue,
    start_date=start_date,
    initial_capital=Decimal('100000'),
    position_sizing=PositionSizingMethod.PERCENT_CAPITAL,
    position_size_value=Decimal('0.02')  # Risk 2% of capital per trade
)

# With $100,000 capital and 2% risk:
# Position Size = $100,000 × 0.02 = $2,000 at risk
# If stop loss is 5%: $2,000 / 0.05 = $40,000 position

Pros: - Scales with account size - Protects against large drawdowns - Industry standard

Cons: - Requires stop-loss calculation - More complex than fixed dollar

Recommended Percentages: - Conservative: 1% - Moderate: 2% - Aggressive: 3-5% - Never exceed 5% per single trade

3. Volatility-Based Sizing (ATR)

Method: Size positions based on volatility using Average True Range (ATR).

When to Use: - Assets with varying volatility - Want consistent risk despite price swings - More sophisticated traders

Formula: Position Size = (Capital × Risk%) / (ATR × ATR Multiplier)

Example:

from backtesting.strategies.base import BaseStrategy

class VolatilityBasedStrategy(BaseStrategy):
    def calculate_position_size(self, symbol):
        """Calculate position size based on ATR."""
        # Get ATR (14-period)
        atr = self.calculate_atr(symbol, period=14)
        if atr is None:
            return Decimal('0')

        # Risk management parameters
        account_value = self.portfolio.get_total_value()
        risk_per_trade = account_value * Decimal('0.02')  # 2% risk
        atr_multiplier = Decimal('2.0')  # Stop at 2× ATR

        # Calculate position size
        risk_per_unit = atr * atr_multiplier
        position_size = risk_per_trade / risk_per_unit

        return position_size

# Example calculation:
# Account: $100,000
# Risk: 2% = $2,000
# Current ATR: $500
# ATR Multiplier: 2.0
# Stop Loss: $500 × 2.0 = $1,000
# Position Size: $2,000 / $1,000 = 2 units

Pros: - Adapts to market volatility - Consistent risk across different market conditions - Professional approach

Cons: - More complex to implement - Requires volatility calculation - May miss opportunities in low-volatility periods

4. Kelly Criterion

Method: Mathematically optimal position sizing based on win rate and risk-reward.

When to Use: - Have reliable statistics (win rate, avg win/loss) - Want to maximize long-term growth - Sophisticated traders

Formula: Kelly % = W - [(1 - W) / R]

Where: - W = Win rate (as decimal) - R = Risk/reward ratio (avg win / avg loss)

Example:

from decimal import Decimal

def calculate_kelly_percentage(win_rate, avg_win, avg_loss):
    """
    Calculate Kelly percentage.

    Args:
        win_rate: Win rate as decimal (e.g., 0.6 for 60%)
        avg_win: Average winning trade amount
        avg_loss: Average losing trade amount (positive number)

    Returns:
        Kelly percentage (0.0 to 1.0)
    """
    if avg_loss == 0:
        return 0

    risk_reward_ratio = avg_win / avg_loss
    kelly = win_rate - ((1 - win_rate) / risk_reward_ratio)

    # Apply fractional Kelly (more conservative)
    fractional_kelly = kelly * 0.5  # Use 50% of Kelly (safer)

    # Cap at maximum
    return max(0, min(fractional_kelly, 0.25))  # Never exceed 25%

# Example:
# Win rate: 60% (0.6)
# Avg win: $1,000
# Avg loss: $500
# Risk/Reward: 1000/500 = 2.0
# Kelly: 0.6 - (0.4 / 2.0) = 0.6 - 0.2 = 0.4 (40%!)
# Fractional Kelly (50%): 0.4 × 0.5 = 0.2 (20%)
# 
# Risk 20% of capital per trade (very aggressive!)

Important Notes: - Full Kelly is too aggressive - use fractional Kelly (25-50%) - Requires accurate statistics (100+ trades minimum) - Small estimation errors can lead to large losses - Most professionals use ¼ Kelly or less

Pros: - Mathematically optimal for long-term growth - Maximizes compound growth rate

Cons: - Very aggressive (large drawdowns) - Requires accurate statistics - Sensitive to estimation errors - Not suitable for most traders

5. Custom Position Sizing

Method: Implement your own position sizing logic.

Example:

class CustomPositionSizing:
    """Custom position sizing based on multiple factors."""

    def __init__(self, base_risk_pct=0.02):
        self.base_risk_pct = Decimal(str(base_risk_pct))

    def calculate_position_size(
        self,
        capital,
        signal_strength,
        volatility,
        market_regime,
        correlation_score
    ):
        """
        Calculate position size based on multiple factors.

        Args:
            capital: Total account value
            signal_strength: Signal confidence (0.0 to 1.0)
            volatility: Current market volatility (0.0 to 1.0)
            market_regime: 'bull', 'bear', or 'sideways'
            correlation_score: Portfolio correlation (0.0 to 1.0)

        Returns:
            Position size in dollars
        """
        # Start with base risk
        risk_pct = self.base_risk_pct

        # Adjust for signal strength
        risk_pct *= Decimal(str(signal_strength))

        # Reduce in high volatility
        if volatility > 0.7:
            risk_pct *= Decimal('0.5')

        # Adjust for market regime
        if market_regime == 'bear':
            risk_pct *= Decimal('0.75')  # More conservative
        elif market_regime == 'bull':
            risk_pct *= Decimal('1.25')  # More aggressive

        # Reduce if portfolio is highly correlated
        if correlation_score > 0.7:
            risk_pct *= Decimal('0.8')

        # Calculate position size
        position_size = capital * risk_pct

        # Apply limits
        max_position = capital * Decimal('0.1')  # Never exceed 10%
        return min(position_size, max_position)

Risk Limits and Controls

1. Maximum Position Size

Limit the maximum size of any single position.

class Portfolio:
    MAX_POSITION_PCT = Decimal('0.20')  # 20% max per position

    def check_position_size(self, position_value):
        """Ensure position doesn't exceed maximum."""
        total_value = self.get_total_value()
        max_position = total_value * self.MAX_POSITION_PCT

        if position_value > max_position:
            logger.warning(
                f"Position size ${position_value} exceeds maximum ${max_position}"
            )
            return max_position

        return position_value

2. Maximum Daily Loss

Stop trading if daily loss exceeds threshold.

class Portfolio:
    MAX_DAILY_LOSS_PCT = Decimal('0.05')  # 5% max daily loss

    def __init__(self, ...):
        super().__init__(...)
        self.daily_start_value = self.initial_capital
        self.trading_halted = False

    def check_daily_loss(self):
        """Check if daily loss limit exceeded."""
        current_value = self.get_total_value()
        daily_loss = (self.daily_start_value - current_value) / self.daily_start_value

        if daily_loss >= self.MAX_DAILY_LOSS_PCT:
            self.trading_halted = True
            logger.critical(
                f"Daily loss limit exceeded: {daily_loss:.2%}. Trading halted."
            )
            return True

        return False

    def reset_daily_loss(self):
        """Reset daily loss tracking (call at start of each day)."""
        self.daily_start_value = self.get_total_value()
        self.trading_halted = False

3. Maximum Drawdown Limit

Monitor drawdown and reduce position sizing if exceeded.

class Portfolio:
    MAX_DRAWDOWN_PCT = Decimal('0.20')  # 20% max drawdown
    DRAWDOWN_REDUCE_FACTOR = Decimal('0.5')  # Cut positions in half

    def __init__(self, ...):
        super().__init__(...)
        self.peak_value = self.initial_capital
        self.in_drawdown_protection = False

    def check_drawdown(self):
        """Check and adjust for drawdown."""
        current_value = self.get_total_value()

        # Update peak
        if current_value > self.peak_value:
            self.peak_value = current_value
            self.in_drawdown_protection = False

        # Calculate drawdown
        drawdown = (self.peak_value - current_value) / self.peak_value

        # Activate protection if needed
        if drawdown >= self.MAX_DRAWDOWN_PCT and not self.in_drawdown_protection:
            self.in_drawdown_protection = True
            logger.warning(
                f"Drawdown protection activated: {drawdown:.2%}. "
                f"Reducing position sizes by {self.DRAWDOWN_REDUCE_FACTOR:.0%}"
            )

        return drawdown

    def get_adjusted_position_size(self, base_size):
        """Adjust position size based on drawdown protection."""
        if self.in_drawdown_protection:
            return base_size * self.DRAWDOWN_REDUCE_FACTOR
        return base_size

4. Maximum Number of Positions

Limit portfolio concentration.

class Portfolio:
    MAX_CONCURRENT_POSITIONS = 10

    def can_open_position(self):
        """Check if we can open another position."""
        num_positions = len([qty for qty in self.current_holdings.values() if qty != 0])

        if num_positions >= self.MAX_CONCURRENT_POSITIONS:
            logger.info(
                f"Cannot open position: already at maximum ({self.MAX_CONCURRENT_POSITIONS})"
            )
            return False

        return True

5. Correlation Limit

Avoid highly correlated positions.

import numpy as np

class Portfolio:
    MAX_CORRELATION = 0.7

    def check_correlation(self, new_symbol, lookback_days=30):
        """Check correlation between new symbol and existing positions."""
        # Get current positions
        current_symbols = [s for s, qty in self.current_holdings.items() if qty != 0]

        if not current_symbols:
            return True  # No positions, no correlation risk

        # Get price history for all symbols
        new_prices = self.get_price_history(new_symbol, lookback_days)

        for existing_symbol in current_symbols:
            existing_prices = self.get_price_history(existing_symbol, lookback_days)

            # Calculate correlation
            correlation = np.corrcoef(new_prices, existing_prices)[0, 1]

            if abs(correlation) > self.MAX_CORRELATION:
                logger.warning(
                    f"High correlation detected: {new_symbol} vs {existing_symbol} "
                    f"({correlation:.2f}). Skipping trade."
                )
                return False

        return True

Stop-Loss Strategies

Stop-losses limit losses on individual trades.

1. Fixed Percentage Stop-Loss

Method: Exit when price moves against you by a fixed percentage.

class FixedPercentageStopLoss:
    """Fixed percentage stop-loss."""

    def __init__(self, stop_loss_pct=0.05):
        self.stop_loss_pct = Decimal(str(stop_loss_pct))

    def calculate_stop_loss(self, entry_price, direction):
        """
        Calculate stop-loss price.

        Args:
            entry_price: Entry price
            direction: 'LONG' or 'SHORT'

        Returns:
            Stop-loss price
        """
        if direction == 'LONG':
            # For long: stop below entry
            stop_loss = entry_price * (1 - self.stop_loss_pct)
        else:
            # For short: stop above entry
            stop_loss = entry_price * (1 + self.stop_loss_pct)

        return stop_loss

# Example:
# Entry: $100
# Stop Loss: 5%
# Long Stop: $100 × (1 - 0.05) = $95
# Short Stop: $100 × (1 + 0.05) = $105

2. ATR-Based Stop-Loss

Method: Set stop-loss based on volatility (ATR).

class ATRStopLoss:
    """ATR-based stop-loss."""

    def __init__(self, atr_multiplier=2.0):
        self.atr_multiplier = Decimal(str(atr_multiplier))

    def calculate_stop_loss(self, entry_price, atr, direction):
        """
        Calculate stop-loss based on ATR.

        Args:
            entry_price: Entry price
            atr: Current Average True Range
            direction: 'LONG' or 'SHORT'

        Returns:
            Stop-loss price
        """
        stop_distance = atr * self.atr_multiplier

        if direction == 'LONG':
            stop_loss = entry_price - stop_distance
        else:
            stop_loss = entry_price + stop_distance

        return stop_loss

# Example:
# Entry: $100
# ATR: $2
# Multiplier: 2.0
# Stop Distance: $2 × 2.0 = $4
# Long Stop: $100 - $4 = $96

Advantages: - Adapts to market volatility - Wider stops in volatile markets (less likely to be stopped out) - Tighter stops in calm markets (better risk management)

3. Trailing Stop-Loss

Method: Stop-loss that follows price upward (for longs) but never moves down.

class TrailingStopLoss:
    """Trailing stop-loss."""

    def __init__(self, trail_pct=0.10):
        self.trail_pct = Decimal(str(trail_pct))
        self.stop_loss = None

    def initialize(self, entry_price, direction):
        """Initialize stop-loss at entry."""
        if direction == 'LONG':
            self.stop_loss = entry_price * (1 - self.trail_pct)
        else:
            self.stop_loss = entry_price * (1 + self.trail_pct)

    def update(self, current_price, direction):
        """Update trailing stop-loss."""
        if direction == 'LONG':
            # For long: move stop up, never down
            new_stop = current_price * (1 - self.trail_pct)
            if self.stop_loss is None or new_stop > self.stop_loss:
                self.stop_loss = new_stop
        else:
            # For short: move stop down, never up
            new_stop = current_price * (1 + self.trail_pct)
            if self.stop_loss is None or new_stop < self.stop_loss:
                self.stop_loss = new_stop

        return self.stop_loss

# Example (Long position):
# Entry: $100, Trail: 10%
# Initial Stop: $90
# Price rises to $120
# New Stop: $120 × 0.9 = $108 (moved up)
# Price falls to $115
# Stop stays at $108 (doesn't move down)
# Price falls to $108 → Stopped out with $8 profit

4. Support/Resistance Stop-Loss

Method: Place stop-loss below support (long) or above resistance (short).

class SupportResistanceStopLoss:
    """Stop-loss based on support/resistance levels."""

    def __init__(self, buffer_pct=0.01):
        self.buffer_pct = Decimal(str(buffer_pct))

    def calculate_stop_loss(self, entry_price, support_level, direction):
        """
        Calculate stop-loss based on support/resistance.

        Args:
            entry_price: Entry price
            support_level: Support level (for longs) or resistance (for shorts)
            direction: 'LONG' or 'SHORT'

        Returns:
            Stop-loss price
        """
        # Add small buffer below support / above resistance
        if direction == 'LONG':
            stop_loss = support_level * (1 - self.buffer_pct)
        else:
            stop_loss = support_level * (1 + self.buffer_pct)

        return stop_loss

# Example:
# Entry: $102
# Support: $98
# Buffer: 1%
# Stop: $98 × 0.99 = $97.02

5. Time-Based Stop-Loss

Method: Exit after a certain time period regardless of price.

from datetime import datetime, timedelta

class TimeBasedStopLoss:
    """Time-based stop-loss."""

    def __init__(self, max_hold_days=7):
        self.max_hold_days = max_hold_days

    def should_exit(self, entry_date, current_date):
        """Check if max hold time exceeded."""
        days_in_trade = (current_date - entry_date).days

        if days_in_trade >= self.max_hold_days:
            logger.info(
                f"Time stop triggered: {days_in_trade} days "
                f"(max: {self.max_hold_days})"
            )
            return True

        return False

Portfolio Risk Metrics

Value at Risk (VaR)

Definition: Maximum expected loss over a given time period at a given confidence level.

import numpy as np
from scipy import stats

def calculate_var(returns, confidence_level=0.95, time_horizon=1):
    """
    Calculate Value at Risk.

    Args:
        returns: Array of returns
        confidence_level: Confidence level (0.95 = 95%)
        time_horizon: Time horizon in days

    Returns:
        VaR as percentage
    """
    # Parametric VaR (assumes normal distribution)
    mean = np.mean(returns)
    std = np.std(returns)
    z_score = stats.norm.ppf(1 - confidence_level)

    var_parametric = -(mean + z_score * std) * np.sqrt(time_horizon)

    # Historical VaR (uses actual distribution)
    var_historical = -np.percentile(returns, (1 - confidence_level) * 100)

    return {
        'parametric': var_parametric * 100,  # As percentage
        'historical': var_historical * 100
    }

# Example:
# Returns: [-0.02, 0.01, -0.01, 0.03, -0.005, ...]
# Confidence: 95%
# Time Horizon: 1 day
# 
# Interpretation: "We are 95% confident we won't lose more than X% in one day"

Conditional Value at Risk (CVaR)

Definition: Expected loss given that VaR has been exceeded.

def calculate_cvar(returns, confidence_level=0.95):
    """
    Calculate Conditional Value at Risk (Expected Shortfall).

    Args:
        returns: Array of returns
        confidence_level: Confidence level (0.95 = 95%)

    Returns:
        CVaR as percentage
    """
    # Get VaR threshold
    var = np.percentile(returns, (1 - confidence_level) * 100)

    # Calculate average of returns below VaR
    cvar = -returns[returns <= var].mean()

    return cvar * 100

# Example:
# 95% VaR: -3.5%
# CVaR: -5.2%
# 
# Interpretation: "When we lose more than VaR, we expect to lose 5.2% on average"

Portfolio Beta

Definition: Measure of portfolio volatility relative to market.

def calculate_beta(portfolio_returns, market_returns):
    """
    Calculate portfolio beta.

    Args:
        portfolio_returns: Array of portfolio returns
        market_returns: Array of market (benchmark) returns

    Returns:
        Beta value
    """
    # Calculate covariance and variance
    covariance = np.cov(portfolio_returns, market_returns)[0, 1]
    market_variance = np.var(market_returns)

    beta = covariance / market_variance

    return beta

# Interpretation:
# Beta = 1.0: Moves with market
# Beta > 1.0: More volatile than market
# Beta < 1.0: Less volatile than market
# Beta < 0.0: Moves opposite to market

Risk-Adjusted Returns

Sharpe Ratio

def sharpe_ratio(returns, risk_free_rate=0.0, periods_per_year=252):
    """Calculate Sharpe ratio."""
    excess_returns = returns - risk_free_rate / periods_per_year
    return (np.mean(excess_returns) / np.std(excess_returns)) * np.sqrt(periods_per_year)

Sortino Ratio

def sortino_ratio(returns, risk_free_rate=0.0, periods_per_year=252):
    """Calculate Sortino ratio (only penalizes downside volatility)."""
    excess_returns = returns - risk_free_rate / periods_per_year
    downside_returns = excess_returns[excess_returns < 0]
    downside_std = np.std(downside_returns)

    return (np.mean(excess_returns) / downside_std) * np.sqrt(periods_per_year)

Calmar Ratio

def calmar_ratio(returns, periods_per_year=252):
    """Calculate Calmar ratio (return / max drawdown)."""
    # Annualized return
    total_return = (1 + returns).prod() - 1
    years = len(returns) / periods_per_year
    annualized_return = (1 + total_return) ** (1 / years) - 1

    # Max drawdown
    cumulative = (1 + returns).cumprod()
    running_max = np.maximum.accumulate(cumulative)
    drawdown = (cumulative - running_max) / running_max
    max_drawdown = abs(drawdown.min())

    return annualized_return / max_drawdown if max_drawdown != 0 else 0

Best Practices

1. Never Risk More Than You Can Afford to Lose

# ❌ Bad - Risking everything
position_size = account_value  # 100% in one trade!

# ✅ Good - Limited risk
max_risk_per_trade = account_value * 0.02  # 2% max

2. Use Position Sizing Consistently

# ❌ Bad - Emotional sizing
if feeling_confident:
    position_size = large_size
else:
    position_size = small_size

# ✅ Good - Systematic sizing
position_size = calculate_position_size(
    capital=account_value,
    risk_pct=0.02,
    stop_loss_pct=0.05
)

3. Always Use Stop-Losses

# ❌ Bad - No stop-loss
place_order(symbol, quantity, price)

# ✅ Good - With stop-loss
place_order(symbol, quantity, price, stop_loss=stop_price)

4. Diversify Across Multiple Positions

# ❌ Bad - All capital in one position
if signal:
    buy(symbol, quantity=max_quantity)

# ✅ Good - Limit per position
max_position_pct = 0.20  # 20% max per position
max_quantity = (account_value * max_position_pct) / price

5. Monitor and Adjust

def daily_risk_check(portfolio):
    """Daily risk monitoring."""
    # Check drawdown
    drawdown = portfolio.calculate_drawdown()
    if drawdown > 0.15:
        logger.warning(f"Drawdown at {drawdown:.2%} - consider reducing risk")

    # Check correlation
    avg_correlation = portfolio.calculate_avg_correlation()
    if avg_correlation > 0.7:
        logger.warning(f"High correlation ({avg_correlation:.2f}) - diversify!")

    # Check position sizes
    for symbol, value in portfolio.position_values.items():
        pct = value / portfolio.total_value
        if pct > 0.25:
            logger.warning(f"{symbol} is {pct:.2%} of portfolio - too concentrated")

Resources

  • Strategy Development: See docs/STRATEGY_DEVELOPMENT.md
  • Backtesting: See docs/BACKTESTING_GUIDE.md
  • Portfolio Management: See src/backtesting/portfolio.py
  • Risk Metrics: See src/backtesting/performance.py

Further Reading

  • "Trade Your Way to Financial Freedom" by Van K. Tharp
  • "The New Trading for a Living" by Alexander Elder
  • "Quantitative Trading" by Ernest P. Chan
  • "Risk Management in Trading" by Davis Edwards

Support

For questions about risk management:

  1. Review this documentation
  2. Check portfolio implementation in src/backtesting/portfolio.py
  3. Examine risk metric calculations in src/backtesting/performance.py
  4. Open an issue with the risk-management label