Cost Model
Real-World Trading Costs on NSE
Most beginner backtests ignore costs entirely, making results look 2–5× better than reality. For an intraday NIFTY futures strategy, total round-trip cost is typically 0.05–0.08% of trade value.
| Cost | Rate | Applied on | Notes |
|---|---|---|---|
| Brokerage | ₹20/order flat (Zerodha) | Each order | Or 0.03% for delivery |
| STT | 0.01% sell side | Futures sell | 0.1% for options exercise |
| Exchange charges | 0.0019% | Both sides | NSE transaction charge |
| SEBI charges | ₹10 per crore | Both sides | Negligible for small traders |
| GST | 18% on brokerage | Brokerage | Auto-applied |
| Slippage | 0.5–2 pts NIFTY | Market orders | Wider in illiquid hours |
Round-trip cost ≈ 2 × (brokerage + STT + exchange + slippage)
Implementation
CostModel Class
PYTHONcost_model.py
from dataclasses import dataclass @dataclass class CostModel: # Zerodha F&O flat brokerage (per order) brokerage_per_order : float = 20.0 # NSE charges (fraction of trade value) stt_pct : float = 0.0001 # 0.01% sell side exchange_pct : float = 0.000019 # 0.0019% both sides sebi_pct : float = 0.000001 # tiny gst_on_brokerage : float = 0.18 # Slippage in points (instrument-specific) slippage_pts : float = 1.0 # per side (0.5 for liquid contracts) def round_trip_cost( self, entry_price : float, exit_price : float, lot_size : int, lots : int = 1, ) -> float: """Total cost in ₹ for one round-trip trade.""" entry_val = entry_price * lot_size * lots exit_val = exit_price * lot_size * lots brok = self.brokerage_per_order * 2 # entry + exit gst = brok * self.gst_on_brokerage stt = exit_val * self.stt_pct # sell side only exch = (entry_val + exit_val) * self.exchange_pct sebi = (entry_val + exit_val) * self.sebi_pct slip = self.slippage_pts * 2 * lot_size * lots return brok + gst + stt + exch + sebi + slip def cost_in_points(self, price: float, lot_size: int, lots: int = 1) -> float: """Express round-trip cost as index points (useful for net P&L calc).""" cost_rs = self.round_trip_cost(price, price, lot_size, lots) return cost_rs / (lot_size * lots)
P&L Engine
TradeSimulator — Net P&L Per Trade
The simulator takes a raw trade log (entry/exit prices) and computes net P&L after all costs. This feeds directly into the performance metrics in L22.
PYTHONtrade_simulator.py
import pandas as pd from cost_model import CostModel class TradeSimulator: def __init__( self, lot_size : int = 50, lots : int = 1, initial_capital: float = 100_000, cost_model : CostModel = None, ): self.lot_size = lot_size self.lots = lots self.initial_capital = initial_capital self.cost = cost_model or CostModel() def simulate(self, trades: pd.DataFrame) -> pd.DataFrame: """ Input: trades df with columns: entry_time, exit_time, direction, entry_price, exit_price Output: same df with added columns: gross_pnl_pts, cost_pts, net_pnl_pts, net_pnl_rs, cum_equity """ df = trades.copy() sign = df["direction"].map({"LONG": 1, "SHORT": -1}) # Gross P&L in index points df["gross_pnl_pts"] = sign * (df["exit_price"] - df["entry_price"]) # Cost in points (round-trip) df["cost_pts"] = df.apply( lambda r: self.cost.cost_in_points( r["entry_price"], self.lot_size, self.lots ), axis=1 ) # Net P&L df["net_pnl_pts"] = df["gross_pnl_pts"] - df["cost_pts"] df["net_pnl_rs"] = df["net_pnl_pts"] * self.lot_size * self.lots # Cumulative equity curve df["cum_equity"] = self.initial_capital + df["net_pnl_rs"].cumsum() return df def summary(self, sim: pd.DataFrame) -> dict: winners = sim[sim["net_pnl_rs"] > 0] losers = sim[sim["net_pnl_rs"] < 0] return { "total_trades" : len(sim), "winners" : len(winners), "losers" : len(losers), "win_rate" : round(len(winners)/len(sim)*100, 1), "gross_pnl" : round(sim["net_pnl_rs"].sum(), 2), "avg_winner" : round(winners["net_pnl_rs"].mean(), 2), "avg_loser" : round(losers["net_pnl_rs"].mean(), 2), "profit_factor" : round( winners["net_pnl_rs"].sum() / abs(losers["net_pnl_rs"].sum()), 2 ) if len(losers) > 0 else float("inf"), "final_equity" : round(sim["cum_equity"].iloc[-1], 2), }
Profit Factor interpretation
Profit Factor = Gross Profit ÷ Gross Loss. A value above 1.5 is acceptable; above 2.0 is good. Values below 1.0 mean the strategy is a net loser after costs.
Usage
Putting It Together
PYTHONrun_simulation.py
from backtest_engine import BacktestEngine from trade_simulator import TradeSimulator from cost_model import CostModel # 1. Run backtest to get trade log engine = BacktestEngine() result = engine.run(df_nifty, ema_cross_signal) # 2. Simulate realistic P&L sim = TradeSimulator( lot_size=50, lots=1, initial_capital=100_000, cost_model=CostModel(slippage_pts=1.5) # conservative ) trade_df = sim.simulate(result.trades) print(sim.summary(trade_df))
