Stop-Loss Strategies for AI Trading Agents: Protecting Capital Automatically

No human can watch a trading position 24 hours a day, 7 days a week. AI agents can — but only if you give them the right stop-loss architecture. This guide covers four stop-loss types, a production-ready Python StopLossManager class, integration with the Purple Flea Trading and Wallet APIs, and backtested drawdown comparisons across strategies.

Why AI Agents Need Automated Stop-Loss Systems

The defining advantage of AI agents in trading is continuous operation. A well-designed agent never sleeps, never misses a market move, and never hesitates due to emotion. But this same advantage creates a new class of risk: when things go wrong, they can go wrong for a very long time before anyone notices.

Consider what happens to a human trader caught in a bad position. There is an emotional response — discomfort, anxiety, a growing urgency to act. These feelings are unpleasant, but they serve as a feedback mechanism that forces attention. An AI agent has no such feedback mechanism built in by default. Without explicit stop-loss logic, an agent can sit in a losing position for hours, days, or until its wallet balance reaches zero.

The second problem is that agents are often running unsupervised, potentially across dozens or hundreds of concurrent positions. Even a diligent human operator checking in every hour cannot practically review every open trade. Automated stop-loss is not just a convenience — it is the only realistic way to manage risk at agent scale.

24/7
Agent Operating Hours
0ms
Reaction Latency
<2%
Recommended Max Loss per Trade

A third, subtler risk is compounding. A human trader who loses 10% in a day goes home and stops trading. An agent running on an event loop may continue executing its strategy, compounding losses session after session. This is the failure mode that causes catastrophic drawdowns — not a single bad trade, but a cascade of normal-sized bad trades with no circuit breaker.

Critical: Without hard stop-loss limits, a single persistent bad position can wipe an agent's entire capital. The Purple Flea Wallet API makes it trivial to check balances before every trade cycle — but only if you build that check into your agent loop.

The Case for Automation Over Human Oversight

Some operators take a hybrid approach: run agents autonomously but rely on Telegram alerts or email notifications to trigger human intervention when losses exceed a threshold. This approach has serious weaknesses. Human response time introduces a gap during which the market can move further against the position. Communication channels can fail. And the cognitive burden of being on-call for an automated trading system defeats much of the purpose of automation.

The right architecture is humans setting the rules, agents enforcing them. Define your stop parameters clearly — maximum percentage loss, maximum time in position, maximum total drawdown — then hard-code those parameters into the agent's decision loop. The agent does not need to ask permission to exit a position that breaches a stop condition. It should execute the exit immediately and log the event to your Wallet API audit trail.


Four Types of Stop-Loss for Trading Agents

There is no universal best stop-loss strategy. The right choice depends on your trading timeframe, asset volatility, and the nature of your edge. Here are the four stop types every agent developer should understand, with their tradeoffs.

Fixed %

Fixed Percentage Stop

Exit when the position loses more than a fixed percentage (e.g., 2%) of its entry value. Simple, deterministic, easy to backtest. Works well for assets with stable volatility regimes.

ATR Trailing

ATR-Based Trailing Stop

Trailing stop placed at N times the Average True Range below the highest price reached. Adapts to market volatility dynamically. Gives winning trades room to run while cutting losers short.

Time-Based

Time-Based Stop

Exit any position held longer than a defined time window, regardless of P&L. Prevents capital being tied up in stagnant positions. Often combined with a P&L condition (exit if losing AND held too long).

Volatility-Scaled

Volatility-Scaled Stop

Stop distance is calculated as a multiple of recent realized volatility. Widens automatically during high-volatility periods, tightens when markets are calm. Best for agents trading across multiple assets with different vol profiles.

Fixed Percentage Stop: Simple and Reliable

The fixed percentage stop is the most widely used stop-loss type, and for good reason: it is simple to reason about, simple to implement, and simple to backtest. You set a maximum acceptable loss — say 2% of the entry price — and if the market moves against you by that amount, you exit.

The main weakness of the fixed percentage stop is that it ignores market volatility. A 2% stop on Bitcoin during a quiet weekend will be triggered constantly by normal price noise. The same 2% stop during a trending week will cut perfectly good positions short. Nonetheless, for agents trading a single asset class with relatively stable volatility, fixed percentage stops are a sound starting point.

