◈   ∿ algotrading · Intermediate

Trailing Stop Loss in Python: Code for Crypto Traders

Learn how to code a trailing stop loss in Python for crypto trading. Includes working examples, formulas, and risk management rules for Binance and Bybit.

Uncle Solieditor · voc · 18.05.2026 ·views 4
◈   Contents
  1. → Is a Trailing Stop Loss a Good Idea for Crypto?
  2. → The Core Formula and How Trailing Stops Work
  3. → Python Trailing Stop Loss: Full Working Code
  4. → Connecting to Binance API for Live Trailing Stops
  5. → Position Sizing and Risk Management Numbers
  6. → Backtesting Your Trailing Stop Logic
  7. → Frequently Asked Questions
  8. → Putting It All Together

A trailing stop loss is one of the most practical tools in a crypto trader's arsenal — it lets your winners run while automatically cutting losses if the market reverses. Unlike a fixed stop, it moves with price. When BTC climbs from $60,000 to $70,000, your stop climbs too. If it then drops 5%, you're out — with profit locked in. Coding this yourself in Python gives you full control over the logic, the exchange API, and how it fits into your broader trading system.

Is a Trailing Stop Loss a Good Idea for Crypto?

The honest answer is: it depends on the market regime. In trending markets — bull runs, breakout moves, momentum plays — a trailing stop loss is excellent. It captures the bulk of a move without requiring you to manually exit. In choppy, sideways markets, it gets whipsawed constantly, triggering exits on noise rather than real reversals.

For crypto specifically, volatility is the defining characteristic. Bitcoin can swing 5-8% intraday. Set your trail too tight and you'll be stopped out on normal price breathing. Set it too loose and you give back too much. The key is calibrating your trailing percentage to the asset's Average True Range (ATR) — not picking a round number like 2% because it sounds reasonable.

Platforms like Bybit and OKX have native trailing stop functionality built into their order types. But native exchange trailing stops often have limitations — minimum trail distances, no ATR support, and no custom logic. Coding your own gives you full flexibility.

The Core Formula and How Trailing Stops Work

The math behind a trailing stop is straightforward. For a long position, the stop level is calculated from the highest price observed since entry. For a short position, it tracks the lowest price.

# Core trailing stop formulas

# For LONG positions:
# stop_price = highest_price_since_entry * (1 - trail_percent / 100)

# For SHORT positions:
# stop_price = lowest_price_since_entry * (1 + trail_percent / 100)

# Example: BTC long at $60,000 with 5% trail
entry_price = 60_000
trail_percent = 5.0
highest_price = 70_000  # price has moved up

stop_price = highest_price * (1 - trail_percent / 100)
print(f"Current trailing stop: ${stop_price:,.2f}")  # $66,500

# ATR-based trail (more robust)
# stop_price = highest_price - (atr * multiplier)
atr = 1_500  # 14-period ATR
multiplier = 2.0
stop_atr = highest_price - (atr * multiplier)
print(f"ATR-based stop: ${stop_atr:,.2f}")  # $67,000

The ATR-based approach is generally more robust for crypto. It automatically widens stops during volatile periods and tightens them during calm periods — adapting to the market rather than using a fixed percentage that might be too tight on a volatile Tuesday and too loose on a quiet weekend.

Python Trailing Stop Loss: Full Working Code

Below is a production-ready trailing stop class you can integrate with any exchange. It handles both percentage-based and ATR-based modes, tracks the high-water mark, and returns a signal when the stop is breached.

