Reconciling an F&O prop desk’s P&L to ₹0.00
An Indian derivatives proprietary trading desk was assembling its dealer-wise P&L, its interest and its monthly invoice in spreadsheets, by hand. Building the screens was never the hard part.
- Client
- An Indian F&O proprietary trading desk, unnamed at their request
- Sector
- Capital markets — equity derivatives
- Engagement
- Full-stack product design, build and deployment
- Stack
- React 18 · TypeScript · Vite · Python · FastAPI · SQLAlchemy 2 · PostgreSQL · nginx · systemd
- Outcome
- ₹ 0.00 difference against the reference ODIN Clientwise Net Position report
A back office that has to agree with the terminal
The desk runs several dealers under one or more client entities. Every evening its ODIN terminal produces an export of the day’s fills, and from that the desk needs to know, per dealer and per scrip: what was realised, what is still open, what the day’s mark-to-market came to, what the trading cost in exchange and statutory charges, what interest is owed on the deposit funding each dealer’s margin — and what the month’s invoice adds up to.
All of it was being assembled in spreadsheets, by hand, every month.
The answers have to agree with the terminal’s own reports, because those are what the client sees.
A console that is approximately right is a console that starts an argument at the end of every month. That single constraint — reconcile to the rupee against a dated Clientwise Net Position report — drove nearly every non-obvious decision in the system.
Twelve screens, one filter, under a minute a day
Three problems between “approximately right” and ₹0.00
None of them appeared in the requirements. All three were found by chasing a difference until it explained itself.
- 01
The closing price was coming from the wrong place
The terminal's position export carries a Market Price column, and it is the obvious thing to value positions from. It is also wrong: the file has no timestamp, so a mid-session export cannot be told apart from an end-of-day one, and the price in it is an unpublished intraday snapshot rather than a settlement figure.
So: A closing price only ever comes from the exchange bhavcopy, never from an uploaded sheet. That meant building NSE and BSE bhavcopy ingestion properly — downloading the zipped UDiFF CSVs from the archives, parsing them at contract level, and caching each settled day on disk.
- 02
An expiring option is not worth what it last traded at
On its expiry day an option settles at intrinsic value — zero if it is out of the money — not at the last price it printed at. Valuing a single expiring out-of-the-money contract at its last trade put a four-figure error into the month.
So: One rule, applied at the mark: futures and non-expiry days take the settlement price; an option on its own expiry day takes intrinsic value.
- 03
A date window inherits the position, not the trades
A dealer long 500 lots on the 28th does not have those fills counted into a window that opens on the 29th — but the position is still there, and its P&L inside that window has to be measured from the price it was carried in at, not from what was originally paid for it.
So: The system generates one synthetic carry-in entry per open position at its carry price, filtered by exactly the same clauses the window itself is filtered by. They are marked so the trade log never lists them and nothing downstream can mistake one for a deal.
With those three rules in place, the console matched the reference report exactly.
Where a book quietly loses the last few rupees
Exact-decimal money, everywhere
No floating-point arithmetic on a rupee anywhere in the backend. On a system whose entire purpose is to agree with another system to the paise, that is not fastidiousness — it is the requirement.
Round where the invoice prints
The window is split at every deposit change and every month boundary, each segment rounded to paise on its own. The invoice always equals the sum of its own printed lines.
Charges per dealer, never desk-wide
The book figure is the sum of the per-dealer bills, each off that dealer's own slab. A dealer with no slab contributes nothing rather than falling back to a default.
One tenancy gate
Every book endpoint resolves its client id through a single function. Grants are read from the database on every request, never from the login token, so revoking access takes effect on the next click.
A pure valuation core
The computation and interest modules are plain functions over lists of fills, free of database and HTTP concerns — which is why the reconciliation could be done against real data rather than by clicking through a UI.
Degrade cleanly, not partially
The optional live-quote integration requires all four of its settings; miss one and the feature switches off entirely rather than half-working.
What the desk got back
- The monthly workbook is gone. One file uploaded a day; the month’s invoice is a PDF download.
- Billing is defensible per dealer — charges off each dealer’s own slab, interest off a dated ledger, and an invoice whose printed lines add up to its printed total.
- Closing prices arrive on their own. A scheduled job runs four times each evening, Monday to Friday, because the exchange does not publish at a predictable minute.
- Tested where it counts. The reconciliation rules — carry-in pricing, expiry-day valuation, exchange-only closes, access boundaries — are each covered by a suite named after the rule it protects.
Honest about the edges
- Open positions cannot tie out to the terminal to the paise. The terminal marks at the last traded price — an unpublished intraday snapshot — while this system marks at the exchange settlement price. The difference is real and unavoidable from this side, so we documented it rather than tuning a number until it disappeared.
- The terminal’s own reports disagree with each other. Its risk screen is cumulative over whatever range is loaded; the Clientwise Net Position report is dated. We reconcile to the dated report, because that is what the desk bills from — and said so in writing, so nobody compares against the wrong screen six months later.
- Gaps in exchange data are data, not bugs. If the exchange has not published a day’s file, that day has no marks and is flagged as a gap rather than filled with a guess. Re-running the close job once the file appears resolves it.
- ₹ 0.00 is one window. It is how the system is built rather than a number that was tuned until it disappeared — but “reconciled exactly on the reference window, 29 Jul — 25 Aug 2026” is the defensible phrasing, and it is the one we use.
One server the desk controls
PostgreSQL in production, SQLite for a pilot with no database setup at all — one environment variable switches between them and no application code moves. nginx serves the console and proxies the API; scheduled closing-price fetches and backups run as systemd timers. The desk’s data never leaves their server except as requests to the public exchange archives.
Have a book that has to agree with someone else’s numbers?
That is the kind of problem we like. Send us one session’s export and we will show you the book it produces.
Book a demo