Paper trading mode

Zerodha's Kite Connect has no built-in sandbox — you build your own paper-trading simulator that plugs into the exact same BrokerAdapter interface (chapter 68) as the real broker, so strategy code is identical between paper and live.

# brokers/paper_adapter.py
import uuid

class PaperAdapter(BrokerAdapter):
    def __init__(self, price_cache: PriceCache, starting_cash: float = 100000):
        self.price_cache = price_cache
        self.cash = starting_cash
        self.positions: dict[str, dict] = {}
        self.order_log: list[dict] = []

    def place_order(self, exchange, symbol, side, qty, product, order_type, price=None, trigger_price=None, tag=None) -> str:
        order_id = str(uuid.uuid4())
        fill_price = self._simulate_fill_price(exchange, symbol, side, order_type, price)
        self._apply_fill(symbol, side, qty, fill_price)
        self.order_log.append({
            "order_id": order_id, "symbol": symbol, "side": side, "qty": qty,
            "intended_price": price or fill_price, "filled_price": fill_price, "tag": tag,
        })
        return order_id

    def _simulate_fill_price(self, exchange, symbol, side, order_type, price):
        token = resolve_token(exchange, symbol)   # from chapter 22
        ltp = self.price_cache.get_price(token)
        if order_type == "MARKET":
            slippage = ltp * 0.0005   # realistic assumption, chapter 86 — calibrate from real data
            return ltp + slippage if side == "BUY" else ltp - slippage
        return price   # LIMIT — assume fills exactly at limit price (optimistic, see caveat below)

    def _apply_fill(self, symbol, side, qty, price):
        direction = 1 if side == "BUY" else -1
        self.cash -= direction * qty * price
        pos = self.positions.setdefault(symbol, {"quantity": 0, "avg_price": 0})
        # simplified average-price update logic
        pos["quantity"] += direction * qty

    def cancel_order(self, order_id): pass   # paper orders fill instantly in this simple model
    def get_positions(self): return [{"symbol": s, **p} for s, p in self.positions.items() if p["quantity"] != 0]
    def get_margins(self): return {"available_cash": self.cash, "used": 0}

The honesty problem with paper trading — and how to fix it

A naive paper simulator that fills every limit order instantly at the requested price is systematically optimistic — real limit orders often don't fill at all, or fill only partially, especially at aggressive prices. A more honest simulator checks whether the market actually traded through your limit price using the same historical/live tick data before marking it filled:

def would_limit_have_filled(order_side: str, limit_price: float, subsequent_ltp_series: list[float]) -> bool:
    if order_side == "BUY":
        return any(p <= limit_price for p in subsequent_ltp_series)
    return any(p >= limit_price for p in subsequent_ltp_series)

Why paper trading is a distinct, necessary stage — not optional

It validates the *entire pipeline* — auth, data feed, signal logic, order sequencing, risk checks — end to end, without financial risk, before any real capital is at stake. A strategy that looks great in backtest but has a bug in live order sequencing (e.g. chapter 47's dependent-order ordering mistake) will surface here, not in the backtest, because paper trading runs the actual live code path.

What paper trading cannot validate

Real fill quality under real market impact, real broker API behavior under load/rate-limits, and your own emotional discipline (chapter 98) watching real money move — paper trading is necessary but not sufficient; plan a small-real-capital phase after it, not a direct jump to full size.

Next: 088 — From backtest to live: the strategy state machine