class TrailingStop:
    """
    Trailing stop loss for long crypto positions.
    Supports percentage-based and ATR-based trailing.
    """

    def __init__(self, trail_pct: float = None, atr_multiplier: float = None):
        self.trail_pct = trail_pct          # e.g. 5.0 for 5%
        self.atr_multiplier = atr_multiplier  # e.g. 2.0
        self.peak_price = None
        self.stop_price = None
        self.triggered = False

    def update(self, current_price: float, atr: float = None) -> dict:
        """
        Call this on every new price tick or candle close.
        Returns dict with stop_price and triggered flag.
        """
        if self.peak_price is None:
            self.peak_price = current_price

        # Update peak
        if current_price > self.peak_price:
            self.peak_price = current_price

        # Calculate stop level
        if self.trail_pct is not None:
            self.stop_price = self.peak_price * (1 - self.trail_pct / 100)
        elif self.atr_multiplier is not None and atr is not None:
            self.stop_price = self.peak_price - (atr * self.atr_multiplier)
        else:
            raise ValueError("Provide trail_pct or atr_multiplier + atr")

        # Check if triggered
        if current_price <= self.stop_price:
            self.triggered = True

        return {
            "peak_price": self.peak_price,
            "stop_price": round(self.stop_price, 2),
            "current_price": current_price,
            "triggered": self.triggered,
            "drawdown_pct": round(
                (self.peak_price - current_price) / self.peak_price * 100, 2
            ),
        }

    def reset(self):
        self.peak_price = None
        self.stop_price = None
        self.triggered = False


# --- Usage Example ---
if __name__ == "__main__":
    ts = TrailingStop(trail_pct=5.0)

    price_feed = [60000, 62000, 65000, 70000, 68000, 66000, 65500]

    for price in price_feed:
        result = ts.update(price)
        status = "STOP TRIGGERED" if result["triggered"] else "active"
        print(
            f"Price: ${price:} | Peak: ${result['peak_price']:} "
            f"| Stop: ${result['stop_price']:} | {status}"
        )
        if result["triggered"]:
            print(f"  => Exit at ${result['stop_price']:} "
                  f"(drawdown from peak: {result['drawdown_pct']}%)")
            break

This outputs a clean trace of your stop level as price moves. When the stop triggers, you'd feed that signal into your exchange API — whether that's placing a market order on Binance, Bybit, or Coinbase via their REST APIs.

Connecting to Binance API for Live Trailing Stops

Binance has a native trailing stop order type, but if you want custom logic (ATR-based, multi-asset, or conditional), you'll use their websocket price stream combined with your own TrailingStop class. Here's the pattern:

import asyncio
import json
import websockets
from binance.client import Client  # pip install python-binance

API_KEY = "your_api_key"
API_SECRET = "your_api_secret"

client = Client(API_KEY, API_SECRET)
ts = TrailingStop(trail_pct=4.0)  # 4% trail

SYMBOL = "BTCUSDT"
QUANTITY = 0.001  # position size in BTC


async def run_trailing_stop():
    stream_url = f"wss://stream.binance.com:9443/ws/{SYMBOL.lower()}@trade"

    async with websockets.connect(stream_url) as ws:
        print(f"Connected to Binance stream for {SYMBOL}")
        async for message in ws:
            data = json.loads(message)
            price = float(data["p"])  # trade price

            result = ts.update(price)

            if result["triggered"]:
                print(f"Stop triggered at ${result['stop_price']}")
                # Place market sell order on Binance
                order = client.order_market_sell(
                    symbol=SYMBOL,
                    quantity=QUANTITY
                )
                print(f"Order placed: {order['orderId']}")
                break


asyncio.run(run_trailing_stop())
Always test with small quantities first. On Binance, use the testnet endpoint (testnet.binance.vision) before going live. On Bybit, the testnet is at api-testnet.bybit.com. Never run untested bot code on a live account with real funds.

For OKX users, the pattern is identical — swap in the OKX SDK and adjust the websocket URL. Gate.io and KuCoin both have Python SDKs that follow the same structure. The TrailingStop class above is exchange-agnostic; only the order placement code changes.

Position Sizing and Risk Management Numbers

A trailing stop loss is only as good as the position sizing that comes before it. If you size positions too large, even a well-designed trailing stop can create catastrophic drawdowns on a string of losses. Here's a practical framework:

The standard rule: risk no more than 1-2% of total portfolio capital per trade. Your position size is determined by where your initial stop is placed, not by how much you want to make.

