Skip to content

Risk Management Guide

The tradedesk.portfolio and tradedesk.execution modules provide utilities for position sizing and position state tracking.

Position Sizing

ATR-Normalized Sizing

The atr_normalised_size() function calculates position size based on Average True Range (ATR):

from tradedesk.portfolio import atr_normalised_size

# Calculate position size
size = atr_normalised_size(
    risk_per_trade=100.0,    # Amount of capital to risk
    atr=0.0050,              # Current ATR value
    atr_risk_mult=2.0,       # ATR multiplier for stop distance
    min_size=0.1,            # Minimum position size
    max_size=10.0,           # Maximum position size
)

# size = risk_per_trade / (atr * atr_risk_mult)
# size = 100.0 / (0.0050 * 2.0) = 10000.0
# Clamped to max_size: 10.0

Formula

raw_size = risk_per_trade / (atr * atr_risk_mult * point_value)
final_size = clamp(raw_size, min_size, max_size)
  • risk_per_trade: The amount of capital you're willing to risk on this trade
  • atr: Current ATR value (measure of volatility)
  • atr_risk_mult: How many ATRs away your stop loss is
  • point_value: Unit-independent scale factor (default: 1.0)
  • min_size: Minimum position size (broker/risk limits)
  • max_size: Maximum position size (risk limits)

Unit-Independent Sizing with point_value

The point_value parameter allows the same risk_per_trade to produce the same contract count across price feeds at different decimal scales.

Example: Spot gold may be reported as dollars on IG (1234.56) but as cents in Dukascopy cache (123456). To maintain consistent position sizing:

# IG live (price in dollars): gold at 1234.56
size_live = atr_normalised_size(
    risk_per_trade=100.0,
    atr=5.0,
    atr_risk_mult=2.0,
    point_value=1.0,  # IG reports in dollars
    min_size=0.1,
    max_size=50.0,
)

# Backtest with Dukascopy cache (price in cents): gold at 123456
size_backtest = atr_normalised_size(
    risk_per_trade=100.0,
    atr=500.0,           # 500 cents = 5.00 dollars
    atr_risk_mult=2.0,
    point_value=0.01,    # Dukascopy reports in cents
    min_size=0.1,
    max_size=50.0,
)

# Both produce the same contract count despite the 100x price scale difference
assert size_live == size_backtest

This ensures backtests with Dukascopy cache produce identical position sizes to live IG runs.

Example Use Cases

Conservative Sizing

# Risk 1% of $10,000 account = $100
# ATR = 0.0020, stop at 2x ATR

size = atr_normalised_size(
    risk_per_trade=100.0,
    atr=0.0020,
    atr_risk_mult=2.0,
    min_size=0.1,
    max_size=50.0,
)
# size = 100 / (0.0020 * 2.0) = 25000
# Clamped to max: 50.0

Adaptive Sizing

# Adjust position size based on market volatility
from tradedesk.marketdata.indicators import ATR

atr_indicator = ATR(period=14)

# ... update indicator with candles ...

current_atr = atr_indicator.value()

if current_atr:
    size = atr_normalised_size(
        risk_per_trade=risk_amount,
        atr=current_atr,
        atr_risk_mult=2.0,
        min_size=min_trade_size,
        max_size=max_trade_size,
    )

Position Tracking

PositionTracker

The PositionTracker class maintains state for an open position:

from tradedesk.execution import PositionTracker
from tradedesk.types import Direction

# Create tracker
position = PositionTracker()

# Open a position
position.open(
    direction=Direction.LONG,
    size=1.5,
    entry_price=1.1000
)

# Check position state
assert not position.is_flat()
assert position.direction == Direction.LONG
assert position.size == 1.5
assert position.entry_price == 1.1000
assert position.bars_held == 0

Tracking Position Metrics

from tradedesk import Candle

# Update with each new candle
candle = Candle(
    timestamp="1234567890000",
    open=1.1010,
    high=1.1050,
    low=1.1000,
    close=1.1030
)

# Track bars held
position.bars_held += 1

# Update Maximum Favorable Excursion (MFE)
position.update_mfe(candle)
print(f"MFE: {position.mfe_points} points")

# Calculate current P&L
pnl = position.current_pnl_points(current_price=1.1030)
print(f"Current P&L: {pnl} points")

MFE/MAE Tracking

Maximum Favorable Excursion (MFE) tracks the best price movement:

# For LONG positions: MFE = max(high - entry_price)
# For SHORT positions: MFE = max(entry_price - low)

position.open(Direction.LONG, size=1.0, entry_price=100.0)

candle1 = Candle(timestamp="1", open=100, high=105, low=99, close=103)
position.update_mfe(candle1)
assert position.mfe_points == 5.0  # 105 - 100

