Understanding Market Microstructure in Indian Markets
Deep dive into order books, bid-ask spreads, market impact, and liquidity dynamics specific to NSE and BSE trading.
You place a market order for 1000 shares of Reliance. The quote shows ₹2,450 bid and ₹2,451 ask. But your order fills at ₹2,452.50. What happened? Welcome to market microstructure—the hidden mechanics of how orders actually execute.
Understanding market microstructure is crucial for algorithmic traders in India. It’s the difference between profitable strategies and those killed by execution costs.
Part 1: The Order Book
Anatomy of NSE Order Book
import pandas as pd
from datetime import datetime
class OrderBook:
"""
Simulate NSE order book structure
"""
def __init__(self, symbol: str):
self.symbol = symbol
self.bids = [] # Buy orders (price, quantity, timestamp)
self.asks = [] # Sell orders (price, quantity, timestamp)
self.trades = []
def add_order(self, side: str, price: float, quantity: int):
"""Add order to book"""
timestamp = datetime.now()
order = {'price': price, 'quantity': quantity, 'timestamp': timestamp}
if side == 'BUY':
self.bids.append(order)
# Sort bids: highest price first
self.bids.sort(key=lambda x: x['price'], reverse=True)
else:
self.asks.append(order)
# Sort asks: lowest price first
self.asks.sort(key=lambda x: x['price'])
# Try to match orders
self.match_orders()
def match_orders(self):
"""Match buy and sell orders"""
while self.bids and self.asks:
best_bid = self.bids[0]
best_ask = self.asks[0]
# If bid price >= ask price, we have a match
if best_bid['price'] >= best_ask['price']:
# Execute at ask price (price-time priority)
trade_price = best_ask['price']
trade_qty = min(best_bid['quantity'], best_ask['quantity'])
# Record trade
self.trades.append({
'price': trade_price,
'quantity': trade_qty,
'timestamp': datetime.now()
})
# Update quantities
best_bid['quantity'] -= trade_qty
best_ask['quantity'] -= trade_qty
# Remove filled orders
if best_bid['quantity'] == 0:
self.bids.pop(0)
if best_ask['quantity'] == 0:
self.asks.pop(0)
else:
break
def get_market_depth(self, levels: int = 5) -> dict:
"""
Get market depth (L2 data)
Similar to NSE Market Depth view
"""
return {
'bids': self.bids[:levels],
'asks': self.asks[:levels],
'bid_price': self.bids[0]['price'] if self.bids else None,
'ask_price': self.asks[0]['price'] if self.asks else None,
'spread': (self.asks[0]['price'] - self.bids[0]['price'])
if self.bids and self.asks else None
}
def calculate_vwap(self, quantity: int, side: str) -> float:
"""
Calculate VWAP for given quantity
Useful for estimating execution price
"""
orders = self.asks if side == 'BUY' else self.bids
total_cost = 0
total_qty = 0
for order in orders:
qty_from_level = min(quantity - total_qty, order['quantity'])
total_cost += qty_from_level * order['price']
total_qty += qty_from_level
if total_qty >= quantity:
break
if total_qty < quantity:
return None # Insufficient liquidity
return total_cost / total_qty
# Example: Build order book
book = OrderBook('RELIANCE')
# Add some orders
book.add_order('BUY', 2450, 100)
book.add_order('BUY', 2449, 200)
book.add_order('BUY', 2448, 150)
book.add_order('SELL', 2451, 100)
book.add_order('SELL', 2452, 200)
book.add_order('SELL', 2453, 150)
# Check market depth
depth = book.get_market_depth()
print(f"Best Bid: ₹{depth['bid_price']}")
print(f"Best Ask: ₹{depth['ask_price']}")
print(f"Spread: ₹{depth['spread']}")
# Estimate execution price for 250 shares
vwap = book.calculate_vwap(250, 'BUY')
print(f"VWAP for 250 shares: ₹{vwap:.2f}")
Market Depth Visualization
import matplotlib.pyplot as plt
def visualize_order_book(book: OrderBook):
"""
Visualize order book depth
"""
depth = book.get_market_depth(levels=10)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
# Bids (green)
if depth['bids']:
bid_prices = [b['price'] for b in depth['bids']]
bid_qtys = [b['quantity'] for b in depth['bids']]
ax1.barh(bid_prices, bid_qtys, color='#00ff00', alpha=0.7)
ax1.set_xlabel('Quantity')
ax1.set_ylabel('Price (₹)')
ax1.set_title('Bids (Buy Orders)', fontweight='bold')
ax1.grid(True, alpha=0.3)
# Asks (red)
if depth['asks']:
ask_prices = [a['price'] for a in depth['asks']]
ask_qtys = [a['quantity'] for a in depth['asks']]
ax2.barh(ask_prices, ask_qtys, color='#ff0000', alpha=0.7)
ax2.set_xlabel('Quantity')
ax2.set_ylabel('Price (₹)')
ax2.set_title('Asks (Sell Orders)', fontweight='bold')
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
visualize_order_book(book)
Part 2: Bid-Ask Spread Analysis
Spread Dynamics in Indian Markets
class SpreadAnalyzer:
"""
Analyze bid-ask spread patterns
"""
def __init__(self):
self.spread_data = []
def calculate_spread_metrics(
self,
bid: float,
ask: float,
mid_price: float = None
) -> dict:
"""
Calculate various spread metrics
"""
if mid_price is None:
mid_price = (bid + ask) / 2
# Absolute spread
absolute_spread = ask - bid
# Relative spread (%)
relative_spread = (absolute_spread / mid_price) * 100
# Effective spread (for actual trades)
# Typically half of quoted spread
effective_spread = absolute_spread / 2
return {
'absolute_spread': absolute_spread,
'relative_spread_pct': relative_spread,
'effective_spread': effective_spread,
'mid_price': mid_price
}
def estimate_liquidity_cost(
self,
quantity: int,
bid: float,
ask: float,
side: str
) -> dict:
"""
Estimate cost of immediate execution
"""
mid_price = (bid + ask) / 2
if side == 'BUY':
execution_price = ask
slippage = execution_price - mid_price
else:
execution_price = bid
slippage = mid_price - execution_price
total_cost = quantity * slippage
cost_pct = (slippage / mid_price) * 100
return {
'execution_price': execution_price,
'mid_price': mid_price,
'slippage_per_share': slippage,
'total_cost': total_cost,
'cost_pct': cost_pct
}
def analyze_spread_by_time(self, market_data: pd.DataFrame):
"""
Analyze spread variation throughout trading day
Spreads typically:
- Wider at market open (9:15-9:30)
- Tightest during peak hours (11:00-14:00)
- Wider near close (15:15-15:30)
"""
market_data['hour'] = market_data.index.hour
market_data['spread'] = market_data['ask'] - market_data['bid']
market_data['spread_pct'] = (
market_data['spread'] /
((market_data['bid'] + market_data['ask']) / 2)
) * 100
hourly_spread = market_data.groupby('hour')['spread_pct'].agg(['mean', 'std'])
return hourly_spread
# Example usage
analyzer = SpreadAnalyzer()
# Morning trade (wider spread)
morning_cost = analyzer.estimate_liquidity_cost(
quantity=100,
bid=2450,
ask=2452, # ₹2 spread
side='BUY'
)
print("Morning Trade (9:20 AM):")
print(f"Slippage: ₹{morning_cost['slippage_per_share']:.2f} per share")
print(f"Total Cost: ₹{morning_cost['total_cost']:.2f}")
print(f"Cost: {morning_cost['cost_pct']:.4f}%")
# Midday trade (tighter spread)
midday_cost = analyzer.estimate_liquidity_cost(
quantity=100,
bid=2450,
ask=2450.50, # ₹0.50 spread
side='BUY'
)
print("\nMidday Trade (12:00 PM):")
print(f"Slippage: ₹{midday_cost['slippage_per_share']:.2f} per share")
print(f"Total Cost: ₹{midday_cost['total_cost']:.2f}")
print(f"Cost: {midday_cost['cost_pct']:.4f}%")
Spread Classification by Stock
def classify_liquidity(symbol: str, avg_spread_pct: float) -> str:
"""
Classify stock by liquidity based on spread
"""
if avg_spread_pct < 0.05:
return "Highly Liquid" # Nifty 50 blue chips
elif avg_spread_pct < 0.10:
return "Liquid" # Nifty Next 50
elif avg_spread_pct < 0.25:
return "Moderately Liquid" # Mid caps
else:
return "Illiquid" # Small caps
# Typical spreads in Indian markets
spread_examples = {
'RELIANCE': 0.02, # 0.02% - Highly liquid
'TCS': 0.03, # 0.03% - Highly liquid
'TATASTEEL': 0.08, # 0.08% - Liquid
'SMALLCAP': 0.30 # 0.30% - Illiquid
}
for stock, spread in spread_examples.items():
liquidity = classify_liquidity(stock, spread)
print(f"{stock}: {spread:.2f}% spread - {liquidity}")
Part 3: Market Impact
Price Impact Models
class MarketImpactModel:
"""
Estimate market impact of large orders
Based on Almgren-Chriss model adapted for Indian markets
"""
def __init__(self):
# Impact parameters (calibrated for NSE)
self.permanent_impact_coeff = 0.1
self.temporary_impact_coeff = 0.5
def calculate_impact(
self,
order_size: int,
adv: float, # Average daily volume
volatility: float,
participation_rate: float = 0.10
) -> dict:
"""
Calculate market impact
Parameters:
- order_size: Number of shares to trade
- adv: Average daily volume
- volatility: Daily volatility
- participation_rate: % of volume we represent
"""
# Participation rate
pr = order_size / adv
# Permanent impact (price moves permanently)
permanent_impact_pct = self.permanent_impact_coeff * np.sqrt(pr) * volatility
# Temporary impact (price recovers after)
temporary_impact_pct = self.temporary_impact_coeff * pr * volatility
# Total impact
total_impact_pct = permanent_impact_pct + temporary_impact_pct
return {
'permanent_impact_pct': permanent_impact_pct * 100,
'temporary_impact_pct': temporary_impact_pct * 100,
'total_impact_pct': total_impact_pct * 100,
'participation_rate': pr * 100
}
def optimal_execution_time(
self,
order_size: int,
adv: float,
risk_aversion: float = 1e-6
) -> dict:
"""
Calculate optimal execution horizon
Trade-off: Execute fast (more impact) vs slow (more risk)
"""
# Simplified model
optimal_time_days = (order_size / adv) ** (1/3)
return {
'optimal_days': optimal_time_days,
'recommended_strategy': self._get_strategy_recommendation(optimal_time_days)
}
def _get_strategy_recommendation(self, days: float) -> str:
"""Recommend execution strategy"""
if days < 0.1:
return "Market order (immediate execution)"
elif days < 1:
return "Intraday VWAP or TWAP"
elif days < 5:
return "Multi-day VWAP"
else:
return "Patient limit orders"
# Usage
impact_model = MarketImpactModel()
# Large order example
order_impact = impact_model.calculate_impact(
order_size=50000, # 50,000 shares
adv=5000000, # 50 lakh shares daily average
volatility=0.02, # 2% daily volatility
participation_rate=0.10 # 10% of volume
)
print("Market Impact Analysis:")
print(f"Participation Rate: {order_impact['participation_rate']:.2f}%")
print(f"Permanent Impact: {order_impact['permanent_impact_pct']:.4f}%")
print(f"Temporary Impact: {order_impact['temporary_impact_pct']:.4f}%")
print(f"Total Impact: {order_impact['total_impact_pct']:.4f}%")
# Optimal execution
execution_plan = impact_model.optimal_execution_time(
order_size=50000,
adv=5000000
)
print(f"\nRecommended Execution Time: {execution_plan['optimal_days']:.2f} days")
print(f"Strategy: {execution_plan['recommended_strategy']}")
VWAP and TWAP Execution
class ExecutionAlgorithm:
"""
VWAP and TWAP execution algorithms
"""
def vwap_schedule(
self,
total_quantity: int,
historical_volume_profile: pd.DataFrame
) -> pd.DataFrame:
"""
Create VWAP execution schedule
Distribute orders according to historical volume patterns
"""
# Normalize volume profile
total_volume = historical_volume_profile['volume'].sum()
historical_volume_profile['volume_pct'] = (
historical_volume_profile['volume'] / total_volume
)
# Distribute order quantity
historical_volume_profile['order_qty'] = (
total_quantity * historical_volume_profile['volume_pct']
).astype(int)
return historical_volume_profile[['time', 'volume', 'order_qty']]
def twap_schedule(
self,
total_quantity: int,
start_time: str,
end_time: str,
interval_minutes: int = 15
) -> pd.DataFrame:
"""
Create TWAP execution schedule
Distribute orders evenly across time
"""
# Calculate number of intervals
start = pd.to_datetime(start_time)
end = pd.to_datetime(end_time)
time_range = pd.date_range(
start=start,
end=end,
freq=f'{interval_minutes}min'
)
# Equal quantity per interval
qty_per_interval = total_quantity // len(time_range)
schedule = pd.DataFrame({
'time': time_range,
'order_qty': qty_per_interval
})
# Handle remainder
remainder = total_quantity - (qty_per_interval * len(time_range))
schedule.loc[0, 'order_qty'] += remainder
return schedule
# Example: VWAP execution
# Historical volume profile (simplified)
volume_profile = pd.DataFrame({
'time': ['09:15', '10:00', '11:00', '12:00', '13:00', '14:00', '15:00'],
'volume': [100000, 200000, 300000, 400000, 300000, 200000, 150000]
})
algo = ExecutionAlgorithm()
# Schedule 10,000 share order using VWAP
vwap_schedule = algo.vwap_schedule(10000, volume_profile)
print("\nVWAP Execution Schedule:")
print(vwap_schedule)
# Schedule using TWAP
twap_schedule = algo.twap_schedule(
total_quantity=10000,
start_time='09:15',
end_time='15:30',
interval_minutes=30
)
print("\nTWAP Execution Schedule:")
print(twap_schedule)
Part 4: Practical Implementation
Smart Order Router
class SmartOrderRouter:
"""
Intelligent order routing for optimal execution
"""
def __init__(self, impact_model: MarketImpactModel):
self.impact_model = impact_model
def route_order(
self,
quantity: int,
urgency: str, # 'immediate', 'normal', 'patient'
current_spread: float,
adv: float,
volatility: float
) -> dict:
"""
Determine best execution strategy
"""
participation_rate = quantity / adv
# Calculate impact
impact = self.impact_model.calculate_impact(
quantity, adv, volatility
)
# Decision logic
if urgency == 'immediate':
if impact['total_impact_pct'] > 0.5:
return {
'strategy': 'Iceberg order with passive fills',
'reasoning': 'High impact - use hidden orders'
}
else:
return {
'strategy': 'Market order',
'reasoning': 'Low impact - execute immediately'
}
elif urgency == 'normal':
if participation_rate > 0.10:
return {
'strategy': 'VWAP algorithm',
'reasoning': 'Large order - blend with volume'
}
else:
return {
'strategy': 'Limit order at mid-price',
'reasoning': 'Normal size - seek price improvement'
}
else: # patient
return {
'strategy': 'Passive limit orders over multiple days',
'reasoning': 'Minimize impact with patient execution'
}
# Example
router = SmartOrderRouter(impact_model)
decision = router.route_order(
quantity=25000,
urgency='normal',
current_spread=0.50,
adv=5000000,
volatility=0.02
)
print("\nSmart Order Routing Decision:")
print(f"Strategy: {decision['strategy']}")
print(f"Reasoning: {decision['reasoning']}")
Conclusion
Market microstructure mastery is essential for algo traders:
- Order Book: Understand depth, price-time priority
- Spreads: Know when to cross spread vs wait
- Market Impact: Large orders need careful execution
- Execution Algos: VWAP, TWAP, smart routing
The difference between gross returns and net returns often lies in microstructure understanding.
Want to optimize your execution? Contact us for advanced execution consulting.