def calculate_position_size(
    portfolio_value: float,
    risk_pct: float,
    entry_price: float,
    initial_stop_pct: float
) -> dict:
    """
    Calculate position size based on fixed risk per trade.

    Args:
        portfolio_value: Total account value in USD
        risk_pct: Max risk per trade as % of portfolio (e.g. 1.0 for 1%)
        entry_price: Asset entry price
        initial_stop_pct: Initial stop distance as % (e.g. 5.0 for 5%)
    """
    max_loss_usd = portfolio_value * (risk_pct / 100)
    stop_distance_usd = entry_price * (initial_stop_pct / 100)
    position_size_units = max_loss_usd / stop_distance_usd
    position_value_usd = position_size_units * entry_price

    return {
        "max_loss_usd": round(max_loss_usd, 2),
        "position_size_units": round(position_size_units, 6),
        "position_value_usd": round(position_value_usd, 2),
        "portfolio_allocation_pct": round(
            position_value_usd / portfolio_value * 100, 1
        ),
    }


# Example: $50,000 portfolio, 1% risk, BTC at $70,000, 5% initial stop
result = calculate_position_size(50_000, 1.0, 70_000, 5.0)
print(result)
# {'max_loss_usd': 500.0, 'position_size_units': 0.142857,
#  'position_value_usd': 10000.0, 'portfolio_allocation_pct': 20.0}
Position Sizing Examples — $50,000 Portfolio
AssetEntryTrail %Risk/TradePosition SizeAllocation
BTC$70,0005%1% ($500)0.143 BTC20%
ETH$3,5006%1% ($500)2.38 ETH16.7%
SOL$1808%1% ($500)34.7 SOL12.5%
BTC$70,0005%2% ($1,000)0.286 BTC40%
ALT$1.2012%0.5% ($250)1,736 units41.7%

Notice how high-volatility alts require wider stops, which paradoxically limits your position size even when allocating the same dollar risk. This is the math enforcing discipline that emotion often can't. Tools like VoiceOfChain can feed you real-time signal strength data, so you know whether a trade is high-conviction enough to use your full 1-2% risk or scale back to 0.5%.

Drawdown Scenarios with Trailing Stop vs No Stop
ScenarioPosition SizeTrail %PeakExitP&L
BTC uptrend, clean exit$10,0005%$70K→$85K$80,750+$1,071
BTC reversal, stop works$10,0005%$70K→$75K→$65K$71,250+$178
No stop, same reversal$10,000None$70K→$75K→$55K$55K (manual)-$2,143
Whipsaw in range$10,0003%$70K→$72K→$69.8K$69,840-$23
Whipsaw, wider trail$10,0007%$70K→$72K→$65K$66,960-$432

Backtesting Your Trailing Stop Logic

Before running any trailing stop in live markets, backtest it against historical OHLCV data. The quickest way is to pull candle data from Binance's public API — no auth required — and simulate the stop logic across hundreds of trades.

import requests
import pandas as pd

def fetch_binance_ohlcv(symbol: str, interval: str, limit: int = 500) -> pd.DataFrame:
    url = "https://api.binance.com/api/v3/klines"
    params = {"symbol": symbol, "interval": interval, "limit": limit}
    data = requests.get(url, params=params).json()
    df = pd.DataFrame(data, columns=[
        "open_time", "open", "high", "low", "close",
        "volume", "close_time", "qav", "num_trades",
        "taker_buy_base", "taker_buy_quote", "ignore"
    ])
    df["close"] = df["close"].astype(float)
    df["high"] = df["high"].astype(float)
    df["low"] = df["low"].astype(float)
    return df


def backtest_trailing_stop(df: pd.DataFrame, trail_pct: float) -> dict:
    """Simple long-only backtest: enter on first candle, trail stop from there."""
    ts = TrailingStop(trail_pct=trail_pct)
    entry_price = df["close"].iloc[0]
    exit_price = None
    exit_idx = None

    for i, row in df.iterrows():
        result = ts.update(row["close"])
        if result["triggered"]:
            exit_price = result["stop_price"]
            exit_idx = i
            break

    if exit_price is None:
        exit_price = df["close"].iloc[-1]
        exit_idx = df.index[-1]

    pnl_pct = (exit_price - entry_price) / entry_price * 100
    return {
        "entry": entry_price,
        "exit": exit_price,
        "candles_held": exit_idx,
        "pnl_pct": round(pnl_pct, 2),
        "max_peak": ts.peak_price,
        "gave_back_pct": round(
            (ts.peak_price - exit_price) / ts.peak_price * 100, 2
        ),
    }