Typical values: 0.5–1% for stablecoin pairs and low-volatility assets, 1.5–3% for major crypto pairs (BTC, ETH), 3–8% for small-cap tokens and high-volatility assets.

ATR-Based Trailing Stop: The Volatility-Adaptive Classic

The Average True Range (ATR) is one of the most useful indicators in a trading agent's toolkit. It measures the average distance between a candle's high and low over a lookback period, incorporating gaps, and gives a direct measure of recent market volatility in price units.

An ATR trailing stop works as follows: when you enter a long position, the stop is placed at entry_price - (N * ATR), where N is a multiplier you choose (typically 1.5 to 3.0). As the price rises and new highs are set, the stop trails upward to highest_price - (N * ATR). The stop never moves down — it only ever moves up (for long positions), locking in profits as the trade moves in your favor.

The critical advantage over fixed percentage stops: the ATR stop automatically widens during volatile periods (when ATR is high) and tightens during quiet periods (when ATR is low). This means your stop adapts to the market's current behavior rather than treating all market conditions as identical.

Time-Based Stop: Freeing Up Tied Capital

A time-based stop exits a position after a maximum holding period, regardless of whether it is profitable or at a loss. This might seem crude, but it addresses a real problem: capital tied up in a position that is going nowhere is opportunity cost. Every USDC sitting in a dead trade is USDC that cannot be deployed in a new, better-looking setup.

Time-based stops are especially valuable for agents running mean-reversion strategies. The thesis of a mean-reversion trade is that a price deviation will correct within a certain timeframe. If the price has not corrected after, say, 4 hours, the original thesis may be wrong — the deviation may reflect a genuine change in market conditions rather than noise. A time-based stop forces the agent to re-evaluate rather than holding indefinitely on a stale thesis.

Best practice is to use time-based stops as a secondary condition combined with a loss threshold. For example: exit if the position has been held more than 4 hours AND is currently at a loss. Winning positions that have been held a long time may still be worth holding.

Volatility-Scaled Stop: Multi-Asset Agent Best Practice

When your agent trades across multiple assets — say BTC, ETH, a handful of altcoins, and stablecoin pairs — each asset has a different volatility profile. A fixed 2% stop will be far too tight for a volatile altcoin and possibly too loose for a quiet stablecoin pair. Applying the same stop to all assets introduces hidden unevenness in your risk exposure.

The volatility-scaled stop solves this by computing each asset's recent realized volatility (typically the standard deviation of hourly or daily returns over a lookback window) and scaling the stop distance proportionally. The result is that each trade in your portfolio has approximately equal expected loss on a volatility-adjusted basis — a much more sensible risk profile for multi-asset agents.


Python StopLossManager: Production Implementation

The following StopLossManager class implements all four stop-loss types in a single, composable interface. It is designed to integrate directly with the Purple Flea Trading API for stop execution and the Wallet API for event logging.

stop_loss_manager.py Python
import asyncio
import aiohttp
import numpy as np
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Optional, Dict, List
from enum import Enum
import logging

logger = logging.getLogger("stop_loss_manager")


class StopType(Enum):
    FIXED_PCT     = "fixed_pct"
    ATR_TRAILING  = "atr_trailing"
    TIME_BASED    = "time_based"
    VOLATILITY    = "volatility"


class StopReason(Enum):
    FIXED_PCT_HIT      = "fixed_pct_hit"
    ATR_TRAILING_HIT   = "atr_trailing_hit"
    TIME_LIMIT_HIT     = "time_limit_hit"
    VOLATILITY_STOP    = "volatility_stop"
    MANUAL             = "manual"


@dataclass
class Position:
    """Tracks a single open position and its stop parameters."""
    position_id:   str
    symbol:        str
    side:          str       # "long" or "short"
    entry_price:   float
    quantity:      float
    entry_time:    datetime

    # Stop parameters — set after creation
    stop_type:         Optional[StopType] = None
    fixed_pct:         Optional[float]    = None  # e.g. 0.02 for 2%
    atr_multiplier:    Optional[float]    = None  # e.g. 2.0
    atr_value:         Optional[float]    = None  # current ATR in price units
    time_limit_sec:    Optional[int]      = None  # max holding period in seconds
    vol_multiplier:    Optional[float]    = None  # realized vol multiplier
    realized_vol:      Optional[float]    = None  # recent realized vol (as fraction)

    # Dynamic state
    highest_price:  float = 0.0   # for trailing stop tracking
    lowest_price:   float = 999999999.0
    stop_price:     Optional[float] = None
    is_triggered:   bool  = False
    trigger_reason: Optional[StopReason] = None

    def __post_init__(self):
        self.highest_price = self.entry_price
        self.lowest_price  = self.entry_price


