Building an Order Manager abstraction

Chapters 43-66 covered raw API calls. A real bot shouldn't scatter kite.place_order() calls through strategy code — wrap them behind an OrderManager that centralizes idempotency (ch 58), tagging (ch 65), retry/error handling (ch 57), and reconciliation (ch 66) in one place.

# orders/manager.py
import logging, time, uuid
from kiteconnect.exceptions import OrderException, NetworkException

class OrderManager:
    def __init__(self, kite):
        self.kite = kite
        self.local_orders: dict[str, dict] = {}   # order_id -> our record

    def place(self, strategy_code: str, **params) -> dict:
        tag = params.pop("tag", None) or f"{strategy_code}-{uuid.uuid4().hex[:12]}"
        params["tag"] = tag

        try:
            order_id = self._place_with_retry(**params)
        except OrderException as e:
            logging.error(f"Order rejected: {e}")
            return {"success": False, "order_id": None, "reason": str(e)}

        self.local_orders[order_id] = {"strategy": strategy_code, "params": params, "status": "PENDING"}
        return {"success": True, "order_id": order_id, "reason": None}

    def _place_with_retry(self, **params, max_attempts=2):
        tag = params["tag"]
        for attempt in range(max_attempts):
            try:
                return self.kite.place_order(**params)
            except NetworkException:
                existing = [o for o in self.kite.orders() if o.get("tag") == tag]
                if existing:
                    return existing[0]["order_id"]
                if attempt == max_attempts - 1:
                    raise
                time.sleep(1)

    def cancel(self, order_id: str) -> bool:
        order = next((o for o in self.kite.orders() if o["order_id"] == order_id), None)
        if not order:
            return False
        try:
            self.kite.cancel_order(variety=order["variety"], order_id=order_id)
            return True
        except OrderException:
            return False

    def sync_status(self):
        """Refresh local records against broker truth — call periodically."""
        broker_orders = {o["order_id"]: o for o in self.kite.orders()}
        for order_id, record in self.local_orders.items():
            if order_id in broker_orders:
                record["status"] = broker_orders[order_id]["status"]
                record["filled_quantity"] = broker_orders[order_id]["filled_quantity"]

    def open_orders_for(self, strategy_code: str) -> list[dict]:
        return [
            {**rec, "order_id": oid} for oid, rec in self.local_orders.items()
            if rec["strategy"] == strategy_code and rec["status"] in ("PENDING", "OPEN", "TRIGGER PENDING")
        ]

Why centralize this instead of leaving it in strategy code

Strategy code (chapters 79+) should express *decisions* ("enter long here," "exit here") — not retry loops, tag generation, or exception handling boilerplate. A single, well-tested OrderManager means every strategy gets idempotency and error handling for free, and a bug fix here (e.g. a new rejection reason to handle) benefits every strategy at once instead of needing to be patched into N copy-pasted order-placement blocks.

Next: 068 — Broker-agnostic order interface