Skip to content
Back to App

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:

  1. Per-trade risk — Stop loss and take profit on every position
  2. Daily limits — Maximum loss per day, maximum trades per day
  3. Streak protection — Stop after N consecutive losses
  4. Position sizing — Risk a fixed percentage of capital per trade
  5. 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 entry
elif 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 = None

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 = 20
TP_TICKS = int(SL_TICKS * TP_RATIO) # 40 ticks = 10 points
# Buy 1 contract with a 20-tick stop loss and a 40-tick take profit
result = 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 entry
if 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_distance

2. 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.0
daily_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 logic

3. Maximum Trades Per Day

Prevents overtrading. Even if your strategy produces 50 trade setups in a day, cap it:

MAX_TRADES_PER_DAY = 10
daily_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 += 1

4. 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 = 3
daily_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 win

5. 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 # Points
size = 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 ticks
TP_RATIO = 2.0 # Take profit as multiple of SL
MAX_DAILY_LOSS = 1000 # Dollar daily loss limit
MAX_TRADES_PER_DAY = 10 # Max entries per day
MAX_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 = None
entry_price = 0.0
trade_count = 0
wins = 0
current_day = ""
daily_pnl = 0.0
daily_trades = 0
daily_locked = False
daily_loss_streak = 0
balance_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 RatioRequired Win Rate to Break Even
1:150%
1.5:140%
2:133%
3:125%

What’s Next