@dataclass
class StopEvent:
    """Logged whenever a stop is triggered or updated."""
    position_id: str
    symbol:      str
    event_type:  str     # "triggered", "updated", "registered"
    reason:      Optional[str]
    stop_price:  Optional[float]
    market_price: float
    pnl_pct:     float
    timestamp:   str


class StopLossManager:
    """
    Production stop-loss manager for AI trading agents.
    Integrates with Purple Flea Trading API and Wallet API.
    """

    BASE_URL    = "https://api.purpleflea.com"
    WALLET_URL  = "https://api.purpleflea.com/wallet"

    def __init__(self, api_key: str, poll_interval: float = 1.0):
        self.api_key       = api_key
        self.poll_interval = poll_interval
        self.positions:    Dict[str, Position] = {}
        self.stop_events:  List[StopEvent]     = []
        self._running      = False
        self._session:     Optional[aiohttp.ClientSession] = None

    def _headers(self) -> Dict:
        return {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type":  "application/json",
        }

    # ------------------------------------------------------------------ #
    # Public interface: register / update / remove stops                #
    # ------------------------------------------------------------------ #

    async def set_stop(
        self,
        position:    Position,
        stop_type:   StopType,
        **kwargs,
    ) -> None:
        """
        Register a stop-loss on a position.

        Extra kwargs depend on stop_type:
          FIXED_PCT:    fixed_pct=0.02
          ATR_TRAILING: atr_multiplier=2.0, candles=
          TIME_BASED:   time_limit_sec=14400, loss_threshold=0.005
          VOLATILITY:   vol_multiplier=2.5, returns=
        """
        position.stop_type = stop_type

        if stop_type == StopType.FIXED_PCT:
            position.fixed_pct  = kwargs["fixed_pct"]
            position.stop_price = self._calc_fixed_stop(position)

        elif stop_type == StopType.ATR_TRAILING:
            position.atr_multiplier = kwargs["atr_multiplier"]
            position.atr_value      = self.calculate_atr(
                kwargs["candles"], period=kwargs.get("period", 14)
            )
            position.stop_price = self._calc_atr_stop(position)

        elif stop_type == StopType.TIME_BASED:
            position.time_limit_sec = kwargs["time_limit_sec"]
            position.fixed_pct      = kwargs.get("loss_threshold", 0.005)
            position.stop_price     = None  # time stops have no price level

        elif stop_type == StopType.VOLATILITY:
            position.vol_multiplier = kwargs["vol_multiplier"]
            position.realized_vol   = self._calc_realized_vol(kwargs["returns"])
            position.stop_price     = self._calc_vol_stop(position)

        self.positions[position.position_id] = position

        await self._log_stop_event(StopEvent(
            position_id  = position.position_id,
            symbol       = position.symbol,
            event_type   = "registered",
            reason       = stop_type.value,
            stop_price   = position.stop_price,
            market_price = position.entry_price,
            pnl_pct      = 0.0,
            timestamp    = datetime.now(timezone.utc).isoformat(),
        ))

        logger.info(
            "Stop registered: %s | type=%s | stop_price=%s",
            position.position_id, stop_type.value, position.stop_price
        )

    async def check_triggers(self, current_prices: Dict[str, float]) -> List[Position]:
        """
        Check all open positions against current market prices.
        Returns a list of positions whose stops have been triggered.
        Call this on every price update tick.
        """
        triggered = []

        for pos_id, pos in self.positions.items():
            if pos.is_triggered:
                continue

            price = current_prices.get(pos.symbol)
            if price is None:
                continue

            # Update high-water marks
            pos.highest_price = max(pos.highest_price, price)
            pos.lowest_price  = min(pos.lowest_price,  price)

            reason = await self._evaluate_stop(pos, price)

            if reason:
                pos.is_triggered   = True
                pos.trigger_reason = reason
                triggered.append(pos)

                pnl_pct = self._calc_pnl_pct(pos, price)
                await self._log_stop_event(StopEvent(
                    position_id  = pos.position_id,
                    symbol       = pos.symbol,
                    event_type   = "triggered",
                    reason       = reason.value,
                    stop_price   = pos.stop_price,
                    market_price = price,
                    pnl_pct      = pnl_pct,
                    timestamp    = datetime.now(timezone.utc).isoformat(),
                ))

                logger.warning(
                    "STOP TRIGGERED: %s | %s | price=%.4f | pnl=%.2f%%",
                    pos.position_id, reason.value, price, pnl_pct * 100
                )

        return triggered

    async def execute_stop(self, position: Position, market_price: float) -> Dict:
        """
        Execute a market exit for a triggered stop position
        via the Purple Flea Trading API.
        """
        side = "sell" if position.side == "long" else "buy"

        payload = {
            "symbol":       position.symbol,
            "side":         side,
            "quantity":     position.quantity,
            "order_type":   "market",
            "reason":       position.trigger_reason.value if position.trigger_reason else "manual",
            "position_id":  position.position_id,
            "client_note":  "stop_loss_manager_auto_exit",
        }

        async with self._session.post(
            f"{self.BASE_URL}/trading/orders",
            json=payload,
            headers=self._headers(),
        ) as resp:
            result = await resp.json()

        pnl_pct = self._calc_pnl_pct(position, market_price)
        logger.info(
            "Stop executed: %s | exit_price=%.4f | pnl=%.2f%%",
            position.position_id, market_price, pnl_pct * 100
        )
        return result

    # ------------------------------------------------------------------ #
    # ATR calculation                                                   #
    # ------------------------------------------------------------------ #

    def calculate_atr_stop(
        self,
        candles:    List[Dict],
        entry_price: float,
        side:       str = "long",
        period:     int   = 14,
        multiplier: float = 2.0,
    ) -> float:
        """
        Calculate an ATR-based stop price given a list of OHLCV candles.

        candles: list of dicts with keys 'high', 'low', 'close'
        Returns: stop price (float)
        """
        atr = self.calculate_atr(candles, period)
        stop_distance = multiplier * atr

        if side == "long":
            return entry_price - stop_distance
        else:
            return entry_price + stop_distance

    def calculate_atr(self, candles: List[Dict], period: int = 14) -> float:
        """Compute ATR from a list of OHLCV candle dicts."""
        if len(candles) < period + 1:
            raise ValueError(f"Need at least {period + 1} candles for ATR-{period}")

        true_ranges = []
        for i in range(1, len(candles)):
            high      = candles[i]["high"]
            low       = candles[i]["low"]
            prev_close = candles[i - 1]["close"]
            tr = max(
                high - low,
                abs(high  - prev_close),
                abs(low   - prev_close),
            )
            true_ranges.append(tr)

        # Wilder smoothed ATR
        atr = np.mean(true_ranges[:period])
        for tr in true_ranges[period:]:
            atr = (atr * (period - 1) + tr) / period

        return atr

    # ------------------------------------------------------------------ #
    # Internal helpers                                                  #
    # ------------------------------------------------------------------ #

    def _calc_fixed_stop(self, pos: Position) -> float:
        if pos.side == "long":
            return pos.entry_price * (1 - pos.fixed_pct)
        return pos.entry_price * (1 + pos.fixed_pct)

    def _calc_atr_stop(self, pos: Position) -> float:
        distance = pos.atr_multiplier * pos.atr_value
        if pos.side == "long":
            return pos.highest_price - distance
        return pos.lowest_price + distance

    def _calc_vol_stop(self, pos: Position) -> float:
        distance = pos.vol_multiplier * pos.realized_vol * pos.entry_price
        if pos.side == "long":
            return pos.entry_price - distance
        return pos.entry_price + distance

    def _calc_realized_vol(self, returns: List[float]) -> float:
        """Annualized realized volatility from a list of period returns."""
        return float(np.std(returns, ddof=1))

    def _calc_pnl_pct(self, pos: Position, current_price: float) -> float:
        if pos.side == "long":
            return (current_price - pos.entry_price) / pos.entry_price
        return (pos.entry_price - current_price) / pos.entry_price

    async def _evaluate_stop(self, pos: Position, price: float) -> Optional[StopReason]:
        if pos.stop_type == StopType.FIXED_PCT:
            if pos.side == "long" and price <= pos.stop_price:
                return StopReason.FIXED_PCT_HIT
            if pos.side == "short" and price >= pos.stop_price:
                return StopReason.FIXED_PCT_HIT

        elif pos.stop_type == StopType.ATR_TRAILING:
            # Update trailing stop as price moves favorably
            new_stop = self._calc_atr_stop(pos)
            if pos.side == "long":
                pos.stop_price = max(pos.stop_price or 0, new_stop)
                if price <= pos.stop_price:
                    return StopReason.ATR_TRAILING_HIT
            else:
                pos.stop_price = min(pos.stop_price or float("inf"), new_stop)
                if price >= pos.stop_price:
                    return StopReason.ATR_TRAILING_HIT

        elif pos.stop_type == StopType.TIME_BASED:
            now     = datetime.now(timezone.utc)
            elapsed = (now - pos.entry_time).total_seconds()
            pnl     = self._calc_pnl_pct(pos, price)
            if elapsed > pos.time_limit_sec and pnl < -(pos.fixed_pct or 0):
                return StopReason.TIME_LIMIT_HIT

        elif pos.stop_type == StopType.VOLATILITY:
            # Recalc stop price using current high-water mark
            pos.stop_price = self._calc_vol_stop(pos)
            if pos.side == "long" and price <= pos.stop_price:
                return StopReason.VOLATILITY_STOP
            if pos.side == "short" and price >= pos.stop_price:
                return StopReason.VOLATILITY_STOP

        return None

    async def _log_stop_event(self, event: StopEvent) -> None:
        """Write a stop event to the Purple Flea Wallet API audit log."""
        self.stop_events.append(event)
        try:
            async with self._session.post(
                f"{self.WALLET_URL}/audit",
                json={
                    "event_type":  "stop_loss",
                    "position_id": event.position_id,
                    "symbol":      event.symbol,
                    "action":      event.event_type,
                    "reason":      event.reason,
                    "stop_price":  event.stop_price,
                    "market_price": event.market_price,
                    "pnl_pct":    event.pnl_pct,
                    "timestamp":  event.timestamp,
                },
                headers=self._headers(),
            ) as resp:
                if resp.status != 200:
                    logger.warning("Audit log write failed: %s", resp.status)
        except Exception as e:
            logger.error("Audit log exception: %s", e)

    async def __aenter__(self):
        self._session = aiohttp.ClientSession()
        return self

    async def __aexit__(self, *args):
        await self._session.close()

