Position Sizing, Risk of Ruin & Trading Psychology¶
Monte Carlo equity curves on real volatility, and the quantified cost of a psychology-driven mistake.
Part 30 of 36 in the ServLoci algo/options trading notebook series — full index in notebooks/README.md.
Setup — no broker account needed¶
!pip install -q yfinance
import yfinance as yf
import pandas as pd
import numpy as np
# No broker account, no API key, no ServLoci setup needed for this chapter —
# yfinance reads public end-of-day data from Yahoo Finance. Indian tickers take
# an ".NS" suffix (NSE) or ".BO" (BSE); index tickers are prefixed with "^"
# (^NSEI = Nifty 50, ^BSESN = Sensex, ^GSPC = S&P 500).
NSE_TICKER = "RELIANCE.NS"
US_TICKER = "AAPL"
INDEX_TICKER = "^NSEI"
print("yfinance", yf.__version__)
"Risk 1-2% per trade" is usually asserted, never shown¶
Every trading course repeats some version of this rule. Almost none of them show you why the number matters, because that requires simulating many possible sequences of wins and losses and watching what position size does to the tail of the outcome distribution — not just its average. This is the same distinction notebook 15 makes between expected value and risk of ruin, but here it's run as an actual Monte Carlo instead of stated as a maxim.
The setup. A real trading strategy has its own win rate and average win/loss ratio, ideally measured from its own backtest (notebook 29). As a reproducible stand-in, this notebook derives a win rate and payoff ratio from the real index's up-day vs. down-day history — the index doesn't know or care about position sizing, so it's a clean, real-data source of "how often do you win, and by how much relative to a loss" without hand-picking numbers to make a point. Each simulated "trade" then risks a fixed fraction of current equity, wins with that empirical probability, and either gains risk_pct × payoff_ratio or loses risk_pct.
Ruin isn't "the strategy loses money." It's a large enough drawdown that recovery becomes mathematically or psychologically implausible — losing 50% requires a 100% gain just to get back to even. The simulation below runs thousands of independent trade sequences per risk level and reports the fraction of paths that ever cross that floor. Watch how a strategy that looks perfectly reasonable in expectation can still ruin a meaningful share of accounts once you size it too aggressively — and how that share does not scale linearly with risk_pct.
hist = yf.download(INDEX_TICKER, period="2y", interval="1d", progress=False, auto_adjust=True, multi_level_index=False)
daily_returns = hist["Close"].pct_change().dropna()
ann_vol = float(daily_returns.std() * np.sqrt(252))
print(f"{INDEX_TICKER} trailing 2y annualized volatility: {ann_vol:.1%}")
wins = daily_returns[daily_returns > 0]
losses = daily_returns[daily_returns < 0]
win_rate = float(len(wins) / len(daily_returns))
payoff_ratio = float(wins.mean() / abs(losses.mean()))
print(f"Empirical up-day rate: {win_rate:.1%} avg-up / avg-down ratio: {payoff_ratio:.2f}")
print("(a real strategy should use its own win rate / payoff ratio from notebook 29, not the raw index)")
rng = np.random.default_rng(42)
N_PATHS, N_TRADES, RUIN_FLOOR = 3000, 250, 0.5 # ruin = equity falls below 50% of starting capital
def simulate(risk_pct, revenge_multiplier=None, cap=0.20):
"""Fixed-fractional sizing. If revenge_multiplier is set, risk doubles
(up to `cap`) after every loss and resets to risk_pct after every win —
the classic 'get back to even' behavioral deviation."""
equity = np.ones(N_PATHS)
ruined = np.zeros(N_PATHS, dtype=bool)
current_risk = np.full(N_PATHS, risk_pct)
for _ in range(N_TRADES):
win = rng.random(N_PATHS) < win_rate
step = np.where(win, current_risk * payoff_ratio, -current_risk)
equity = np.clip(equity * (1 + step), 0, None)
ruined |= equity < RUIN_FLOOR
if revenge_multiplier is not None:
current_risk = np.where(win, risk_pct, np.minimum(current_risk * revenge_multiplier, cap))
return equity, ruined
print("\n-- Position size vs. risk of ruin (disciplined fixed-fractional) --")
for risk_pct in (0.01, 0.02, 0.05, 0.10):
equity, ruined = simulate(risk_pct)
print(f"risk_pct={risk_pct:>4.0%} median ending equity={np.median(equity):.2f}x start "
f"P(ruin)={ruined.mean():.1%}")
Quantifying a psychological mistake, not just naming it¶
Trading-psychology material — Zerodha Varsity's Innerworth module is a good example — correctly identifies behaviors like revenge trading ("double the size to get back to even after a loss") or moving a stop-loss further away mid-trade ("give it more room, it'll come back"). What it doesn't do is put a number on what that behavior costs, so the warning stays abstract and easy to ignore under real drawdown stress.
The simulation below runs the identical trade-generating process from above twice, at the same starting risk_pct: once with strict fixed-fractional sizing, once with a "revenge" rule that doubles risk after every loss (capped so it can't reach 100% in one trade) and resets after a win. Same win rate, same payoff ratio, same number of trades — the only variable is whether position size is allowed to react emotionally to the last outcome.
disciplined_eq, disciplined_ruin = simulate(0.02)
revenge_eq, revenge_ruin = simulate(0.02, revenge_multiplier=2.0)
print("-- Same 250-trade sequence, disciplined vs. revenge-sized --")
print(f"Disciplined 2% fixed risk : median {np.median(disciplined_eq):.2f}x start P(ruin)={disciplined_ruin.mean():.1%}")
print(f"Same rule + revenge sizing: median {np.median(revenge_eq):.2f}x start P(ruin)={revenge_ruin.mean():.1%}")
print("\nSame edge, same market — the only difference is whether size reacted to the last loss.")
The fix is mechanical, not motivational¶
Notice that guard_max_loss() in notebook 15 is a hard raise, not a warning — and now you've seen why that design choice matters more than it looks. A rule that can be overridden in the moment ("just this once, I'll size up — I'm confident") is not a rule; it's a suggestion that fails exactly when a real strategy's variance puts it under the most pressure, which is precisely the moment simulated above where revenge sizing does the most damage. Position sizing discipline that survives contact with a real drawdown is enforced by code or a broker-side hard limit, not by remembering to be calm.
« Previous: Backtesting Without Fooling Yourself
Next: Capstone: Build Your Own Strategy End to End »
Try the concepts above interactively: Options Strategy Builder · Docs · Get your static IP