# Run it
df = fetch_binance_ohlcv("BTCUSDT", "4h", limit=200)
result = backtest_trailing_stop(df, trail_pct=5.0)
print(result)

The "gave_back_pct" metric is key — it tells you how much of your peak profit you surrendered before the stop fired. If you're consistently giving back 70-80% of a move, your trail is too loose. If you're getting stopped out with tiny gains on moves that later went much higher, it's too tight. Tune until gave_back_pct sits in the 20-40% range, which balances capture rate with letting winners breathe.

VoiceOfChain's signal platform can complement this approach — its real-time order flow signals can tell you when institutional buying pressure is building, helping you decide when to tighten your trail versus hold it loose for a larger move.

Frequently Asked Questions

Is a trailing stop loss a good idea for crypto trading?
Yes, in trending markets a trailing stop is one of the best tools available — it locks in profits automatically as price rises and exits when momentum reverses. The main caveat is choppy, sideways markets where it triggers constantly on noise. Always match your trail distance to the asset's volatility using ATR rather than a fixed percentage.
What percentage should I set for a crypto trailing stop?
For Bitcoin, 4-7% is a common range on the 4-hour timeframe. For higher-volatility altcoins, 8-15% is more appropriate. The best approach is ATR-based: multiply the 14-period ATR by 1.5-2.5 and use that as your trail distance. This automatically adjusts as volatility changes.
Does Binance have a built-in trailing stop order?
Yes, Binance supports trailing stop orders natively through their app and API using the TRAILING_STOP_MARKET order type. However, the minimum callback rate is 0.1% and you can't use ATR-based logic. For custom trailing stop behavior — like ATR-adaptive stops or multi-condition triggers — you need to code your own solution using their websocket API.
Can my trailing stop get slipped past during a flash crash?
Yes — this is a real risk in crypto. If price gaps down past your stop level (common during low-liquidity events or news shocks), your market order will fill at the next available price, which could be significantly below your intended stop. Using limit orders instead of market orders on stop triggers reduces slippage but risks not filling at all.
What's the difference between a trailing stop loss and a regular stop loss?
A regular stop loss is fixed — it stays at the price you set when entering the trade. A trailing stop moves upward (for longs) as price increases, always maintaining a set distance from the peak. The practical effect is that a trailing stop lets you capture the bulk of a trend, while a fixed stop only prevents losses from the original entry level.
How do I avoid getting whipsawed by my trailing stop in Python?
Three approaches help: first, only update your trailing stop on candle closes rather than every tick (this ignores intracandle wicks). Second, use ATR-based trails that widen automatically during volatile periods. Third, add a minimum profit threshold — only activate the trailing stop after you're already up 2-3%, so small moves against you don't trigger an exit before the trade has room to develop.

Putting It All Together

A trailing stop loss coded in Python gives you something no native exchange order type can: complete control. You decide the trail distance, whether it's percentage or ATR-based, when to activate it, and what happens when it triggers. The TrailingStop class shown here is intentionally simple — production systems will add logging, error handling, reconnection logic, and multi-asset support.

The most important thing isn't the code itself — it's the discipline of using it consistently. Trailing stop python code that runs reliably beats a perfect strategy that you override manually every time a trade goes slightly against you. Start with the backtesting framework, dial in your trail percentage against real historical data from Binance or Bybit, and only then connect it to a live account with small position sizes. Let the math work for you.

Track your trailing stop performance separately from your entry signals. You might have an excellent entry signal but a poorly tuned trail — or vice versa. Separating these metrics tells you exactly where to improve.
◈   more on this topic
⌘ api Kraken API Documentation for Crypto Traders: Essentials and Examples ◉ basics Mastering the ccxt library documentation for crypto traders