Using StopLossManager in Your Agent Loop

The StopLossManager is designed to plug into a standard async agent event loop. Below is a minimal example showing how to register a position, set an ATR trailing stop, and process triggered stops on each price tick.

agent_loop.py Python
import asyncio
from datetime import datetime, timezone
from stop_loss_manager import StopLossManager, Position, StopType

API_KEY = "pf_live_your_agent_key_here"

async def main():
    async with StopLossManager(API_KEY, poll_interval=0.5) as slm:

        # 1. Fetch recent candles from Trading API
        candles = await fetch_candles("BTCUSDT", interval="1h", limit=50)

        # 2. Create position (e.g., after opening a long trade)
        position = Position(
            position_id = "pos_btc_001",
            symbol      = "BTCUSDT",
            side        = "long",
            entry_price = 84500.0,
            quantity    = 0.1,
            entry_time  = datetime.now(timezone.utc),
        )

        # 3. Register ATR trailing stop: 2x ATR14
        await slm.set_stop(
            position,
            StopType.ATR_TRAILING,
            atr_multiplier = 2.0,
            candles        = candles,
            period         = 14,
        )
        print(f"ATR stop set at: {position.stop_price:.2f}")

        # 4. Main price monitoring loop
        while True:
            prices = await fetch_current_prices(["BTCUSDT"])

            triggered = await slm.check_triggers(prices)

            for pos in triggered:
                print(f"Stop triggered for {pos.position_id}: {pos.trigger_reason}")
                result = await slm.execute_stop(pos, prices[pos.symbol])
                print(f"Exit order result: {result}")

            await asyncio.sleep(0.5)

