
Stop-loss is the most overlooked—and error-prone—component in quant strategies. Many novice strategies look great in backtests but blow up the moment you run them live. Often, the culprit is poor stop-loss design: either it’s too wide and lets single trades lose too much, or it’s too tight and gets whipsawed by market noise.
This article draws from the actual design of the Polaris strategy and covers general stop-loss design principles.
Why Stop-Loss Matters
Stop-loss is fundamentally a mechanism for admitting when you’re wrong. The logic of trend-following strategies is: most of the time there’s no trend, so the strategy bleeds small losses. Once in a while, there’s a real trend, and the strategy makes big money—and those big wins need to cover the small losses.
Without a stop-loss, that one catastrophic loss can blow out your account, leaving no opportunity for the big wins that follow.
Mathematically, stop-loss controls the maximum single-trade loss, which in turn controls the maximum drawdown:
Max Drawdown ≈ f(stop-loss %, consecutive losses, win rate)
Having 5 consecutive losing trades over some period is perfectly normal. If each loss is 10%, you’re down to 59% of your account after five losses. If each loss is 3%, you still have 85%—a massive difference.
Stop-Loss Types
1. Fixed Percentage Stop-Loss
The simplest approach: exit if your loss exceeds a fixed percentage from entry.
# Entry price 40000, stop-loss 3%
entry_price = 40000
stop_loss_pct = 0.03
stop_price_long = entry_price * (1 - stop_loss_pct) # long: 38800
stop_price_short = entry_price * (1 + stop_loss_pct) # short: 41200
Pros: Simple, predictable, fixed risk
Cons: Ignores market volatility. BTC can swing 3% intraday on a calm day, so you get whipsawed.
When to use: Longer timeframes (daily and up), or you’ve already verified that your stop is much wider than average daily volatility.
2. ATR Dynamic Stop-Loss (Recommended)
ATR (Average True Range) measures recent market volatility. Using ATR multiples for stops means your stops widen when volatility spikes and tighten when it’s calm—matching actual market conditions.
import pandas as pd
def calculate_atr(df, period=14):
"""Calculate ATR"""
high = df['high']
low = df['low']
close = df['close']
tr1 = high - low
tr2 = abs(high - close.shift(1))
tr3 = abs(low - close.shift(1))
true_range = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
atr = true_range.rolling(period).mean()
return atr
def get_stop_price(entry_price, atr_value, direction, multiplier=2.0):
"""ATR-based stop-loss price"""
if direction == 'long':
return entry_price - atr_value * multiplier
else:
return entry_price + atr_value * multiplier
# Example: BTC 1H, ATR=1200, entry 40000, long
# Stop = 40000 - 1200 * 2 = 37600 (-6%)
Choosing ATR multipliers:
- 1.0–1.5: Tight, gets triggered easily in noisy markets
- 2.0–3.0: Moderate, reasonable for most trend strategies
- 4.0+: Wide, catches big trends but larger per-trade losses
On the Polaris strategy’s 1H timeframe, backtests show fixed percentage stops are more stable than ATR. The reason: BTC’s ATR balloons during trending markets, so ATR-based stops keep widening—the opposite of what you want.
3. Trailing Stop-Loss
After entry, the stop follows price upward (on longs) or downward (on shorts), locking in floating profits so you don’t give back all your gains.
class TrailingStop:
def __init__(self, entry_price, direction, trail_pct=0.05):
self.direction = direction
self.trail_pct = trail_pct
if direction == 'long':
self.best_price = entry_price
self.stop_price = entry_price * (1 - trail_pct)
else:
self.best_price = entry_price
self.stop_price = entry_price * (1 + trail_pct)
def update(self, current_price):
"""Update every candle"""
if self.direction == 'long':
if current_price > self.best_price:
self.best_price = current_price
self.stop_price = current_price * (1 - self.trail_pct)
else:
if current_price < self.best_price:
self.best_price = current_price
self.stop_price = current_price * (1 + self.trail_pct)
return self.stop_price
def is_triggered(self, current_price):
if self.direction == 'long':
return current_price <= self.stop_price
else:
return current_price >= self.stop_price
When trailing stops work well:
- You’ve got floating profits and don’t want to give them all back
- Trending market—you want to ride the move while protecting gains
When they don’t:
- Choppy markets, where price bounces back and forth and trailing stops trigger repeatedly
4. Time-Based Stop-Loss
Exit if you’ve been holding longer than expected without the trade moving your direction, regardless of profit or loss:
def check_time_stop(entry_time, current_time, max_bars=48):
"""Force exit if held for more than 48 candles (1H = 2 days)"""
bars_held = (current_time - entry_time).total_seconds() / 3600
return bars_held >= max_bars
The logic: if price isn’t moving your way after entry, your analysis was probably wrong. Better to free up capital and hunt for a better setup.
Stop-Loss Design in the Polaris Strategy
I won’t dive into specific parameters, but here’s the design philosophy:
Core principle: fixed stop-loss percentage + cooldown protection
Why fixed percentage instead of ATR?
- Stable parameters that don’t change with market volatility
- Clearer signals in backtest results
- Works well combined with scaled take-profits, giving optimized overall expectancy
After a stop-loss is triggered, a cooldown period prevents new entries. This cooldown:
- Avoids getting whipsawed repeatedly in choppy markets
- Waits for clearer direction before re-entering
- Reduces slippage and fees
Coordinating Take-Profit with Stop-Loss
In a good strategy, take-profit design is just as important as stop-loss. The Polaris strategy uses scaled take-profits:
- First portion exits at a modest profit, reducing exposure
- Remaining position rides the trend
- Either all profits are taken or the whole position gets stopped out
This ensures that even if the market later reverses, earlier exits have already locked in some gains.
How to Determine Stop-Loss Parameters
Backtesting is the only reliable method, but watch for these pitfalls:
1. Validate on out-of-sample data
Optimize parameters using 2020–2023 data, then verify on 2024–2025 data. Don’t use the same period for both.
2. Focus on max drawdown, not win rate
High win rate doesn’t mean a good strategy. Controlling max drawdown is what matters:
- Under 20%: comfortable, within psychological tolerance
- 30%+: most people break and interfere manually
3. Avoid overfitting
If your stop-loss is precise to two decimals (like 3.17%), it’s probably overfit. Use round or half-integer values (1%, 2%, 2.5%) for general parameters.
4. Use Monte Carlo simulation
Randomly shuffle trade order and simulate 1000 possible sequences to see the drawdown distribution:
import numpy as np
def monte_carlo_max_drawdown(returns, n_simulations=1000):
"""Monte Carlo simulation of maximum drawdown"""
max_drawdowns = []
for _ in range(n_simulations):
shuffled = np.random.permutation(returns)
cumulative = np.cumprod(1 + shuffled)
peak = np.maximum.accumulate(cumulative)
drawdown = (peak - cumulative) / peak
max_drawdowns.append(drawdown.max())
return {
'median': np.median(max_drawdowns),
'p95': np.percentile(max_drawdowns, 95),
'worst': max(max_drawdowns)
}
If the p95 max drawdown is within your tolerance, your stop-loss parameters are robust.
Stop-Loss Execution in Live Trading
Good design is just step one. Live execution has its own pitfalls:
1. Limit or market orders?
- Market orders: guarantee execution, but bigger slippage (BTC can slippage 0.1–0.5% in volatile moves)
- Limit orders: control price, but might not fill (price can gap through your stop)
In extreme volatility, use market orders. Better to take slippage than fail to execute.
2. Submit stops to the exchange
Don’t just check in code—place stops on the exchange itself:
# Create stop-loss order on Binance
order = client.create_order(
symbol='BTCUSDT',
side='SELL',
type='STOP_MARKET',
stopPrice=stop_price,
closePosition=True, # close entire position
timeInForce='GTC'
)
If your strategy process crashes, the exchange-side order still protects you.
3. Regularly verify stops exist
When your strategy starts, check for open orders:
def verify_stop_loss(client, symbol):
"""Verify stop-loss order exists"""
open_orders = client.get_open_orders(symbol=symbol)
stop_orders = [o for o in open_orders if o['type'] == 'STOP_MARKET']
if not stop_orders:
print("⚠️ Warning: Stop-loss order not found!")
# re-submit order logic
Further reading:
- Polaris Strategy Live Tracking
- Writing Quant Strategies with Claude
- Binance API Security Configuration
📷 Cover image · Generated with Flux Schnell AI · Panshi.io brand style