Modeling slippage and transaction costs
The direct answer to "how much does live execution differ from backtest numbers" — the single biggest gap between a good-looking backtest and a disappointing live strategy is usually here, not in the signal logic.
Every real cost component for Indian equity/F&O
STT was hiked effective April 1, 2026 (this is now in effect, not a future change): futures STT rose from 0.02% to 0.05% (a 150% increase), options STT from 0.10% to 0.15% on premium (and 0.125% to 0.15% on exercise) — part of a deliberate push to curb retail F&O speculation. This directly cuts into thin-edge intraday/options strategies more than it did before — re-run chapter 82's backtests with these updated rates before trusting any pre-2026 performance number.
def compute_transaction_costs(turnover: float, segment: str, is_buy: bool) -> dict:
"""Rates as of April 2026 (post-hike) — verify current values before relying on this long-term,
these are exactly the numbers that just changed once and will change again."""
brokerage = min(20, turnover * 0.0003) # many discount brokers: flat fee or 0.03%, whichever lower
stt_rates = {
"equity_delivery": 0.001, # unchanged in the 2026 hike — delivery STT applies on both buy and sell
"equity_intraday": 0.00025, # sell side only, unchanged
"futures": 0.0005, # hiked from 0.0002 (0.02% -> 0.05%), effective April 1, 2026
"options_premium": 0.0015, # hiked from 0.0010 (0.10% -> 0.15%), effective April 1, 2026
}
stt = turnover * stt_rates.get(segment, 0.0005)
exchange_txn_charge = turnover * 0.0000345 # NSE, approx
gst = (brokerage + exchange_txn_charge) * 0.18
sebi_charges = turnover * 0.0000010
stamp_duty = turnover * 0.00003 if is_buy else 0 # stamp duty only on buy side
total = brokerage + stt + exchange_txn_charge + gst + sebi_charges + stamp_duty
return {"brokerage": brokerage, "stt": stt, "gst": gst, "total": total}
These exact rates change (STT rates, exchange charges, GST are all subject to regulatory revision) — treat this function as a template to keep updated from your broker's current charges page, not a permanent constant.
Slippage — the harder, more important cost to model honestly
Unlike fixed statutory charges, slippage depends on your order type, instrument liquidity, and order size relative to depth (chapter 26).
def model_slippage(order_type: str, depth: dict, qty: int) -> float:
"""Returns estimated slippage in price terms (not percentage)."""
if order_type == "LIMIT":
return 0.0 # assumes limit fully fills at your price — optimistic if size > depth
# MARKET order: walk the book (chapter 26's estimate_market_buy_cost pattern)
best_price = depth["sell"][0]["price"]
avg_fill_price = estimate_market_buy_cost(depth, qty)
return avg_fill_price - best_price
Backtest slippage assumption should come from real data, not a guess
def calibrate_slippage_from_live_trades(order_log: list[dict]) -> float:
"""order_log: your own recorded {intended_price, filled_price} pairs from actual live/paper trades."""
slippages = [abs(o["filled_price"] - o["intended_price"]) / o["intended_price"] for o in order_log]
return sum(slippages) / len(slippages) if slippages else 0.001 # fallback default
Run the strategy in paper mode (chapter 87) or with small real size first, log actual slippage experienced, and feed that measured number back into your backtest assumptions — don't backtest with a slippage assumption pulled from nowhere.
Why this single modeling choice can flip a strategy from profitable to not
A high-frequency mean-reversion strategy with a small per-trade edge (say, 0.3% expected gain per trade) can be entirely consumed by realistic costs (0.1-0.2% round-trip in fees plus slippage) — a backtest run with zero-cost assumptions will look robustly profitable while the live version barely breaks even or loses. Always re-run your backtest with realistic costs (chapter 82's fees= and slippage= parameters) before trusting any performance number.
Next: 087 — Paper trading mode