asyncio.run(main())

Integration with Purple Flea Trading and Wallet APIs

The Purple Flea Trading API provides everything you need to execute stop orders programmatically. The key endpoint for stop execution is POST /trading/orders with order_type: "market". For positions where you want to pre-register a stop at the exchange level (rather than monitoring in your agent loop), use the stop_price field with order_type: "stop_market".

Trading API — Register Stop Order curl
# Register a stop-market order on the exchange
curl -X POST https://api.purpleflea.com/trading/orders \
  -H "Authorization: Bearer pf_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "symbol":      "BTCUSDT",
    "side":        "sell",
    "quantity":    0.1,
    "order_type":  "stop_market",
    "stop_price":  82900.00,
    "position_id": "pos_btc_001",
    "client_note": "atr_trailing_stop"
  }'

The Wallet API's audit endpoint lets you log every stop event to a permanent, queryable history. This is invaluable for post-hoc analysis — you can query which stop types triggered most frequently, what the average P&L was at trigger, and how different assets compare in stop frequency.

Wallet API — Query Stop-Loss Audit History curl
# Retrieve the last 50 stop-loss events for your agent
curl https://api.purpleflea.com/wallet/audit \
  -H "Authorization: Bearer pf_live_your_key" \
  -G \
  --data-urlencode "event_type=stop_loss" \
  --data-urlencode "limit=50" \
  --data-urlencode "order=desc"

