Automating stop-loss and target

Chapters 50-51 covered placing individual SL/SL-M orders. This chapter wires entry, stop-loss, and target into one managed unit your bot places automatically the moment an entry fills — closing the gap flagged back in chapter 47 about not sequencing dependent orders correctly.

class ManagedPosition:
    def __init__(self, kite, order_manager, exchange, symbol, side, qty, product, tick_size):
        self.kite = kite
        self.om = order_manager
        self.exchange, self.symbol, self.side, self.qty, self.product = exchange, symbol, side, qty, product
        self.tick_size = tick_size
        self.entry_order_id = None
        self.sl_order_id = None
        self.target_order_id = None

    def enter(self, entry_type="MARKET", limit_price=None):
        result = self.om.place(
            strategy_code="managed-pos", exchange=self.exchange, tradingsymbol=self.symbol,
            transaction_type=self.side, quantity=self.qty, product=self.product,
            order_type=entry_type, price=limit_price,
        )
        self.entry_order_id = result["order_id"]
        return result

    def wait_for_entry_fill(self, timeout_s=15) -> bool:
        status = poll_order_status(self.kite, self.entry_order_id, timeout_s=timeout_s)
        return status["status"] == "COMPLETE"

    def attach_stop_and_target(self, stop_trigger: float, target_price: float):
        exit_side = "SELL" if self.side == "BUY" else "BUY"
        filled_qty = kite_filled_quantity(self.kite, self.entry_order_id)

        sl_result = self.om.place(
            strategy_code="managed-pos-sl", exchange=self.exchange, tradingsymbol=self.symbol,
            transaction_type=exit_side, quantity=filled_qty, product=self.product,
            order_type="SL-M", trigger_price=round_to_tick(stop_trigger, self.tick_size),
        )
        self.sl_order_id = sl_result["order_id"]

        target_result = self.om.place(
            strategy_code="managed-pos-target", exchange=self.exchange, tradingsymbol=self.symbol,
            transaction_type=exit_side, quantity=filled_qty, product=self.product,
            order_type="LIMIT", price=round_to_tick(target_price, self.tick_size),
        )
        self.target_order_id = target_result["order_id"]

    def handle_oco(self):
        """Call periodically: if one exit leg filled, cancel the other (manual OCO, ch 55)."""
        sl_status = self.kite.order_history(self.sl_order_id)[-1]["status"]
        target_status = self.kite.order_history(self.target_order_id)[-1]["status"]
        if sl_status == "COMPLETE" and target_status not in ("COMPLETE", "CANCELLED"):
            self.om.cancel(self.target_order_id)
        elif target_status == "COMPLETE" and sl_status not in ("COMPLETE", "CANCELLED"):
            self.om.cancel(self.sl_order_id)

The sequencing that matters

  1. Place entry.
  2. Wait and confirm the fill (chapter 53) before proceeding.
  3. Only then compute stop/target off the *actual* filled quantity and average price (chapter 56) — not the originally intended quantity.
  4. Poll periodically for either exit leg completing, and cancel the sibling (manual OCO — chapter 59's native GTT OCO is an alternative for less time-sensitive positions).

Skipping step 2 is exactly the bug chapter 47 flagged: placing exits before confirming the entry actually happened, and for the size that actually happened.

Next: 076 — Trailing stop-loss