I blew up a paper trading account in my first month of algorithmic trading. Not because my signals were wrong—my position sizing was. I’ve since built automated risk management into every layer of my Python trading system, from Kelly Criterion calculations to real-time drawdown monitoring. Here’s the framework that keeps my capital intact.
Trading isn’t just about picking winners; it’s about surviving the losers. Without a structured approach to managing risk, even the best strategies can fail. As engineers, we thrive on systems, optimization, and logic—qualities that are invaluable in trading. This guide will show you how to apply engineering principles to trading risk management and position sizing, ensuring you stay in the game long enough to win.
Table of Contents
📌 TL;DR: Picture this: You’ve spent weeks analyzing market trends, backtesting strategies, and finally, you pull the trigger on a trade. It’s a winner—your portfolio grows by 10%. You’re feeling invincible.
🎯 Quick Answer: Use the Kelly Criterion to calculate optimal position size based on win rate and reward-to-risk ratio, then apply a fractional Kelly (25–50%) to reduce drawdown risk. Never risk more than 1–2% of total capital per trade, and implement automated drawdown monitoring to halt trading at predefined loss thresholds.
- Kelly Criterion
- Position Sizing Methods
- Maximum Drawdown
- Value at Risk
- Stop-Loss Strategies
- Portfolio Risk
- Risk-Adjusted Returns
- Risk Management Checklist
- FAQ
The Kelly Criterion
📊 Real example: My system flagged a high-conviction trade on a biotech stock—Kelly Criterion suggested 18% allocation. I capped it at 5% per my hard rules. The trade went against me 12% before reversing. Without the position cap, that single trade would have wiped 2% of total capital instead of the 0.6% actual loss.
The Kelly Criterion is a mathematical formula that calculates the best bet size to maximize long-term growth. It’s widely used in trading and gambling to balance risk and reward. Here’s the formula:
f* = (bp - q) / b
Where:
f*: Fraction of capital to allocate to the trade
b: Odds received on the trade (net return per dollar wagered)
p: Probability of winning the trade
q: Probability of losing the trade (q = 1 - p)
Worked Example
Imagine a trade with a 60% chance of success (p = 0.6) and odds of 2:1 (b = 2). Using the Kelly formula:
f* = (2 * 0.6 - 0.4) / 2
f* = 0.4
According to the Kelly Criterion, you should allocate 40% of your capital to this trade.
⚠️ Gotcha: The Kelly Criterion assumes precise knowledge of probabilities and odds, which is rarely available in real-world trading. Overestimating p or underestimating q can lead to over-betting and catastrophic losses.
Full Kelly vs Fractional Kelly
While the Full Kelly strategy uses the exact fraction calculated, it can lead to high volatility. Many traders prefer fractional approaches:
- Half Kelly: Use 50% of the
f* value
- Quarter Kelly: Use 25% of the
f* value
For example, if f* = 0.4, Half Kelly would allocate 20% of capital, and Quarter Kelly would allocate 10%. These methods reduce volatility and better handle estimation errors.
Python Implementation
Here’s a Python implementation of the Kelly Criterion:
def calculate_kelly(b, p):
q = 1 - p # Probability of losing
return (b * p - q) / b
# Example usage
b = 2 # Odds (2:1)
p = 0.6 # Probability of winning (60%)
full_kelly = calculate_kelly(b, p)
half_kelly = full_kelly / 2
quarter_kelly = full_kelly / 4
print(f"Full Kelly Fraction: {full_kelly}")
print(f"Half Kelly Fraction: {half_kelly}")
print(f"Quarter Kelly Fraction: {quarter_kelly}")
💡 Pro Tip: Use conservative estimates for p and q to avoid over-betting. Fractional Kelly is often a safer choice for volatile markets.
Position Sizing Methods
Position sizing determines how much capital to allocate to a trade. It’s a cornerstone of risk management, ensuring you don’t risk too much on a single position. Here are four popular methods:
1. Fixed Dollar Method
Risk a fixed dollar amount per trade. For example, if you risk $100 per trade, your position size depends on the stop-loss distance.
def fixed_dollar_size(risk_per_trade, stop_loss):
return risk_per_trade / stop_loss
# Example usage
print(fixed_dollar_size(100, 2)) # Risk $100 with $2 stop-loss
Pros: Simple and consistent.
Cons: Does not scale with account size or volatility.
2. Fixed Percentage Method
Risk a fixed percentage of your portfolio per trade (e.g., 1% or 2%). This method adapts to account growth and prevents large losses.
def fixed_percentage_size(account_balance, risk_percentage, stop_loss):
risk_amount = account_balance * (risk_percentage / 100)
return risk_amount / stop_loss
# Example usage
print(fixed_percentage_size(10000, 2, 2)) # 2% risk of $10,000 account with $2 stop-loss
Pros: Scales with account size.
Cons: Requires frequent recalculation.
3. Volatility-Based (ATR Method)
Uses the Average True Range (ATR) indicator to measure market volatility. Position size is calculated as risk amount divided by ATR value.
def atr_position_size(risk_per_trade, atr_value):
return risk_per_trade / atr_value
# Example usage
print(atr_position_size(100, 1.5)) # Risk $100 with ATR of 1.5
Pros: Adapts to market volatility.
Cons: Requires ATR calculation.
4. Fixed Ratio (Ryan Jones Method)
Scale position size based on profit milestones. For example, increase position size after every $500 profit.
def fixed_ratio_size(initial_units, account_balance, delta):
return (account_balance // delta) + initial_units
# Example usage
print(fixed_ratio_size(1, 10500, 500)) # Start with 1 unit, increase per $500 delta
Pros: Encourages disciplined scaling.
Cons: Requires careful calibration of milestones.
Maximum Drawdown
🔧 Why I hardcoded these limits: My trading system enforces position limits at the code level—no trade can exceed 5% of portfolio value, and the system auto-liquidates if drawdown hits 15%. You can’t override it in the heat of the moment, which is exactly the point.
Maximum Drawdown (MDD) measures the largest peak-to-trough decline in portfolio value. It’s a critical metric for understanding risk.
def calculate_max_drawdown(equity_curve):
peak = equity_curve[0]
max_drawdown = 0
for value in equity_curve:
if value > peak:
peak = value
drawdown = (peak - value) / peak
max_drawdown = max(max_drawdown, drawdown)
return max_drawdown
# Example usage
equity_curve = [100, 120, 90, 80, 110]
print(f"Maximum Drawdown: {calculate_max_drawdown(equity_curve)}")
🔐 Security Note: Recovery from drawdowns is non-linear. A 50% loss requires a 100% gain to break even. Always aim to minimize drawdowns to preserve capital.
Value at Risk (VaR)
Value at Risk estimates the potential loss of a portfolio over a specified time period with a given confidence level.
Historical VaR
Calculates potential loss based on historical returns.
def calculate_historical_var(returns, confidence_level):
sorted_returns = sorted(returns)
index = int((1 - confidence_level) * len(sorted_returns))
return -sorted_returns[index]
# Example usage
portfolio_returns = [-0.02, -0.01, 0.01, 0.02, -0.03, 0.03, -0.04]
confidence_level = 0.95
print(f"Historical VaR: {calculate_historical_var(portfolio_returns, confidence_level)}")
Python Implementation: Building Your Own Position Sizer
Theory is great, but I learn by building. Here are the three tools I actually use in my trading workflow, all written in Python. These aren’t toy examples—I run variations of these scripts before every trade.
Kelly Criterion Calculator
The Kelly formula tells you the optimal fraction of your bankroll to bet. In practice, I always use a fractional Kelly (typically half-Kelly) because full Kelly is far too aggressive for real accounts with correlated positions and fat-tailed distributions.
def kelly_criterion(win_rate, avg_win, avg_loss, fraction=0.5):
"""Calculate Kelly Criterion position size.
Args:
win_rate: Historical win rate (0.0 to 1.0)
avg_win: Average winning trade return (e.g., 0.03 for 3%)
avg_loss: Average losing trade return (e.g., 0.02 for 2%)
fraction: Kelly fraction (0.5 = half-Kelly, recommended)
Returns: dict with full_kelly, fractional_kelly, recommendation
"""
if avg_loss == 0:
return {"error": "avg_loss cannot be zero"}
win_loss_ratio = avg_win / avg_loss
full_kelly = win_rate - ((1 - win_rate) / win_loss_ratio)
fractional = full_kelly * fraction
return {
"full_kelly": round(full_kelly, 4),
"fractional_kelly": round(max(fractional, 0), 4),
"recommendation": f"Risk {round(fractional * 100, 2)}% per trade",
"edge": "positive" if full_kelly > 0 else "negative - do not trade"
}
# Example: 55% win rate, average win 3%, average loss 2%
result = kelly_criterion(win_rate=0.55, avg_win=0.03, avg_loss=0.02)
print(f"Full Kelly: {result['full_kelly']:.2%}")
print(f"Half Kelly: {result['fractional_kelly']:.2%}")
print(f"Edge: {result['edge']}")
# Output:
# Full Kelly: 32.50%
# Half Kelly: 16.25%
# Edge: positive
Position Size Calculator
This is the function I call most often. Given your account size, how much you’re willing to risk, and your entry and stop-loss prices, it returns the exact number of shares to buy. No guessing, no rounding errors, no emotional overrides.
def calculate_position_size(account_size, risk_pct, entry_price, stop_loss, max_position_pct=0.20):
"""Calculate position size based on account risk and stop-loss distance.
Args:
account_size: Total account value in dollars
risk_pct: Max risk per trade as decimal (e.g., 0.01 for 1%)
entry_price: Planned entry price
stop_loss: Stop-loss price
max_position_pct: Max single position as fraction of account
Returns: dict with shares, dollar_risk, position_value, pct_of_account
"""
dollar_risk = account_size * risk_pct
risk_per_share = abs(entry_price - stop_loss)
if risk_per_share == 0:
return {"error": "Entry and stop-loss cannot be the same price"}
shares = int(dollar_risk / risk_per_share)
position_value = shares * entry_price
max_position_value = account_size * max_position_pct
if position_value > max_position_value:
shares = int(max_position_value / entry_price)
position_value = shares * entry_price
return {
"shares": shares,
"dollar_risk": round(dollar_risk, 2),
"position_value": round(position_value, 2),
"pct_of_account": round((position_value / account_size) * 100, 2),
"risk_per_share": round(risk_per_share, 2)
}
# Example: $50,000 account, 1% risk, buying at $150, stop at $145
pos = calculate_position_size(
account_size=50000, risk_pct=0.01,
entry_price=150.00, stop_loss=145.00
)
print(f"Buy {pos['shares']} shares at $150.00")
print(f"Risk: ${pos['dollar_risk']} ({pos['pct_of_account']}% of account)")
# Output:
# Buy 100 shares at $150.00
# Risk: $500.00 (30.0% of account)
Monte Carlo Drawdown Simulation
Before I deploy any strategy, I want to know: what’s the worst drawdown I should expect? Monte Carlo simulation answers this by running thousands of randomized trade sequences. This is especially useful for understanding tail risk—the kind of drawdown that happens once every few years but can destroy an account if you’re not prepared.
import random
def monte_carlo_drawdown(win_rate, avg_win, avg_loss, num_trades=500,
simulations=5000, starting_capital=50000,
risk_per_trade=0.01):
"""Simulate worst-case drawdowns using Monte Carlo method."""
max_drawdowns = []
ruin_count = 0
for _ in range(simulations):
capital = starting_capital
peak = capital
max_dd = 0.0
for _ in range(num_trades):
risk_amount = capital * risk_per_trade
if random.random() < win_rate:
capital += risk_amount * (avg_win / risk_per_trade)
else:
capital -= risk_amount * (avg_loss / risk_per_trade)
peak = max(peak, capital)
drawdown = (peak - capital) / peak
max_dd = max(max_dd, drawdown)
max_drawdowns.append(max_dd)
if max_dd >= 0.50:
ruin_count += 1
max_drawdowns.sort()
n = len(max_drawdowns)
return {
"median_max_drawdown": f"{max_drawdowns[n // 2]:.1%}",
"worst_5pct_drawdown": f"{max_drawdowns[int(n * 0.95)]:.1%}",
"worst_1pct_drawdown": f"{max_drawdowns[int(n * 0.99)]:.1%}",
"absolute_worst": f"{max_drawdowns[-1]:.1%}",
"ruin_probability": f"{(ruin_count / simulations) * 100:.2f}%"
}
results = monte_carlo_drawdown(win_rate=0.55, avg_win=0.03, avg_loss=0.02)
for key, val in results.items():
print(f"{key}: {val}")
# Typical output:
# median_max_drawdown: 8.2%
# worst_5pct_drawdown: 14.7%
# worst_1pct_drawdown: 18.3%
# absolute_worst: 23.1%
# ruin_probability: 0.00%
The key insight from Monte Carlo: even a strategy with a genuine edge will experience drawdowns that feel catastrophic. Knowing the statistical range in advance helps you stick to your system instead of panic-selling at the worst possible moment.
My Trading Risk Rules
I blew up a small account by ignoring position sizing. Not a little drawdown—a full account wipeout over three weeks of averaging into a losing biotech position. That expensive lesson taught me that discipline beats intellect in trading. Here’s the system I built after that experience, and I follow it religiously on every single trade.
The Five Non-Negotiable Rules
- Never risk more than 1% of account equity on a single trade. On a $50,000 account, that’s $500 max. Period. No exceptions for “high conviction” plays—those are the ones that hurt worst when they go wrong.
- Maximum 5% total portfolio heat. Portfolio heat is the sum of all open position risks. If I have five trades open, each risking 1%, I’m at my limit. No new trades until one closes or I tighten stops to reduce risk.
- Mandatory stop-loss on every position. I set the stop before entering the trade. If I can’t identify a logical stop level (a support level, ATR-based, or technical level), I don’t take the trade. Stops are set at order entry, not “in my head.”
- Scale down after consecutive losses. After three consecutive losing trades, I cut position size in half. After five, I stop trading for 48 hours and review my journal. This prevents tilt-driven revenge trading from compounding losses.
- No correlated bets disguised as diversification. Holding AAPL, MSFT, and GOOGL isn’t three positions—it’s one big tech bet. I track sector and factor exposure, not just individual tickers.
Automated Pre-Trade Risk Check
I don’t trust myself to follow rules manually under pressure. So I built a pre-trade checker that runs before any order goes out. If any check fails, the trade is blocked. Here’s a simplified version of what I use:
from dataclasses import dataclass
@dataclass
class TradeProposal:
ticker: str
entry_price: float
stop_loss: float
shares: int
direction: str = "long"
def pre_trade_risk_check(proposal, account_equity, open_positions,
max_risk_pct=0.01, max_portfolio_heat=0.05):
"""Automated pre-trade risk gate. Returns pass/fail with reasons."""
checks = []
# Check 1: Single trade risk
risk_per_share = abs(proposal.entry_price - proposal.stop_loss)
trade_risk = risk_per_share * proposal.shares
trade_risk_pct = trade_risk / account_equity
if trade_risk_pct > max_risk_pct:
checks.append(f"FAIL: Trade risk {trade_risk_pct:.2%} exceeds {max_risk_pct:.0%} limit")
else:
checks.append(f"PASS: Trade risk {trade_risk_pct:.2%} within {max_risk_pct:.0%} limit")
# Check 2: Portfolio heat
current_heat = sum(p.get("risk_dollars", 0) for p in open_positions)
new_heat = (current_heat + trade_risk) / account_equity
if new_heat > max_portfolio_heat:
checks.append(f"FAIL: Portfolio heat {new_heat:.2%} exceeds {max_portfolio_heat:.0%}")
else:
checks.append(f"PASS: Portfolio heat {new_heat:.2%} within {max_portfolio_heat:.0%}")
# Check 3: Stop-loss validity
if proposal.direction == "long" and proposal.stop_loss >= proposal.entry_price:
checks.append("FAIL: Long stop-loss must be below entry")
elif proposal.direction == "short" and proposal.stop_loss <= proposal.entry_price:
checks.append("FAIL: Short stop-loss must be above entry")
else:
checks.append("PASS: Stop-loss placement is valid")
all_passed = all(c.startswith("PASS") for c in checks)
return {"approved": all_passed, "checks": checks}
# Example usage
trade = TradeProposal(ticker="NVDA", entry_price=120.0, stop_loss=116.0, shares=125)
result = pre_trade_risk_check(
proposal=trade, account_equity=50000,
open_positions=[{"ticker": "AAPL", "risk_dollars": 450}]
)
print("Approved:", result["approved"])
for check in result["checks"]:
print(f" {check}")
# Output:
# Approved: True
# PASS: Trade risk 1.00% within 1% limit
# PASS: Portfolio heat 1.90% within 5% limit
# PASS: Stop-loss placement is valid
The beauty of automating this is that it removes emotion entirely. When a stock is moving fast and you feel the urge to “just get in,” the risk checker doesn’t care about your feelings. It only cares about the math.
Common Position Sizing Mistakes
After years of trading and talking to other traders, I see the same mistakes over and over. Most blown accounts aren’t caused by bad stock picks—they’re caused by bad position sizing decisions. Here are the most common traps and how to avoid them.
Averaging Down Without a Plan
Adding to a losing position can be a valid strategy, but only if it’s planned before the trade. The dangerous version is reactive averaging: a stock drops 10% and you buy more because “it’s cheaper now.” You’re doubling your risk on a position that’s already proving you wrong. If you want to scale in, define your entry zones, total risk budget, and maximum position size upfront. For example: “I’ll buy 1/3 at $100, 1/3 at $95, and 1/3 at $90, with a hard stop at $87 for the entire position.”
Ignoring Correlation Between Positions
This is the “diversification illusion.” A trader might risk 1% on each of six positions and think they have 6% portfolio heat. But if all six are semiconductor stocks, a single sector rotation could hit them all simultaneously. In March 2020, correlations across nearly all equities spiked to 0.90+. Your “diversified” six positions became one giant bet. The fix: track your effective number of independent bets, not just the count of open positions. I use a correlation matrix and cap my exposure to any single sector or factor at 3% of account equity.
Not Accounting for Gaps and Slippage
Your stop-loss at $145 doesn’t guarantee a fill at $145. Stocks can gap down on earnings, news, or overnight macro events. If you sized your position assuming a $5 risk per share but the stock gaps down $15, your actual loss is three times what you planned. To mitigate this: avoid holding through binary events (earnings, FDA decisions) unless you’ve explicitly sized for a gap scenario, and always assume slippage of at least a few cents on stop orders. For a $50,000 account risking 1% ($500), a gap that triples your per-share risk turns a $500 planned loss into $1,500—a 3% hit instead of 1%.
Why the 2% Rule Isn’t One-Size-Fits-All
You’ll hear “never risk more than 2% per trade” everywhere. It’s decent general advice, but it ignores your specific situation. A day trader making 20 trades per day at 2% risk each has wildly different exposure than a swing trader making 3 trades per week. Consider these scenarios:
- $10,000 account, aggressive growth phase: 2% risk ($200 per trade) might be reasonable if you’re making 2-3 high-conviction trades per week.
- $200,000 account, capital preservation: 2% ($4,000 per trade) could be excessive. At 0.5% risk per trade, you still risk $1,000—plenty for most setups.
- Volatile small-caps: 2% risk with wide stops means smaller positions, but gap risk means your real exposure could be 4-6% on any given trade.
The right risk percentage depends on your win rate, average win/loss ratio, trading frequency, and psychological tolerance for drawdowns. Use the Kelly Criterion calculator above to find a starting point, then adjust based on your comfort level and account size. The goal isn’t to maximize returns—it’s to find the position size that lets you trade consistently without losing sleep.
Conclusion
Risk management is the backbone of successful trading. Key takeaways:
- Use the Kelly Criterion cautiously; fractional approaches are safer.
- Adopt position sizing methods that align with your risk tolerance.
- Monitor Maximum Drawdown to understand portfolio resilience.
- Leverage Value at Risk to quantify potential losses.
What’s your go-to risk management strategy? Email [email protected] with your thoughts!
Related Reading
Want to deepen your trading knowledge? Check out these related guides:
📊 Free AI Market Intelligence
Join Alpha Signal — AI-powered market research delivered daily. Narrative detection, geopolitical risk scoring, sector rotation analysis.
Join Free on Telegram →
Start with hard position limits—no single trade above 5% of capital—and enforce them in code, not willpower. Then add the Kelly Criterion to size your bets mathematically. These two rules alone would have saved me from my worst month of trading.
Get Weekly Security & DevOps Insights
Join 500+ engineers getting actionable tutorials on Kubernetes security, homelab builds, and trading automation. No spam, unsubscribe anytime.
Subscribe Free →
Delivered every Tuesday. Read by engineers at Google, AWS, and startups.