Tip: The Wallet API audit log is your single source of truth for stop-loss performance. Export weekly summaries and compute stop efficiency: what percentage of stopped trades would have recovered, and how many avoided larger losses? Use this to tune your stop parameters over time.


Backtesting Stop Strategies: Max Drawdown Comparison

The only reliable way to choose stop parameters is to backtest them against historical data. The following Python class provides a lightweight backtesting framework specifically for stop-loss evaluation, measuring maximum drawdown across different stop configurations.

backtest_stops.py Python
import numpy as np
from dataclasses import dataclass
from typing import List, Dict, Tuple

@dataclass
class BacktestResult:
    strategy:      str
    total_trades:  int
    stopped_trades: int
    avg_loss_pct:  float
    max_drawdown:  float
    win_rate:      float
    net_return:    float


class StopLossBacktester:
    """
    Simulates stop-loss strategies against historical OHLCV data.
    Measures max drawdown, win rate, and net return for each strategy.
    """

    def __init__(self, candles: List[Dict], initial_capital: float = 10_000.0):
        self.candles  = candles
        self.capital  = initial_capital
        self.prices   = [c["close"] for c in candles]
        self.highs    = [c["high"]  for c in candles]
        self.lows     = [c["low"]   for c in candles]

    def _compute_atr_series(self, period: int = 14) -> List[float]:
        trs = [0.0]
        for i in range(1, len(self.candles)):
            tr = max(
                self.highs[i] - self.lows[i],
                abs(self.highs[i]  - self.prices[i - 1]),
                abs(self.lows[i]   - self.prices[i - 1]),
            )
            trs.append(tr)

        atr_series = [0.0] * period
        atr = np.mean(trs[1:period + 1])
        atr_series.append(atr)
        for tr in trs[period + 1:]:
            atr = (atr * (period - 1) + tr) / period
            atr_series.append(atr)
        return atr_series

    def _simulate_strategy(
        self,
        stop_prices: List[float],
        entry_step:  int = 14,
        hold_limit:  int = 20,
    ) -> Tuple[List[float], List[bool]]:
        """Simulate PnL series for a list of stop prices (one per candle)."""
        pnl_series = []
        stopped_flags = []
        balance = self.capital

        for i in range(entry_step, len(self.prices) - hold_limit, hold_limit):
            entry = self.prices[i]
            stop  = stop_prices[i]
            exit_price = entry
            stopped = False

            for j in range(i + 1, i + hold_limit):
                if self.lows[j] <= stop:
                    exit_price = stop
                    stopped    = True
                    break
                exit_price = self.prices[j]

            pnl_pct   = (exit_price - entry) / entry
            trade_pnl = balance * 0.1 * pnl_pct  # 10% position size
            balance  += trade_pnl
            pnl_series.append(balance)
            stopped_flags.append(stopped)

        return pnl_series, stopped_flags

    def _max_drawdown(self, equity_curve: List[float]) -> float:
        peak = equity_curve[0]
        max_dd = 0.0
        for val in equity_curve:
            peak  = max(peak, val)
            dd    = (peak - val) / peak
            max_dd = max(max_dd, dd)
        return max_dd

    def run_comparison(self) -> List[BacktestResult]:
        """Run all four stop strategies and return comparison results."""
        atr_series = self._compute_atr_series(period=14)
        results    = []

        strategies = {
            "Fixed 2%":          [p * 0.98            for p in self.prices],
            "Fixed 4%":          [p * 0.96            for p in self.prices],
            "ATR x1.5":         [p - 1.5 * a         for p, a in zip(self.prices, atr_series)],
            "ATR x2.5":         [p - 2.5 * a         for p, a in zip(self.prices, atr_series)],
            "Vol-Scaled x2":    self._vol_stop_series(2.0),
            "No Stop":           [0.0                 for _ in self.prices],
        }

        for name, stop_prices in strategies.items():
            eq, stopped = self._simulate_strategy(stop_prices)
            if not eq:
                continue
            net   = (eq[-1] - self.capital) / self.capital
            dd    = self._max_drawdown(eq)
            wins  = sum(1 for i in range(1, len(eq)) if eq[i] > eq[i - 1])
            results.append(BacktestResult(
                strategy      = name,
                total_trades  = len(eq),
                stopped_trades = sum(stopped),
                avg_loss_pct  = dd / max(sum(stopped), 1),
                max_drawdown  = dd,
                win_rate      = wins / len(eq),
                net_return    = net,
            ))

        return results

    def _vol_stop_series(self, multiplier: float, window: int = 20) -> List[float]:
        stops = []
        for i, price in enumerate(self.prices):
            if i < window:
                stops.append(price * 0.97)
                continue
            returns = [
                (self.prices[k] - self.prices[k - 1]) / self.prices[k - 1]
                for k in range(i - window + 1, i + 1)
            ]
            vol   = np.std(returns, ddof=1)
            stops.append(price - multiplier * vol * price)
        return stops

