Handling circuit limits and illiquid symbols

Indian exchanges impose daily price bands ("circuit limits") on most stocks — typically 2%, 5%, 10%, or 20% from the previous close, varying by stock category and volatility classification. An order priced outside this band is rejected outright (chapter 57's "Price out of range").

Reading circuit limits from quote data

quote = kite.quote(["NSE:SMALLCAPXYZ"])["NSE:SMALLCAPXYZ"]
upper_circuit = quote["upper_circuit_limit"]
lower_circuit = quote["lower_circuit_limit"]
def validate_price_within_circuit(price: float, quote: dict) -> bool:
    return quote["lower_circuit_limit"] <= price <= quote["upper_circuit_limit"]

The real risk: a stock frozen at circuit, with no liquidity

When a stock hits its circuit limit (up or down), trading effectively freezes at that price — the order book shows massive quantity waiting on one side and near-zero on the other. A market order to sell into an upper-circuit-frozen stock, or buy into a lower-circuit-frozen one, will simply not fill (no matching counterparty), regardless of price willingness.

def is_effectively_frozen(depth: dict, side: str) -> bool:
    """side: the side you'd need liquidity FROM — 'sell' depth if you're buying, etc."""
    opposite_side = depth[side]
    return sum(level["quantity"] for level in opposite_side) == 0

Practical rules for a bot trading small/mid-caps

  1. Check circuit limits before placing any order on anything outside large-cap/index F&O — never assume price bands are wide enough not to matter.
  2. Check depth, not just LTP, before sizing a position in a low-volume name — a wide, thin book means your exit plan (stop-loss, target) may not be executable at the price you modeled, regardless of where the price band sits.
  3. **Avoid strategies that rely on getting *out* of illiquid names on demand** — if the stop-loss can't fill because the stock has gapped through a circuit limit, your risk management has silently failed despite the order existing correctly on paper.
def liquidity_screen(kite, exchange: str, symbol: str, min_avg_volume: int) -> bool:
    quote = kite.quote([f"{exchange}:{symbol}"])[f"{exchange}:{symbol}"]
    return quote["volume"] > min_avg_volume

Apply a liquidity floor as a pre-trade filter in your universe selection (chapter 79+), not as an afterthought once you're already holding an illiquid position you can't exit cleanly.

Next: 078 — Margin-aware order sizing