44 - PnL Tracker
Overview
The PnL (Profit & Loss) Tracker provides real-time intraday P&L monitoring by combining tradebook data with historical price data. It calculates mark-to-market (MTM) P&L for all positions throughout the trading day and displays it via interactive charts.
Architecture Diagram
┌──────────────────────────────────────────────────────────────────────────────┐
│ PnL Tracker Architecture │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ Data Sources │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Tradebook │ │ Position Book │ │ History API │ │
│ │ (Broker API) │ │ (Broker API) │ │ (1-minute bars)│ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │ │
│ └────────────────────┼────────────────────┘ │
│ │ │
│ ▼ │
└──────────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────────┐
│ PnL Calculation (blueprints/pnltracker.py) │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Position Window Tracking │ │
│ │ │ │
│ │ 1. Parse trades from tradebook │ │
│ │ 2. Group by symbol/exchange │ │
│ │ 3. Create position windows (start_time, end_time, qty, price) │ │
│ │ 4. Apply rate limiting (2 calls/sec for history API) │ │
│ │ 5. Calculate MTM using historical close prices │ │
│ │ 6. Aggregate all symbols into portfolio P&L │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ P&L Calculation Formula │ │
│ │ │ │
│ │ For LONG positions: │ │
│ │ MTM P&L = (Current Price - Entry Price) × Quantity │ │
│ │ │ │
│ │ For SHORT positions: │ │
│ │ MTM P&L = (Entry Price - Current Price) × Quantity │ │
│ │ │ │
│ │ Realized P&L = (Exit Price - Entry Price) × Quantity [Long] │ │
│ │ = (Entry Price - Exit Price) × Quantity [Short] │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────────┐
│ Frontend Display │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Metrics Cards │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Current │ │ Max │ │ Min │ │ Max │ │ │
│ │ │ MTM │ │ MTM │ │ MTM │ │ Drawdown │ │ │
│ │ │ +₹3,750 │ │ +₹4,200 │ │ +₹1,000 │ │ -₹800 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ P&L Chart (LightWeight Charts) │ │
│ │ │ │
│ │ ₹ │ │
│ │ 4000│ ╭──────╮ │ │
│ │ 3000│ ╭───╯ ╰──╮ │ │
│ │ 2000│╭───╯ ╰────── │ │
│ │ 1000│ │ │
│ │ 0├────────────────────────────► Time │ │
│ │ 9:15 10:00 11:00 12:00 1:00 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────┘Implementation Details
Position Window Tracking
The PnL tracker creates "position windows" to track when positions were opened and closed:
# Data structure for each position window
position_window = {
"start_time": datetime, # When position was opened
"end_time": datetime, # When position was closed (None if still open)
"qty": float, # Position quantity
"price": float, # Entry price
"action": str, # "BUY" or "SELL"
"exit_price": float # Exit price (None if still open)
}Trade Timestamp Parsing
The system handles multiple timestamp formats from different brokers:
# Supported formats in parse_trade_timestamp():
formats = [
"%d-%b-%Y %H:%M:%S", # AngelOne: "17-Dec-2025 10:54:03"
"%H:%M:%S %d-%m-%Y", # Flattrade: "09:41:01 17-12-2025"
"%d-%m-%Y %H:%M:%S", # "17-12-2025 09:41:01"
"%Y-%m-%d %H:%M:%S", # ISO-like: "2025-12-17 10:30:00"
"%Y-%m-%dT%H:%M:%S", # ISO: "2025-12-17T10:30:00"
]Rate Limiting
Historical data API calls are rate-limited to avoid broker rate limits:
class RateLimiter:
"""Thread-safe rate limiter for API calls"""
def __init__(self, calls_per_second=2):
self.calls_per_second = calls_per_second
self.min_interval = 1.0 / calls_per_second
self.last_call_time = 0
self.lock = threading.Lock()
def wait(self):
"""Wait if necessary to respect rate limit"""
with self.lock:
current_time = time_module.time()
elapsed = current_time - self.last_call_time
if elapsed < self.min_interval:
sleep_time = self.min_interval - elapsed
time_module.sleep(sleep_time)
self.last_call_time = time_module.time()
# Global instance - 2 calls per second (conservative)
history_rate_limiter = RateLimiter(calls_per_second=2)Carry-Forward Position PnL Tracking
The PnL tracker handles carry-forward positions, meaning positions opened on previous days that are still open today. This is critical for NRML/CNC positions that span multiple trading sessions.
# Carry-forward position handling:
# 1. Detect open positions not in today's tradebook (no entry trade today)
# 2. Fetch historical data from market open to track MTM
# 3. Calculate P&L relative to previous close price
# 4. Two cases handled:
# Case 1: Open carry-forward position (still held, no today trades)
# Case 2: Closed carry-forward position (exit-only trades today)Position Detection:
- Compares current positions (from positionbook) against today's trades
- Positions with quantities but no matching entry trades are carry-forward
- Historical data fetched from 9:15 AM to calculate intraday P&L movement
P&L Calculation for Carry-Forward:
- Uses previous day's close price as the reference
- Tracks MTM from market open using 1-minute historical bars
- Merges carry-forward PnL series with regular trade PnL series
API Endpoint
Get P&L Data
POST /pnltracker/api/pnl
Content-Type: application/json
Cookie: session=...Response:
{
"status": "success",
"data": {
"current_mtm": 3750.00,
"max_mtm": 4200.00,
"max_mtm_time": "11:30",
"min_mtm": 1000.00,
"min_mtm_time": "09:45",
"max_drawdown": -800.00,
"pnl_series": [
{"time": 1706165700000, "value": 1000.00},
{"time": 1706165760000, "value": 1500.00},
{"time": 1706165820000, "value": 2200.00}
],
"drawdown_series": [
{"time": 1706165700000, "value": 0.00},
{"time": 1706165760000, "value": -200.00},
{"time": 1706165820000, "value": 0.00}
]
}
}Response Fields
| Field | Type | Description |
|---|---|---|
current_mtm | number | Current mark-to-market P&L |
max_mtm | number | Maximum P&L reached during the day |
max_mtm_time | string | Time when max P&L was reached (HH:MM) |
min_mtm | number | Minimum P&L during the day |
min_mtm_time | string | Time when min P&L was reached (HH:MM) |
max_drawdown | number | Largest drawdown from peak (negative) |
pnl_series | array | Time series data for P&L chart |
drawdown_series | array | Time series data for drawdown chart |
Calculation Flow
┌──────────────────────────────────────────────────────────────────┐
│ P&L Calculation Flow │
└──────────────────────────────────────────────────────────────────┘
Request arrives at /pnltracker/api/pnl
│
▼
┌─────────────────────────┐
│ Get broker from session │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐ ┌───────────────────────┐
│ Get tradebook via │────▶│ services/tradebook │
│ get_tradebook(api_key) │ │ _service.py │
└───────────┬─────────────┘ └───────────────────────┘
│
▼
┌─────────────────────────┐ ┌───────────────────────┐
│ Get positions via │────▶│ services/positionbook │
│ get_positionbook() │ │ _service.py │
└───────────┬─────────────┘ └───────────────────────┘
│
▼
┌─────────────────────────┐
│ Group trades by symbol │
│ Create position windows │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ For each symbol: │
│ 1. Rate limit wait │
│ 2. Get 1m history │
│ 3. Calculate MTM │
│ 4. Track realized P&L │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ Aggregate portfolio │
│ Calculate drawdown │
│ Return JSON response │
└─────────────────────────┘Data Dependencies
The PnL tracker relies on these services (no dedicated database):
| Service | Purpose |
|---|---|
services/tradebook_service.py | Get today's executed trades |
services/positionbook_service.py | Get current positions |
services/history_service.py | Get 1-minute historical bars |
database/auth_db.py | Get user auth token and API key |
Frontend Components
React Page
Location: frontend/src/pages/PnLTracker.tsx
The React frontend:
- Polls
/pnltracker/api/pnlperiodically - Renders metrics cards (current MTM, max, min, drawdown)
- Uses LightWeight Charts for interactive P&L visualization
- Shows separate drawdown chart below main chart
Legacy Compatibility Route
/pnltracker/legacy is still registered, but the referenced templates/pnltracker.html file is absent from the current tree. Treat this route as unavailable until the route is removed or a template is restored. The supported UI is the React /pnl-tracker page, served by blueprints/react_app.py. blueprints/pnltracker.py also registers /test_chart, which renders the equally absent templates/test_chart.html.
Edge Cases Handled
Sub-Minute Trades
When a position is opened and closed within the same minute (no historical data points):
# Calculate realized PnL even without historical data
if is_closed_position:
if window["action"] == "BUY":
realized = (window["exit_price"] - window["price"]) * window["qty"]
else: # SELL
realized = (window["price"] - window["exit_price"]) * window["qty"]Pre-Trade Period
Zero P&L data is added from market open (9:15 AM IST) to first trade time for complete visualization.
Timezone Handling
All timestamps are converted to IST (Asia/Kolkata) timezone:
ist = pytz.timezone("Asia/Kolkata")
if df["datetime"].dt.tz is None:
df["datetime"] = df["datetime"].dt.tz_localize("UTC").dt.tz_convert(ist)Drawdown Calculation
# Drawdown = Current P&L - Peak P&L (running maximum)
portfolio_pnl["Peak"] = portfolio_pnl["Total_PnL"].cummax()
portfolio_pnl["Drawdown"] = portfolio_pnl["Total_PnL"] - portfolio_pnl["Peak"]
# Max drawdown is the minimum value (most negative)
max_drawdown = portfolio_pnl["Drawdown"].min()Key Files Reference
| File | Purpose |
|---|---|
blueprints/pnltracker.py | Blueprint with P&L calculation logic |
services/tradebook_service.py | Fetches tradebook from broker |
services/positionbook_service.py | Fetches current positions |
services/history_service.py | Fetches historical price data |
frontend/src/pages/PnLTracker.tsx | React UI component |