candle2 = Candle(timestamp="2", open=103, high=107, low=102, close=106)
position.update_mfe(candle2)
assert position.mfe_points == 7.0  # 107 - 100 (updated to new max)

Complete Position Lifecycle

from tradedesk.execution import PositionTracker
from tradedesk.types import Direction

class MyStrategy:
    def __init__(self):
        self.position = PositionTracker()

    async def on_entry_signal(self, direction: Direction, size: float, price: float):
        """Open a new position."""
        if not self.position.is_flat():
            return  # Already in a position

        self.position.open(direction, size, price)
        print(f"Entered {direction.value} at {price}")

    async def on_candle_close(self, candle):
        """Update position metrics."""
        if self.position.is_flat():
            return

        # Track bars
        self.position.bars_held += 1

        # Update MFE
        self.position.update_mfe(candle)

        # Calculate current P&L
        pnl = self.position.current_pnl_points(candle.close)

        print(f"Bars held: {self.position.bars_held}, "
              f"MFE: {self.position.mfe_points:.4f}, "
              f"P&L: {pnl:.4f}")

        # Check exit conditions
        if self.should_exit(pnl):
            await self.exit_position(candle.close)

    async def exit_position(self, price: float):
        """Close the position."""
        pnl = self.position.current_pnl_points(price)
        print(f"Exited at {price}, P&L: {pnl:.4f}, MFE: {self.position.mfe_points:.4f}")

        # Reset for next trade
        self.position.reset()

    def should_exit(self, current_pnl: float) -> bool:
        """Example exit logic."""
        # Stop loss
        if current_pnl < -50.0:
            return True

        # Take profit
        if current_pnl > 100.0:
            return True

        # Trailing stop (give back logic)
        if self.position.bars_held > 10 and self.position.mfe_points > 50.0:
            # Exit if we've given back 50% of MFE
            if current_pnl < self.position.mfe_points * 0.5:
                return True

        return False

P&L Calculation

Points-Based P&L

# LONG position
entry = 1.1000
current = 1.1050

pnl_points = current - entry  # 0.0050 points

# SHORT position
entry = 1.1000
current = 1.0950

pnl_points = entry - current  # 0.0050 points

The PositionTracker.current_pnl_points() method handles this automatically:

# LONG position
position.open(Direction.LONG, size=2.0, entry_price=1.1000)
pnl = position.current_pnl_points(current_price=1.1050)
assert pnl == pytest.approx(0.005)  # 1.1050 - 1.1000

# SHORT position
position.reset()
position.open(Direction.SHORT, size=2.0, entry_price=1.1000)
pnl = position.current_pnl_points(current_price=1.0950)
assert pnl == pytest.approx(0.005)  # 1.1000 - 1.0950

Best Practices

1. Size to Your Risk Tolerance

# Calculate risk per trade as % of account
account_size = 10000.0
risk_percent = 0.01  # 1%
risk_per_trade = account_size * risk_percent

size = atr_normalised_size(
    risk_per_trade=risk_per_trade,
    atr=current_atr,
    atr_risk_mult=2.0,
    min_size=0.1,
    max_size=50.0,
)

2. Use Reasonable ATR Multiples

  • Tight stops (1-1.5 ATR): Higher win rate, but more stop-outs
  • Medium stops (2-3 ATR): Balanced approach
  • Wide stops (>3 ATR): Lower win rate, but winners run longer

3. Set Position Limits

Always enforce min/max position sizes:

# Broker minimums
min_size = 0.01

# Risk management maximums
# (e.g., never risk more than 5% of account on one trade)
max_size = calculate_max_size_from_risk(account_size, max_risk_pct=0.05)

size = atr_normalised_size(
    risk_per_trade=risk_amount,
    atr=atr,
    atr_risk_mult=mult,
    min_size=min_size,
    max_size=max_size,
)

4. Track Position Metrics

Use PositionTracker to analyze trade quality:

# After exit
print(f"Trade held {position.bars_held} bars")
print(f"Max favorable: {position.mfe_points} points")
print(f"Final P&L: {final_pnl} points")
print(f"Efficiency: {final_pnl / position.mfe_points if position.mfe_points else 0:.2%}")

5. Handle Edge Cases

# Zero or negative ATR
atr = get_current_atr()
if atr is None or atr <= 0:
    # Skip this trade or use default size
    return

# Very small ATR (would result in huge position)
if atr < min_atr_threshold:
    # Use minimum ATR to avoid oversizing
    atr = min_atr_threshold

size = atr_normalised_size(risk_per_trade, atr, ...)

See Also


License

Licensed under the Apache License, Version 2.0. See: https://www.apache.org/licenses/LICENSE-2.0

Copyright 2026 Radius Red Ltd. | Contact