Learning Hub Strategy Development Lesson 20
Phase 3 — Lesson 3 of 8
Hub
Phase 3 · Strategy Development · Lesson 20

Trade Simulation
& P&L

Simulate realistic P&L including brokerage, STT, exchange charges, GST, and slippage — using NSE's actual cost structure for Nifty F&O.

~45 min
NSE Cost Model
Phase 3 · Lesson 3 of 8
Phase 3 Progress38%

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.

CostRateApplied onNotes
Brokerage₹20/order flat (Zerodha)Each orderOr 0.03% for delivery
STT0.01% sell sideFutures sell0.1% for options exercise
Exchange charges0.0019%Both sidesNSE transaction charge
SEBI charges₹10 per croreBoth sidesNegligible for small traders
GST18% on brokerageBrokerageAuto-applied
Slippage0.5–2 pts NIFTYMarket ordersWider in illiquid hours
Round-trip cost ≈ 2 × (brokerage + STT + exchange + slippage)

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)

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.

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))