Appendix
Data sources
Every object this app queries, counted live
| Object | Rows | From | To | Used by |
|---|---|---|---|---|
equity_daily | 1,17,81,903~ | 1994-11-03 | 2026-09-09 | Market data, screener, quant, instrument pages |
equity_delivery_daily | 93,33,352~ | 2002-02-12 | 2026-09-09 | Delivery charts and the delivery signal |
index_prices | 9,93,254~ | 1990-01-01 | 2026-09-09 | Index charts, valuation, India VIX |
securities | 20,851 | — | — | Company names, industry, ISIN |
corporate_actions | 41,950 | 2003-01-23 | 2026-09-21 | Price adjustment, action markers, upcoming events |
fno_bars_daily | 4,17,70,520 | 2000-06-12 | 2026-09-09 | Option chain, max pain, PCR |
fno_participant_flows | 36,219 | 2012-01-02 | 2026-09-09 | FII/DII/Pro/Client positioning |
fno_market_daily | 25,095 | 2000-06-12 | 2026-09-09 | Exchange-wide segment activity |
fno_membership_daily | 9,94,660~ | 2001-07-02 | 2026-09-09 | F&O eligibility filter |
mcx_bars_daily | 9,453 | 2025-10-16 | 2026-09-09 | Commodities |
cds_bars_daily | 10,827 | 2025-09-26 | 2026-09-09 | Currencies and G-secs |
index_rebalances | 332 | 1996-09-18 | 2026-09-30 | Inclusion/exclusion history |
vw_index_constituents_latest | 851 | — | — | Index universe filter, constituent ranking |
Sibling projects in this repository
What is imported, what is only copied as a convention, and what is deliberately untouched
portfolio-risk/Yes — importedapi/risk.py adds portfolio-risk/backend to sys.path and calls RiskService(demo=True). That is the entire Portfolio Risk page. Demo mode because the live path calls input() for a broker token, which blocks a web service on stdin with no way to say so.
order-management/Conventions onlyFlat api/main.py with no APIRouter, Depends or CORS; raw psycopg2 connection per call; lib/api.ts copied verbatim; the Route Handler proxy pattern that keeps the browser off FastAPI. No code and no data imported — it holds real order data.
backtesting/Not readThat copy deliberately excludes outputs/, so the backtesting pages read AutoTrader/research/outputs directly. Two copies of a result means two answers to what a strategy returned.
scripts/NoAutomerge and CI tooling — a watch script and a launchd plist.
AutoTrader/Read-onlyresearch/outputs for backtest runs. Nothing in AutoTrader is modified by this app, and it never touches the 5435 staging cluster the collector writes to.
What this dashboard will not show you
Each with the reason, because an unexplained absence reads as an oversight
Live prices during the session
The dashboard reads the SSD archive on port 5434, which holds end-of-day bars. Today's ticks are on the staging cluster (5435), which is the collector's write path during market hours. Keeping the browser off that cluster is why a page load cannot disturb capture.
What you get instead: Everything here is as of the last completed session.
Positions, orders and realised P&L
order-management holds real Zerodha order data and portfolio-risk/backend/accounting can compute P&L from it. Neither is wired in. This app is read-only market data, and the deployment we discussed puts it on a public host.
What you get instead: The risk page runs on a seeded synthetic book, labelled as such. Its maths is the real engine; only the positions are invented.
Point-in-time index membership
index_membership is a reconstruction seeded from an alphabetically-sorted universe. Its current "Nifty 50" omits RELIANCE, HDFCBANK, ICICIBANK, ITC and SBIN while including AARTIIND and AKZOINDIA, which it claims have been members since 1996.
What you get instead: Index filters are current-membership only. "Was X in the Nifty 50 in 2019" cannot be answered from this database.
Bank Nifty, Fin Nifty, Midcap constituents
Membership lists exist for Nifty 50, 100, 200 and 500 and nothing else. Those indices can be charted from index_prices but their members cannot be ranked.
What you get instead: The Banks panel is derived by company name from the Nifty 500 -- 26 names, a superset of Bank Nifty's twelve.
Market capitalisation, free float, listing date
No shares-outstanding column exists in any of the 190 tables, so market cap cannot even be derived. securities.first_seen is populated on 4.7% of rows and means "first seen in index membership", not listed-on.
What you get instead: Turnover is offered instead, labelled traded value rather than size.
Implied volatility
Tested rather than assumed. Put-call parity -- model-free, and the gentlest possible check -- yields implied rates of -2059%, +392%, -181% on NIFTY's own chain, because each contract's daily close is struck at a different moment against a moving underlying. If parity fails that badly, a Black-Scholes inversion would produce plausible nonsense. There is also no risk-free curve anywhere in the schema.
What you get instead: No IV, no greeks from daily data. It would be defensible on derivative_bars_minute (69.9M rows, 2022 onward) and that is future work, not a gap here.
Methodology
Decisions that change the numbers — each measured before it was made
Corporate-action adjustment
Every return and price level is back-adjusted for splits, bonuses and consolidations.
equity_daily is raw and prev_close is the previous close, not NSE's adjusted one. Across the Nifty 500 over six years there are 127 log-returns beyond ±40% across 110 symbols. Unadjusted, RELIANCE's 5-year volatility reads 38.05% instead of 22.40% and its excess kurtosis 526 instead of 3.3; INFY's maximum drawdown becomes −87.9% dated 2020 instead of −50.7% dated 2026.
Annualisation factor
Volatility is annualised over 247 trading days, not 252.
This market traded a mean of 246.91 days a year over 2015–2025, range 244–250. The textbook 252 overstates every volatility by about 1.03%.
Which price column
close, never last_price.
Both are fully populated and differ on 80.3% of rows by a mean 37.5 bps. NSE's own prev_close chains to the prior close 99.4% of the time and to the prior last_price 4.3%. close is the official closing price; last_price is the last trade.
Open interest units
F&O open interest is quoted in shares, and both shares and contracts are shown.
NIFTY 23500 CE held 8,068,840 on 2026-09-09 at lot size 65 — 124,136 contracts. Labelling the raw figure "contracts" overstates it 65× and looks entirely normal.
Commodity and currency prices
MCX is converted from paise; CDS is already in rupees.
Two tables with an identical schema and different units. GOLD reads 15,243,000 and means ₹152,430 per 10g; USDINR reads 95.03 and means 95.03. Verified across six MCX contracts and four currency pairs.
Futures series
Commodity and currency charts use the front month, and name the contract.
GOLD had three contracts trading on 2026-09-09 at 152,430 / 153,983 / 156,256. Grouping by name interleaves them into a sawtooth that is not a price.
Max pain
An argmin over every listed strike, not the strike with the most open interest.
Σ callOI·max(0,S−K) + Σ putOI·max(0,K−S), minimised. On 2026-09-09 the biggest NIFTY call wall was 23,700 and max pain was 23,600 — the two differ, which is what makes the lazy implementation wrong rather than merely different.
Percentiles
Every percentile ships with the window it was ranked against.
The FII long/short ratio has a structural break: its median ran 1.87 in 2012 and 0.14 in 2026. Today's 0.124 is the 3rd percentile of all history and the 15th of the trailing three years. Only the second is useful.
Known traps in the source data
24 found and handled, across 10 areas
| Area | Trap | What it does if ignored | How it is handled |
|---|---|---|---|
| Equity prices | equity_daily is unadjusted | 127 fabricated ±40% returns across 110 Nifty 500 symbols | Back-adjusted via corporate_actions.price_factor |
| Two actions can share an ex-date | AHCL 2026-04-24 is a bonus AND a split: 0.5 × 0.2 = 0.10. Taking one leaves a 5× cliff | Factors multiplied per ex-date (33 such dates) | |
| Demergers and rights carry no price_factor | Real gaps that cannot be corrected — 78 demergers, 49 of them gapping over 15% | Marked differently on the chart; never silently smoothed | |
| Delivery | equity_daily.delivery_pct is NULL before 2020 | A 10-year delivery study silently becomes a 6-year one | Read from equity_delivery_daily (2002 onward) instead |
| equity_daily disagrees with its own quantities on 2020-04-13 | 1,432 rows wrong — 3IINFOTECH reads 58.91% where its quantities say 67.92% | equity_delivery_daily is internally consistent on every one | |
| Joins | equity_daily keys on (trade_date, symbol, series) | 182,516 symbol-days carry multiple series rows; a symbol-only join multiplies them 2–37× | series = 'EQ' pinned everywhere; membership filters use EXISTS |
| 636 of 3,675 traded symbols have no securities row | An inner join drops roughly one symbol in six before any filter | LEFT JOIN throughout | |
| Index data | index_membership is a corrupt reconstruction | Its Nifty 50 omits RELIANCE, HDFCBANK, ICICIBANK, ITC, SBIN and includes AARTIIND | vw_index_constituents_latest used instead; no point-in-time filter offered |
| Nifty 500 ships 501 rows | NSE's file carries a DUMMYHEG placeholder with no price history | Excluded explicitly | |
| index_prices keys on index_name text, not instrument_token | The only price table in the schema that does not; joins differ | Joined on index_name; casing differs from other tables ('Nifty 500' vs 'Nifty500') | |
| index_prices has 11 dates equity_daily does not | Budget Saturdays and Sunday Muhurat sessions; naive beta misreads those days | Common calendar joined before any lag() | |
| Sectors | securities.industry covers 14.2% of traded symbols | It exists only for Nifty 500 members — 498 of 3,511 | Coverage published in the API and stated on the control |
| Derivatives | fno_bars_daily has no instrument_token | Keys on (date, class, underlying, expiry, strike, type) — joins unlike anything else | Queried on its own key |
| derivative_bars_daily is stale | Last row 2026-09-04 while fno_bars_daily is current | fno_bars_daily used throughout | |
| For untraded contracts close is stale and settle is fresh | Mean gap 190 bps on untraded rows; carry can flip sign entirely | Noted; chain shows traded prices and open interest | |
| lot_size is back-derived before 2024 | Non-integral, flickers 75→76→75, producing 1,739 spurious lot-change events | Not used for pre-2024 conversions | |
| Participant flows | Four corrupt dates | 2013-08-22 has labels shifted one row; 2019-12-12's FII row reads 0.103, inside today's plausible range | Excluded surgically — the other participants on 2019-12-12 are kept |
| 'Total' and 'TOTAL' are the same category | One mis-cased row on 2012-01-02, a date with no 'TOTAL' row at all | Folded in via upper(); dropping it would hole day one | |
| total_long_contracts disagrees with its own components | 2017-09-22's short total is exactly double the twelve component columns | Totals summed from components; those columns never read | |
| Contract sizes | Two lot-size changes cut counts overnight | 2015-10-29 and 2025-01-30 — units changes, not activity changes | Marked on every chart showing absolute contract levels |
| Commodities | MCX quotes in paise, CDS in rupees | A silent 100× on every commodity price | MCX divided by 100; verified against real market levels |
| Cross-sections | series='EQ' includes ETFs | ~181 of 2,644 active symbols are not companies | Excluded from breadth, dispersion and the A/D line |
| Dormant names make lag() span months | A three-month move reported as one session | Previous session required within 7 days | |
| Survivorship | 5,409 distinct EQ symbols, 2,678 still trading. Against 2018 point-in-time membership only 228 of 500 overlap | Stated on the quant page rather than solved — the data to solve it is the corrupt table |
If a number here disagrees with your own query, the methodology section above is the first place to look — corporate-action adjustment, the 247-day annualisation, close versus last_price, and the paise-versus-rupees split account for most disagreements. Every one of those is a deliberate choice with the measurement that justified it, and each is enforced by a test that fails if it is reversed.