§ 1 Been a Few Days
Heads-down on microstructure strategies for a while now. Genuinely fun, genuinely frustrating - starting from scratch with no playbook, no mentor, just market data and a suspicion that there's something extractable at the tick level. Found a family of trigger-happy setups that do not lose roughly 90% of the time. Sharpes in double digits. Looked great until I remembered there are three states, not two.
§ 2 The Three-State Problem
Most people think in terms of win and loss. At the microstructure level there's a third state: the draw. A draw is when you correctly predict a 1-tick directional move - the model is right - but the spread width equals that 1 tick. You beat the spread by exactly nothing. Gross P&L: zero.
On paper that 45/45/10 split looks fine - you're right 90% of the time. In gross terms you end up flat on half your trades. The real kick in the teeth comes when you layer transaction costs on top. Draws become losses. Some wins become draws. The 65% that looked like acceptable churn is now net-negative territory. The strategy that didn't lose is now definitely losing.
§ 3 What the Cost Actually Is on NSE
The US version of this problem involves a broker fee and an exchange liquidity removal fee. India's version is structurally different and, at the tick level, considerably more hostile. There are six separate charges stacked on every round trip, and three of them are levied by the government regardless of whether the trade is profitable.
Take a liquid large-cap on NSE at ₹500/share, 1,000 shares, intraday. Tick size is ₹0.05. Here's where the money actually goes:
| Charge | Side | Rate | ₹ / share | Ticks |
|---|---|---|---|---|
| STT | Sell only | 0.025% | 0.1250 | 2.50 |
| Stamp Duty | Buy only | 0.015% | 0.0750 | 1.50 |
| NSE Exchange Txn | Both | 0.00297% each | 0.0297 | 0.59 |
| SEBI Turnover Fee | Both | 0.0001% each | 0.0010 | 0.02 |
| NSE IPFT | Both | 0.0001% each | 0.0010 | 0.02 |
| GST (18%) | On exchange + SEBI + IPFT | - | 0.0056 | 0.11 |
| Brokerage | Both | ₹20 flat / order | 0.0400 | 0.80 |
| Round-trip total | ₹0.2773 | 5.55 ticks | ||
The draw zone on NSE isn't 1 tick wide. It's 5.5 ticks wide. You need to capture at least a ₹0.28/share move just to break even. For a ₹500 stock that's a 0.056% move per trade - on a strategy designed to trade 1–2 tick setups. The math collapses before you've written a single line of strategy logic.
STT alone - 0.025% on sell-side turnover - costs 2.5 ticks per round trip. Stamp Duty on the buy side costs another 1.5. Together, those two government levies account for 4 of your 5.5-tick break-even before the exchange has taken a rupee. There's no way to negotiate or route around them.
§ 4 Futures Are No Escape
The knee-jerk response is to switch to Nifty futures where tick size is 0.5 points and contract value is larger - costs get diluted, right? Partially. But Budget 2026 had other ideas. From April 1st, STT on futures went from 0.02% to 0.05% on the sell side. A 150% jump.
On a single Nifty lot (75 units, ~₹24,000 index level, contract value ≈ ₹18 lakh), the round-trip cost breaks down roughly as:
| Charge | ₹ / lot |
|---|---|
| STT (0.05% on sell, Budget 2026) | 900 |
| Stamp Duty (0.002% on buy) | 36 |
| NSE Exchange Txn (0.00173% × 2) | 62 |
| SEBI + IPFT + GST | 19 |
| Brokerage (₹20 × 2) | 40 |
| Round-trip total | ≈ ₹1,057 |
Each Nifty point is worth ₹75. Break-even on a futures round trip is ~14 Nifty points. Scalping a 1–2 point move on Nifty futures isn't a strategy, it's a donation. The 3× STT hike on futures this year essentially killed the tick-scalp trade at the retail level.
§ 5 The Rejected Jump Shot
Here's what's actually happening in the strategy. The model correctly identifies a 1-tick directional move. Price ticks up. But within the same bar - sometimes within milliseconds - mean reversion kicks in. Everyone who was waiting on the first uptick jumps on it, sells into the move, and the price is knocked straight back down. In basketball terms: a clean read of the defence, a good jump shot, rejected at the rim by the shot blocker who was already positioned.
The gross P&L on a rejected jump shot is zero - the model predicted correctly. The net P&L after 5.5 ticks of transaction cost is deeply negative. And because these rejected shots happen roughly 45% of the time, the cost bleeds are relentless. The strategy fails not because the signal is wrong but because statutory costs alone are wide enough to swallow the available profit on the correctly predicted move.
§ 6 What I Am Not Sure About
Whether any tick-level equity strategy survives India's cost structure. The STT + stamp duty combination is genuinely punishing relative to the available profit on a 1–3 tick move. I've not found published evidence of retail participants profitably scalping NSE equities at this frequency. That absence might mean it's impossible, or it might mean nobody who can do it is writing about it.
Whether the classifier problem is tractable. The difference between a held move and a rejected one may not be cleanly separable from publicly available data. If the rejected shots are driven by latency-advantaged participants reacting faster than any signal can capture, filtering for them from the retail data feed might be fundamentally underdetermined.
Whether futures make sense post-Budget 2026. The STT hike to 0.05% substantially narrows the window for intraday futures strategies. Options have their own cost dynamics - options STT is now 0.15% on premium, up from 0.10% - and the tick structure is non-linear. Neither segment is obviously better than equity cash for a tick-level approach. The search continues.
...the expedition continues.
§ A Appendix - Cost Calculator (Python)
The numbers in this post come from a small utility I keep updated whenever SEBI or the exchanges revise their fee schedules. Rates below reflect the post-Budget 2026 regime (effective April 1, 2026). Drop it into any backtest loop and call it on each round trip to get an honest cost figure rather than a hand-wavy estimate.
python - transaction_costs.py · rates current as of 01-04-2026# =============================================================================
# transaction_costs.py
#
# Statutory + exchange transaction cost calculator for Indian markets (NSE).
# Covers equity cash (delivery & intraday), equity futures, and equity options.
#
# Key rate changes since the previous version (04-02-2025):
# • STT on futures sell side: 0.02% → 0.05% [Budget 2026, Apr 1 2026]
# • STT on options sell side: 0.10% → 0.15% [Budget 2026, Apr 1 2026]
# • NSE IPFT on options: ₹0.01/cr → ₹50/cr (0.000001% → 0.000005%)
#
# Usage:
# buy_ch = calculate_transaction_charges('buy', segment, buy_value, 0)
# sell_ch = calculate_transaction_charges('sell', segment, 0, sell_value)
# total = sum(buy_ch.values()) + sum(sell_ch.values())
# =============================================================================
def calculate_transaction_charges(trade_type, segment, buy_value, sale_value,
trade_category='intraday'):
"""
Return a dict of itemised statutory charges for one side of a trade.
Parameters
----------
trade_type : 'buy' or 'sell'
segment : 'equity', 'futures', or 'options'
buy_value : total buy-side transaction value in ₹ (pass 0 on sell calls)
sale_value : total sell-side transaction value in ₹ (pass 0 on buy calls)
trade_category : 'intraday' or 'delivery' - relevant only for equity segment
Returns
-------
dict - keys: exchange_txn_charge, stt, stamp_duty, sebi_charge,
ipf_charge, gst
"""
charges = {}
# Total turnover for this call - used by charges levied on both sides
turnover = buy_value + sale_value
# ── 1. NSE Exchange Transaction Charge ───────────────────────────────────
# Flat uniform rate per NSE circular (Oct 1, 2024 "true-to-label" mandate).
# Equity cash: ₹2.97 per lakh each side → 0.00297%
# Futures: ₹1.73 per lakh each side → 0.00173%
# Options: ₹35.03 per lakh of premium → 0.03503%
if segment == 'equity':
charges['exchange_txn_charge'] = turnover * 0.0000297
elif segment == 'futures':
charges['exchange_txn_charge'] = turnover * 0.0000173
elif segment == 'options':
charges['exchange_txn_charge'] = turnover * 0.0003503 # applied on premium value
# ── 2. Securities Transaction Tax (STT) ──────────────────────────────────
# Government levy - no routing around it.
#
# Equity delivery: 0.1% on BOTH buy and sell (unchanged)
# Equity intraday: 0.025% on sell side only (unchanged)
# Futures sell: 0.05% ← raised from 0.02% by Budget 2026
# Options sell: 0.15% on premium ← raised from 0.10% by Budget 2026
if trade_type == 'sell':
if segment == 'equity':
if trade_category == 'intraday':
charges['stt'] = sale_value * 0.00025 # 0.025% sell-side intraday
else:
charges['stt'] = sale_value * 0.001 # 0.1% sell-side delivery
elif segment == 'futures':
charges['stt'] = sale_value * 0.0005 # 0.05% - Budget 2026 rate
elif segment == 'options':
charges['stt'] = sale_value * 0.0015 # 0.15% on premium - Budget 2026
elif trade_type == 'buy':
if segment == 'equity' and trade_category == 'delivery':
charges['stt'] = buy_value * 0.001 # 0.1% buy-side delivery
else:
charges['stt'] = 0 # no STT on buy for F&O / intraday
# ── 3. Stamp Duty ─────────────────────────────────────────────────────────
# Charged on buy side only. Uniform rate across all Indian states since 2020.
# Equity: 0.015% on buy value
# Futures: 0.002% on buy value
# Options: 0.003% on buy value (on premium paid)
stamp_duty_rates = {'equity': 0.00015, 'futures': 0.00002, 'options': 0.00003}
if trade_type == 'buy':
charges['stamp_duty'] = buy_value * stamp_duty_rates.get(segment, 0)
else:
charges['stamp_duty'] = 0
# ── 4. SEBI Turnover Fee ──────────────────────────────────────────────────
# ₹10 per crore of turnover (both sides) → 0.0001%. Unchanged.
charges['sebi_charge'] = turnover * 0.000001
# ── 5. NSE Investor Protection Fund Trust (IPFT) ─────────────────────────
# Revised per NSE circular:
# Equity & Futures: ₹10 per crore → 0.000001 (0.0001%) - unchanged
# Options: ₹50 per crore → 0.000005 (0.0005%) - updated
if segment == 'options':
charges['ipf_charge'] = turnover * 0.000005
else:
charges['ipf_charge'] = turnover * 0.000001
# ── 6. GST at 18% ─────────────────────────────────────────────────────────
# Applied on: Exchange Txn Charge + SEBI Charge + IPFT Charge.
# Brokerage GST is handled separately by the broker and excluded here.
gst_base = (charges['exchange_txn_charge']
+ charges['sebi_charge']
+ charges['ipf_charge'])
charges['gst'] = gst_base * 0.18
return charges
# =============================================================================
# Example: ₹500 stock, 1,000 shares, NSE equity intraday round trip
# =============================================================================
if __name__ == '__main__':
buy_price = 500 # ₹ per share
sell_price = 500.30 # ₹ per share - capturing ~6 ticks (₹0.05 tick size)
volume = 1_000 # shares
segment = 'equity'
category = 'intraday'
buy_value = buy_price * volume # ₹ 5,00,000
sell_value = sell_price * volume # ₹ 5,00,300
# Calculate buy-side and sell-side charges independently -
# this matters because STT and stamp duty apply asymmetrically.
buy_charges = calculate_transaction_charges('buy', segment, buy_value, 0, category)
sell_charges = calculate_transaction_charges('sell', segment, 0, sell_value, category)
total_charges = sum(buy_charges.values()) + sum(sell_charges.values())
gross_pnl = sell_value - buy_value
net_pnl = gross_pnl - total_charges
# ── per-share cost breakdown ──────────────────────────────────────────────
cost_per_share = total_charges / volume
tick_size = 0.05
break_even_ticks = cost_per_share / tick_size
print("=" * 58)
print(" TRANSACTION COST SUMMARY - NSE EQUITY INTRADAY")
print("=" * 58)
print(f" Gross P&L : ₹{gross_pnl:,.2f}")
print(f" Total charges : ₹{total_charges:,.2f}")
print(f" Net P&L : ₹{net_pnl:,.2f}")
print(f" Cost per share : ₹{cost_per_share:.4f}")
print(f" Break-even (ticks) : {break_even_ticks:.2f}")
print("-" * 58)
print(" Buy-side charges:")
for k, v in buy_charges.items():
print(f" {k:<22}: ₹{v:.4f}")
print(" Sell-side charges:")
for k, v in sell_charges.items():
print(f" {k:<22}: ₹{v:.4f}")
print("=" * 58)
Running the example above at a 6-tick profit target (₹0.30/share on a ₹500 stock) produces a net positive P&L - but only just. Drop to 5 ticks and the trade is net-zero. Drop to 4 ticks and the statutory charges eat you alive. That's the black hole: it's invisible in gross P&L, relentless in net.