Stops, take-profits & portfolio heat
Where to get out, when to take money off the table, and how to control the total risk across every open position at once — including the correlation trap.
Everything so far has sized a single trade. But an account is a portfolio of open trades, and its risk is not just the sum of the parts — correlated positions can all lose together, turning three "1% risks" into one 3% risk. This lesson covers the exit toolkit (stops and take-profits) and then the portfolio-level discipline that keeps the *whole book* inside your risk budget: managing total heat.
Stop types
A stop is your pre-committed answer to "how do I know I am wrong?" — decided *before* the trade, when you are calm, not during it when you are not. The three you will use constantly:
- Fixed / structural stop — a set price level, usually below a support level, a swing low, or a round number. Simple, but ignores how volatile the market currently is.
- ATR (volatility) stop — placed a multiple of ATR away (lesson 3). Widens in wild markets so normal noise does not stop you out, tightens in calm ones. The workhorse.
- Trailing stop — follows price in your favour and never moves backwards, locking in profit as the trade works while giving the trend room to run. The classic tool for letting winners run.
def update_trailing_stop(current_stop, price, atr_value, atr_mult=class="n">3.0):
class="s">""class="s">"
Ratchet the stop up as price rises; never lower it.
Returns the new stop level.
"class="s">""
candidate = price - atr_mult * atr_value class="c"># stop trails class="n">3 ATRs below price
if current_stop is None:
return candidate
return max(current_stop, candidate) class="c"># only ever moves up
stop = None
for price, atr_value in [(class="n">100, class="n">1.0), (class="n">104, class="n">1.1), (class="n">108, class="n">1.2),
(class="n">106, class="n">1.2), (class="n">112, class="n">1.0)]:
stop = update_trailing_stop(stop, price, atr_value)
print(f"price {price} ATR {atr_value} -> stop {stop:.1f}")
class="c"># price class="n">100 ATR class="n">1.0 -> stop class="n">97.0
class="c"># price class="n">104 ATR class="n">1.1 -> stop class="n">100.7
class="c"># price class="n">108 ATR class="n">1.2 -> stop class="n">104.4
class="c"># price class="n">106 ATR class="n">1.2 -> stop class="n">104.4 <- price dipped, stop HELD, didn't drop
class="c"># price class="n">112 ATR class="n">1.0 -> stop class="n">109.0 <- new high, stop ratchets up againWatch the fourth line: price dipped to 106 but the stop *held* at 104.4 rather than following price down. A trailing stop is a one-way ratchet — it protects gains without choking the trend, which is exactly what you want from a trend-following exit.
Take-profits and the R-multiple
A take-profit is the mirror image of a stop: a level where you bank the win. The cleanest way to reason about both is the R-multiple, where 1R is your initial risk (the distance from entry to stop). A trade that makes twice what it risked is +2R; one that hits its stop is −1R. Thinking in R normalises everything — a 2R win is a 2R win whether the stop was 4 points or 40.
def r_multiple(entry, stop, exit_price):
class="s">""class="s">"How many R (units of initial risk) did the trade make?"class="s">""
risk = abs(entry - stop) class="c"># 1R in price terms
pnl = exit_price - entry class="c"># for a long
return pnl / risk
class="c"># Entry class="n">100, stop class="n">96 -> 1R = class="n">4 points
print(r_multiple(class="n">100, class="n">96, class="n">108)) class="c"># +class="n">2.0R (made class="n">8, risked class="n">4)
print(r_multiple(class="n">100, class="n">96, class="n">96)) class="c"># -class="n">1.0R (stopped out)
print(r_multiple(class="n">100, class="n">96, class="n">102)) class="c"># +class="n">0.5R
class="c"># Expectancy: average R across many trades. Positive = an edge.
trades_R = [class="n">2.0, -class="n">1.0, -class="n">1.0, class="n">3.0, -class="n">1.0, class="n">1.5, -class="n">1.0, class="n">2.0]
expectancy = sum(trades_R) / len(trades_R)
print(fclass="s">"Expectancy: {expectancy:+.2f}R per trade") class="c"># +class="n">0.56RA common rule of thumb: only take trades whose take-profit is at least 2R away — a 2:1 reward-to-risk ratio — so that even a sub-50% win rate is profitable. But the deeper lesson is expectancy: the average R per trade. As long as that is positive over a large sample, the sizing methods from the earlier lessons turn it into account growth. A negative expectancy cannot be sized into a profit — no position-sizing trick rescues a strategy with no edge.
Portfolio heat: total risk across all positions
"Heat" is the sum of the open risk across every position you hold right now — how much of your account is on the line if *everything* hits its stop at once. You size each trade at 1%, fine — but if you hold twelve of them, your total heat is 12%, and a correlated market event could realise a big chunk of that in a single day. Professionals cap total heat (commonly 6–10%) and refuse new trades once the cap is hit, no matter how good the setup looks.
def open_risk(position, equity):
class="s">""class="s">"Risk of one open position as a fraction of equity."class="s">""
per_unit = abs(position[class="s">"entry"] - position[class="s">"stop"])
return (position[class="s">"units"] * per_unit) / equity
def portfolio_heat(positions, equity):
return sum(open_risk(p, equity) for p in positions)
def can_add_trade(positions, equity, new_risk_pct, heat_cap=class="n">0.08):
class="s">""class="s">"Reject a new trade if it would push total heat over the cap."class="s">""
current = portfolio_heat(positions, equity)
if current + new_risk_pct > heat_cap:
return False, current
return True, current
equity = 100_000
book = [
{class="s">"entry": class="n">100, class="s">"stop": class="n">96, class="s">"units": class="n">500}, class="c"># class="n">2.0% risk
{class="s">"entry": class="n">50, class="s">"stop": class="n">48, class="s">"units": class="n">1000}, class="c"># class="n">2.0% risk
{class="s">"entry": class="n">20, class="s">"stop": class="n">19, class="s">"units": class="n">3000}, class="c"># class="n">3.0% risk
]
heat = portfolio_heat(book, equity)
print(fclass="s">"Current heat: {heat:.class="n">1%}") class="c"># class="n">7.0%
ok, current = can_add_trade(book, equity, new_risk_pct=class="n">0.02)
print(fclass="s">"Add a class="n">2% trade? {ok} (heat is {current:.class="n">1%})")
class="c"># Add a class="n">2% trade? False (heat is class="n">7.0%) -> would breach the class="n">8% capEach trade was sized responsibly, yet the book is already carrying 7% of heat, and the fourth trade — perfectly reasonable in isolation — gets rejected because it would push total risk past the 8% cap. The heat check is the governor that stops "a dozen individually-sane bets" from adding up to a reckless portfolio.
The correlation trap
Heat as a simple sum *understates* your true risk when positions are correlated. Long three tech stocks, or long five currency pairs that all move against the dollar, and you do not hold three or five independent 1% bets — you hold one big bet wearing several hats. On a bad day they all hit their stops together, and your "diversified" book behaves like a single concentrated position.
import numpy as np
def correlation_adjusted_heat(risks, corr_matrix):
class="s">""class="s">"
risks : array of per-position risk fractions, e.g. [class="n">0.02, class="n">0.02, class="n">0.02]
corr_matrix : NxN correlation matrix between the positions
returns : effective portfolio heat accounting for correlation
"class="s">""
r = np.array(risks)
class="c"># portfolio class="s">'risk' as a quadratic form: sqrt(r' C r)
return float(np.sqrt(r @ corr_matrix @ r))
risks = [class="n">0.02, class="n">0.02, class="n">0.02] class="c"># three class="n">2% positions -> naive heat = class="n">6%
independent = np.identity(class="n">3) class="c"># zero correlation
correlated = np.array([[class="n">1.0, class="n">0.9, class="n">0.9],
[class="n">0.9, class="n">1.0, class="n">0.9],
[class="n">0.9, class="n">0.9, class="n">1.0]]) class="c"># highly correlated
print(fclass="s">"Naive sum: {sum(risks):.class="n">1%}") class="c"># class="n">6.0%
print(fclass="s">"Independent heat: {correlation_adjusted_heat(risks, independent):.class="n">1%}")
print(fclass="s">"Correlated (class="n">0.9) heat:{correlation_adjusted_heat(risks, correlated):.class="n">1%}")
class="c"># Naive sum: class="n">6.0%
class="c"># Independent heat: class="n">3.5% <- true diversification LOWERS effective risk
class="c"># Correlated (class="n">0.9) heat:class="n">5.7% <- barely diversified; near the full class="n">6%When the three positions are genuinely independent, the effective heat is only 3.5% — real diversification actually *reduces* your risk below the naive sum. But when they are 90% correlated, effective heat is 5.7%, almost the full undiversified 6%: you got none of the benefit you thought you had. The lesson is to count correlated positions as (nearly) one position when you compute heat, and to cap exposure per *theme* — per sector, per currency, per risk factor — not just per ticker.
Putting it all together
You now have the complete risk toolkit. Size each trade by fixed-fractional or volatility-targeted risk (lessons 2–3). Bound the aggressive end with Kelly, and live at a fraction of it (lesson 4). Exit with a stop and let winners run with a trailing take-profit, thinking in R-multiples and expectancy. And govern the *whole book* with a heat cap, adjusted for correlation, so no cluster of bets can sink you at once.
Return to where this part began: risk management is the real edge because it is the only part of trading you fully control. The signal decides how often you are right; risk management decides whether you are still standing to collect. Master this and a modest edge compounds into real money — because you survived long enough to let it. Part 8 takes these controls live, wiring them into an execution engine that trades a real account.