Risk Management in Code
Risk management is the single most important component of any trading strategy. A mediocre entry signal with excellent risk management will outperform a brilliant entry signal with poor risk management every time. This tutorial shows you how to implement professional-grade risk controls in Python.
The Risk Management Stack
Professional algorithmic traders implement risk controls at multiple layers:
- Per-trade risk — Stop loss and take profit on every position
- Daily limits — Maximum loss per day, maximum trades per day
- Streak protection — Stop after N consecutive losses
- Position sizing — Risk a fixed percentage of capital per trade
- Session filters — Only trade during high-liquidity sessions
We will implement each one.
1. Stop Loss and Take Profit
The most basic risk control: set a maximum loss and profit target for every trade.
Fixed-Tick Stop Loss
SL_TICKS = 20 # 5 points on NQ (1 tick = 0.25 points)TP_RATIO = 2.0 # Risk:reward ratio
# On entry:entry_price = bar["c"]if position == "LONG": stop_loss = entry_price - (SL_TICKS * 0.25) # 5 points below entry take_profit = entry_price + (SL_TICKS * 0.25 * TP_RATIO) # 10 points above entryelif position == "SHORT": stop_loss = entry_price + (SL_TICKS * 0.25) take_profit = entry_price - (SL_TICKS * 0.25 * TP_RATIO)Checking SL/TP in the Loop
# Inside the main loop, BEFORE entry logic:if position == "LONG": if bar["l"] <= stop_loss: place_order(ACCOUNT_ID, CONTRACT_ID, 1, 1) # Sell to close pnl = stop_loss - entry_price print(f"[SL HIT] @ {stop_loss:.2f} | P&L: {pnl:+.2f} pts") position = None
elif bar["h"] >= take_profit: place_order(ACCOUNT_ID, CONTRACT_ID, 1, 1) pnl = take_profit - entry_price print(f"[TP HIT] @ {take_profit:.2f} | P&L: {pnl:+.2f} pts") position = None
elif position == "SHORT": if bar["h"] >= stop_loss: place_order(ACCOUNT_ID, CONTRACT_ID, 0, 1) # Buy to close pnl = entry_price - stop_loss print(f"[SL HIT] @ {stop_loss:.2f} | P&L: {pnl:+.2f} pts") position = None
elif bar["l"] <= take_profit: place_order(ACCOUNT_ID, CONTRACT_ID, 0, 1) pnl = entry_price - take_profit print(f"[TP HIT] @ {take_profit:.2f} | P&L: {pnl:+.2f} pts") position = NoneUsing Bracket Orders (Recommended)
TestMax supports bracket orders — attach SL and TP directly to the order so the engine handles execution. This is more accurate than manual checking because the engine evaluates tick-by-tick:
SL_TICKS = 20TP_TICKS = int(SL_TICKS * TP_RATIO) # 40 ticks = 10 points
result = place_order( ACCOUNT_ID, CONTRACT_ID, 0, 1, # Buy 1 contract sl_ticks=SL_TICKS, # Stop loss: 20 ticks from entry tp_ticks=TP_TICKS # Take profit: 40 ticks from entry)For a deep dive on bracket orders, see Bracket Orders (SL/TP).
ATR-Based Stop Loss
Fixed-tick stops do not account for varying volatility. An ATR-based stop adapts:
class ATR: def __init__(self, period=14): self.period = period self.value = None self._prev_close = None self._tr_buffer = [] def update(self, high, low, close): if self._prev_close is not None: tr = max(high - low, abs(high - self._prev_close), abs(low - self._prev_close)) if self.value is None: self._tr_buffer.append(tr) if len(self._tr_buffer) == self.period: self.value = sum(self._tr_buffer) / self.period self._tr_buffer = None else: self.value = (self.value * (self.period - 1) + tr) / self.period self._prev_close = close return self.value
atr = ATR(14)
# In your loop:atr_val = atr.update(bar["h"], bar["l"], bar["c"])
# Set SL at 1.5x ATR from entryif atr_val: sl_distance = atr_val * 1.5 if position == "LONG": stop_loss = entry_price - sl_distance elif position == "SHORT": stop_loss = entry_price + sl_distance2. Daily Loss Limit
Stop trading for the day if cumulative losses exceed a threshold. This prevents a bad strategy from draining the account on a single day.
MAX_DAILY_LOSS = 1000 # Stop trading if daily losses exceed $1,000
current_day = ""daily_pnl = 0.0daily_locked = False
# At the top of your main loop:bar_day = bar["t"][:10]if bar_day != current_day: if current_day: print(f"[INFO] Day {current_day} ended | P&L: ${daily_pnl:+,.2f}") current_day = bar_day daily_pnl = 0.0 daily_locked = False
# After each trade closes:daily_pnl += trade_pnl_dollars
# Check daily limit:if daily_pnl <= -MAX_DAILY_LOSS and not daily_locked: daily_locked = True if position is not None: close_all_positions(ACCOUNT_ID, CONTRACT_ID) position = None print(f"[RISK] Daily loss limit hit: ${daily_pnl:,.2f} — no more trades today")
# In your entry logic, add this guard:if position is not None or daily_locked: continue # Skip entry logic3. Maximum Trades Per Day
Prevents overtrading. Even if your strategy signals 50 trades in a day, cap it:
MAX_TRADES_PER_DAY = 10daily_trades = 0
# Reset with daily_pnl at the top of the loop (same day-change logic)
# Before entry:if daily_trades >= MAX_TRADES_PER_DAY: if not daily_locked: daily_locked = True print(f"[RISK] Max trades ({MAX_TRADES_PER_DAY}) reached for today") continue
# After each trade:daily_trades += 14. Consecutive Loss Streak Protection
If you lose N trades in a row, something is wrong — the market conditions do not suit your strategy today. Stop and wait for the next day.
MAX_LOSS_STREAK = 3daily_loss_streak = 0
# After a losing trade:if trade_pnl < 0: daily_loss_streak += 1 if daily_loss_streak >= MAX_LOSS_STREAK and not daily_locked: daily_locked = True if position is not None: close_all_positions(ACCOUNT_ID, CONTRACT_ID) position = None print(f"[RISK] {daily_loss_streak} consecutive losses — skipping rest of day")else: daily_loss_streak = 0 # Reset on a win5. Position Sizing
Instead of always trading 1 contract, size your position based on how much capital you are willing to risk:
RISK_PER_TRADE = 0.02 # Risk 2% of account per trade
def calculate_position_size(balance, risk_pct, sl_distance_points, tick_value=5.0): """Calculate number of contracts based on risk.
For NQ: 1 tick = 0.25 points, $5 per tick, so $20 per point. For ES: 1 tick = 0.25 points, $12.50 per tick, so $50 per point. """ dollar_risk = balance * risk_pct risk_per_contract = sl_distance_points * (tick_value / 0.25) # Convert points to dollars if risk_per_contract <= 0: return 1 size = int(dollar_risk / risk_per_contract) return max(1, size) # Minimum 1 contract
# Usage:balance = get_account(ACCOUNT_ID)["balance"]sl_distance = atr_val * 1.5 # Pointssize = calculate_position_size(balance, RISK_PER_TRADE, sl_distance, tick_value=5.0)
result = place_order(ACCOUNT_ID, CONTRACT_ID, 0, size)print(f"[BUY] {size} contracts | Risk: ${balance * RISK_PER_TRADE:,.0f} ({RISK_PER_TRADE*100:.0f}%)")Complete Risk-Managed Strategy Template
Here is a full strategy template that includes all five risk controls. Use this as the starting point for every strategy you build:
#!/usr/bin/env python3"""Risk-Managed Strategy Template — TestMax Algo Playground"""import os, time
# --- Setup ---ACCOUNT_ID = int(os.environ.get("ACCOUNT_ID", "0"))CONTRACT_ID = os.environ.get("CONTRACT_ID", "")TOTAL_BARS = int(os.environ.get("TOTAL_BARS", "5000"))STEP_DELAY = float(os.environ.get("STEP_DELAY", "0.02"))SPEED_FILE = os.environ.get("SPEED_FILE", "")
# --- Risk Parameters ---SL_TICKS = 20 # Stop loss in ticksTP_RATIO = 2.0 # Take profit as multiple of SLMAX_DAILY_LOSS = 1000 # Dollar daily loss limitMAX_TRADES_PER_DAY = 10 # Max entries per dayMAX_LOSS_STREAK = 3 # Consecutive losses before stopping
print(f"[INFO] Risk Profile: SL={SL_TICKS}t | TP={TP_RATIO}:1 | Max Loss=${MAX_DAILY_LOSS}/day")print("-" * 50)
# --- State ---position = Noneentry_price = 0.0trade_count = 0wins = 0current_day = ""daily_pnl = 0.0daily_trades = 0daily_locked = Falsedaily_loss_streak = 0balance_before_trade = 50000
try: for i in range(TOTAL_BARS): bar = get_next_bar() if bar is None: break read_speed() if STEP_DELAY > 0: time.sleep(STEP_DELAY)
price = bar["c"]
# --- Daily Reset --- bar_day = bar["t"][:10] if bar_day != current_day: if current_day: print(f"[DAY] {current_day} | P&L: ${daily_pnl:+,.2f} | Trades: {daily_trades}") current_day = bar_day daily_pnl = 0.0 daily_trades = 0 daily_locked = False daily_loss_streak = 0
# --- Check if bracket order closed our position --- if position is not None and i % 5 == 0: positions = get_positions(ACCOUNT_ID) if len(positions) == 0: acct = get_account(ACCOUNT_ID) current_balance = acct["balance"] if acct else balance_before_trade pnl_usd = current_balance - balance_before_trade daily_pnl += pnl_usd trade_count += 1 daily_trades += 1
if pnl_usd >= 0: wins += 1 daily_loss_streak = 0 print(f"[TP] @ ~{price:.2f} | P&L: ${pnl_usd:+,.0f}") else: daily_loss_streak += 1 print(f"[SL] @ ~{price:.2f} | P&L: ${pnl_usd:+,.0f} | Streak: {daily_loss_streak}L") position = None
# --- Daily Loss Limit --- if daily_pnl <= -MAX_DAILY_LOSS and not daily_locked: daily_locked = True if position: close_all_positions(ACCOUNT_ID, CONTRACT_ID) position = None print(f"[RISK] Daily loss limit: ${daily_pnl:,.2f}")
if daily_trades >= MAX_TRADES_PER_DAY and not daily_locked: daily_locked = True print(f"[RISK] Max trades reached: {daily_trades}")
if daily_loss_streak >= MAX_LOSS_STREAK and not daily_locked: daily_locked = True if position: close_all_positions(ACCOUNT_ID, CONTRACT_ID) position = None print(f"[RISK] Loss streak: {daily_loss_streak}")
# --- Skip if locked or in position --- if position is not None or daily_locked: continue
# ============================================ # YOUR ENTRY LOGIC GOES HERE # Replace the condition below with your signal # ============================================ should_buy = False # Replace with your buy condition should_sell = False # Replace with your sell condition
if should_buy: tp_ticks = int(SL_TICKS * TP_RATIO) result = place_order(ACCOUNT_ID, CONTRACT_ID, 0, 1, sl_ticks=SL_TICKS, tp_ticks=tp_ticks) if result.get("orderId") or result.get("success"): position = "LONG" entry_price = price acct = get_account(ACCOUNT_ID) balance_before_trade = acct["balance"] if acct else balance_before_trade sl = price - SL_TICKS * 0.25 tp = price + tp_ticks * 0.25 print(f"[BUY] Bar {i} @ {price:.2f} | SL: {sl:.2f} TP: {tp:.2f}")
elif should_sell: tp_ticks = int(SL_TICKS * TP_RATIO) result = place_order(ACCOUNT_ID, CONTRACT_ID, 1, 1, sl_ticks=SL_TICKS, tp_ticks=tp_ticks) if result.get("orderId") or result.get("success"): position = "SHORT" entry_price = price acct = get_account(ACCOUNT_ID) balance_before_trade = acct["balance"] if acct else balance_before_trade sl = price + SL_TICKS * 0.25 tp = price - tp_ticks * 0.25 print(f"[SELL] Bar {i} @ {price:.2f} | SL: {sl:.2f} TP: {tp:.2f}")
finally: try: close_all_positions(ACCOUNT_ID, CONTRACT_ID) except: pass account = get_account(ACCOUNT_ID) balance = account["balance"] if account else 50000 pnl = balance - 50000 print("-" * 50) print(f"[DONE] Final Balance: ${balance:,.2f}") print(f"[DONE] Net P&L: ${pnl:+,.2f}") print(f"[DONE] Total Trades: {trade_count}") if trade_count > 0: print(f"[DONE] Win Rate: {wins / trade_count * 100:.1f}%")Risk-Reward Ratios
The relationship between your stop loss and take profit determines how often you need to win to be profitable:
| R:R Ratio | Required Win Rate to Break Even |
|---|---|
| 1:1 | 50% |
| 1.5:1 | 40% |
| 2:1 | 33% |
| 3:1 | 25% |
What’s Next
- Bracket Orders (SL/TP) — deep dive on bracket order implementation
- Multi-Indicator Strategies — combine indicators for better entry signals to pair with your risk controls
- ICT Smart Money Strategy — see professional-grade risk management in an advanced strategy