Risk Management in Code
Risk management is the single most important component of any trading strategy. A mediocre entry trigger with excellent risk management will outperform a brilliant entry trigger 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 them against each bar’s full high–low range as it arrives. The Playground’s place_order() helper has no bracket arguments, so the full strategy below defines a place_bracket_order() helper that sends stopLossBracket and takeProfitBracket through api("Order/place"):
SL_TICKS = 20TP_TICKS = int(SL_TICKS * TP_RATIO) # 40 ticks = 10 points
# Buy 1 contract with a 20-tick stop loss and a 40-tick take profitresult = place_bracket_order(0, 1, SL_TICKS, TP_TICKS)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 produces 50 trade setups 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:
import math
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. """ if not all(math.isfinite(value) and value > 0 for value in (balance, risk_pct, sl_distance_points, tick_value)): return 0 dollar_risk = balance * risk_pct risk_per_contract = math.ceil(sl_distance_points / 0.25) * tick_value return int(dollar_risk / risk_per_contract)
# Usage (next_bar() in the full strategy below keeps `balance` current):sl_distance = atr_val * 1.5 # Pointssize = calculate_position_size(balance, RISK_PER_TRADE, sl_distance, tick_value=5.0)
if size > 0: sl_ticks = math.ceil(sl_distance / 0.25) actual_risk = size * sl_ticks * 5.0 result = api("Order/place", { "accountId": ACCOUNT_ID, "contractId": CONTRACT_ID, "type": 2, "side": 0, "size": size, "stopLossBracket": {"ticks": sl_ticks}, }) print(f"[BUY] {size} contracts | Stop risk: ${actual_risk:,.0f} ({actual_risk / balance * 100:.2f}%)")else: print("[SKIP] No contract fits the risk budget or the stop distance is invalid")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)
# --- API HELPERS ---balance = 50000.0
def next_bar(): """Like get_next_bar(), but also records the balance Replay/step reports.""" global balance for attempt in range(11): result = api("Replay/step", {"accountId": ACCOUNT_ID, "steps": 1}) error = result if result.get("errorCode") == 503: try: error = json.loads(result.get("errorMessage", "").removeprefix( "Request outcome is unknown; mutation was not retried: ")) except ValueError: pass if (result.get("success", True) or error != {"success": False, "errorCode": 503, "errorMessage": "Bars are still loading — retry shortly"} or attempt == 10): break time.sleep(1) if not result.get("success", True): print(f"[ERROR] Replay/step failed: {result.get('errorMessage', 'unknown error')}", file=sys.stderr) sys.exit(1) balance = (result.get("account") or {}).get("balance", balance) bars = result.get("bars", []) if not bars: return None bar = bars[0] if END_DATE and bar.get("t", "") > END_DATE + "T23:59:59": return None return bar
def place_bracket_order(side, size, sl_ticks, tp_ticks): """Market order with a stop loss and take profit attached.""" return api("Order/place", { "accountId": ACCOUNT_ID, "contractId": CONTRACT_ID, "type": 2, "side": side, "size": size, "stopLossBracket": {"ticks": sl_ticks}, "takeProfitBracket": {"ticks": tp_ticks}, })
# --- 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 = 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: current_balance = balance 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 entry condition # ============================================ 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_bracket_order(0, 1, SL_TICKS, tp_ticks) if result.get("orderId") or result.get("success"): position = "LONG" entry_price = price balance_before_trade = balance 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_bracket_order(1, 1, SL_TICKS, tp_ticks) if result.get("orderId") or result.get("success"): position = "SHORT" entry_price = price balance_before_trade = balance 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 end = api("Replay/end", {"accountId": ACCOUNT_ID}) balance = (end.get("account") or {}).get("balance", balance) 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 conditions to pair with your risk controls
- ICT Smart Money Strategy — see professional-grade risk management in an advanced strategy