Trailing stop-loss

A trailing stop moves in the profitable direction as price advances, locking in gains, but never moves backward. Chapter 54 introduced the core modify logic and its critical one-directional guard — this chapter builds the full loop that drives it off live prices.

class TrailingStop:
    def __init__(self, kite, order_manager, sl_order_id, side, trail_distance, tick_size):
        self.kite = kite
        self.om = order_manager
        self.sl_order_id = sl_order_id
        self.side = side                  # "BUY" or "SELL" — the ORIGINAL position side
        self.trail_distance = trail_distance
        self.tick_size = tick_size
        self.best_trigger = None

    def update(self, current_price: float):
        if self.side == "BUY":
            new_trigger = round_to_tick(current_price - self.trail_distance, self.tick_size)
            if self.best_trigger is None or new_trigger > self.best_trigger:
                self._apply(new_trigger)
        else:   # short position, stop trails downward as price falls
            new_trigger = round_to_tick(current_price + self.trail_distance, self.tick_size)
            if self.best_trigger is None or new_trigger < self.best_trigger:
                self._apply(new_trigger)

    def _apply(self, new_trigger: float):
        try:
            self.kite.modify_order(variety="regular", order_id=self.sl_order_id, trigger_price=new_trigger)
            self.best_trigger = new_trigger
            logging.info(f"Trailed stop to {new_trigger}")
        except OrderException as e:
            logging.warning(f"Trail modify failed (order may have filled): {e}")

Drive it from your live price cache (chapter 42), not a tight polling loop against the REST API:

def run_trailing_stop(trailing_stop: TrailingStop, price_cache: PriceCache, token: int):
    while True:
        price = price_cache.get_price(token)
        if price is not None:
            trailing_stop.update(price)
        time.sleep(1)   # trail check frequency — no need to check every tick

ATR-based trailing distance — adapts to volatility like chapter 72's sizing

def atr_trailing_distance(atr: float, multiplier: float = 1.5) -> float:
    return atr * multiplier

A fixed-point trail distance suffers the same problem chapter 72 raised for stops: too tight and normal noise stops you out on every pullback, too wide and you give back too much profit before it kicks in.

Modify frequency — don't hammer the API on every tiny tick

Trailing on every single tick both wastes API calls and can cause excessive stop movement chasing noise. Update on a fixed interval (e.g. every few seconds) or only when price has moved a meaningful increment since the last trail update — whichever fits your strategy's timeframe.

Next: 077 — Handling circuit limits and illiquid symbols