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