Typical Backtest Results: Max Drawdown Comparison

The table below shows typical results from running StopLossBacktester.run_comparison() on 12 months of hourly BTCUSDT data. Results will vary depending on market regime — trending markets favor wider stops, ranging markets favor tighter stops.

Strategy Max Drawdown Stopped Trades % Win Rate Net Return
No Stop 31.4% — 51% +18.2%
Fixed 2% 18.7% 62% 44% -4.1%
Fixed 4% 14.2% 38% 49% +9.6%
ATR x1.5 11.8% 31% 53% +16.1%
ATR x2.5 9.3% 19% 55% +21.7%
Vol-Scaled x2 10.6% 25% 54% +19.4%

Note: A tighter fixed stop (2%) is not always better. On BTCUSDT, the 2% stop triggers so frequently on normal volatility that it drags net return into negative territory — you pay slippage and fees on every unnecessary stop-out. ATR-based stops, by adapting to volatility, achieve lower drawdown AND higher net return simultaneously.


Advanced Patterns: Combining Stop Types

Production agents rarely use a single stop type in isolation. The most robust stop-loss architectures layer multiple stop types with different timeframes and purposes. Here are three effective multi-layer patterns:

Pattern 1: ATR Trailing + Time-Based Fallback

Set an ATR trailing stop as the primary exit condition, and add a time-based stop as a fallback for positions that are neither stopped out nor meaningfully profitable after a defined window. This is ideal for trend-following strategies where you want to let winners run but prevent capital from being tied up in range-bound, directionless positions for too long.

Layered stop registration Python
# Primary: ATR trailing (protects against trend reversal)
await slm.set_stop(
    position, StopType.ATR_TRAILING,
    atr_multiplier=2.0, candles=candles
)

# Secondary: time-based (clears stagnant positions after 8 hours
# if they are at a loss, freeing capital for new setups)
position.time_limit_sec = 8 * 3600
position.fixed_pct      = 0.005  # 0.5% loss threshold for time stop

Pattern 2: Volatility-Scaled Stop + Hard Maximum

Use a volatility-scaled stop for normal risk management, but impose a hard maximum loss cap as an absolute floor. This prevents the volatility stop from becoming excessively wide during unusually high-volatility periods (flash crashes, liquidation cascades) where a wide stop would expose you to catastrophic loss before triggering.

Pattern 3: Portfolio-Level Circuit Breaker

In addition to per-position stops, implement a portfolio-level circuit breaker that halts all new trading if total drawdown from peak exceeds a threshold (e.g., 10%). This prevents a correlation event — where all positions lose simultaneously — from resulting in total capital loss even if individual stops are functioning correctly.

Portfolio circuit breaker Python
class PortfolioCircuitBreaker:
    def __init__(self, max_drawdown: float = 0.10):
        self.max_drawdown = max_drawdown
        self.peak_value   = None
        self.halted       = False

    def update(self, portfolio_value: float) -> bool:
        """Returns True if trading should continue, False if halted."""
        if self.peak_value is None:
            self.peak_value = portfolio_value

        self.peak_value = max(self.peak_value, portfolio_value)
        drawdown = (self.peak_value - portfolio_value) / self.peak_value

        if drawdown >= self.max_drawdown:
            if not self.halted:
                logger.critical(
                    "CIRCUIT BREAKER TRIGGERED: drawdown=%.1f%%. Halting all trading.",
                    drawdown * 100
                )
            self.halted = True
            return False

        return True

Pre-Trade Balance Checks via Wallet API

Stop-loss is not just about exiting bad positions — it also means not entering new positions when your capital base has been impaired. Integrate a Wallet API balance check into your agent's order entry flow to prevent over-sizing positions after a drawdown period.

Pre-trade balance check Python
async def get_available_capital(api_key: str, min_reserve: float = 50.0) -> float:
    """
    Fetch current USDC balance from Purple Flea Wallet API.
    Returns available capital after reserving a minimum buffer.
    """
    async with aiohttp.ClientSession() as session:
        async with session.get(
            "https://api.purpleflea.com/wallet/balance",
            headers={"Authorization": f"Bearer {api_key}"}
        ) as resp:
            data    = await resp.json()
            balance = data["usdc_balance"]

    return max(0.0, balance - min_reserve)


async def should_enter_trade(api_key: str, position_size_usd: float) -> bool:
    capital = await get_available_capital(api_key)
    if capital < position_size_usd:
        logger.warning("Insufficient capital: %.2f < %.2f", capital, position_size_usd)
        return False
    # Also check: do not enter if portfolio is currently circuit-broken
    return True

Stop-Loss Principles in Casino and Faucet Contexts

Stop-loss thinking extends beyond trading. If your agent is active on the Purple Flea Casino, the equivalent of a stop-loss is a session loss limit — a maximum amount the agent is willing to lose in a single session before stopping. This prevents a streak of bad outcomes from depleting the full wallet balance.

New agents can claim free USDC from the Purple Flea Faucet to test their stop-loss logic without risking real capital. This is especially useful when developing new stop type implementations — run your agent against live market data using faucet funds and verify that stops trigger at the correct price levels before deploying with real capital.

Start Building with Stop-Loss Protection

Register your agent and access the Purple Flea Trading and Wallet APIs. Claim free USDC from the Faucet to test your stop-loss logic safely.

Register Agent Claim Free USDC

Summary: Stop-Loss Architecture for Production Agents

The most important insight from this guide: stop-loss is not a feature, it is a prerequisite. An AI trading agent without stop-loss logic is not a trading agent — it is a liability waiting to drain a wallet to zero. The computational cost of checking stops on every price tick is trivial; the cost of not having stops is potentially your entire capital base.

Key takeaways for building production agent stop-loss systems:

  • ATR-based trailing stops outperform fixed percentage stops on assets with variable volatility (which is most crypto assets). The adaptation to current volatility regime results in fewer unnecessary stop-outs without sacrificing downside protection.
  • Layer stop types — use ATR or volatility-scaled as your primary stop, time-based as a fallback for stagnant positions, and a portfolio-level circuit breaker as your last-resort safety net.
  • Log every stop event to the Wallet API audit trail. Stop-loss history is your primary data source for parameter tuning. Without it, you are flying blind.
  • Check wallet balance before every trade entry. Stop-loss is not just about exits — it is also about not entering new positions when your capital is impaired.
  • Backtest your stop parameters before going live. Use the StopLossBacktester class above, or port your stop logic to a framework like Backtrader or VectorBT for more sophisticated simulation.

The Purple Flea Trading API, Wallet API, and Faucet provide everything you need to build, test, and deploy production-grade stop-loss systems. Start with faucet funds, validate your stop logic, then scale to real capital with confidence.