Appendix

what this dashboard reads, what it refuses to, and why
Written for someone who did not build this. The source inventory below is queried live, not transcribed — a hand-written table of row counts is wrong the day after it is written and nobody notices. Everything else on this page is a judgement rather than a measurement, and is written down deliberately so that changing it takes an edit.

Data sources

Every object this app queries, counted live

13 objects
ObjectRowsFromToUsed by
equity_daily1,17,81,903~1994-11-032026-09-09Market data, screener, quant, instrument pages
equity_delivery_daily93,33,352~2002-02-122026-09-09Delivery charts and the delivery signal
index_prices9,93,254~1990-01-012026-09-09Index charts, valuation, India VIX
securities20,851Company names, industry, ISIN
corporate_actions41,9502003-01-232026-09-21Price adjustment, action markers, upcoming events
fno_bars_daily4,17,70,5202000-06-122026-09-09Option chain, max pain, PCR
fno_participant_flows36,2192012-01-022026-09-09FII/DII/Pro/Client positioning
fno_market_daily25,0952000-06-122026-09-09Exchange-wide segment activity
fno_membership_daily9,94,660~2001-07-022026-09-09F&O eligibility filter
mcx_bars_daily9,4532025-10-162026-09-09Commodities
cds_bars_daily10,8272025-09-262026-09-09Currencies and G-secs
index_rebalances3321996-09-182026-09-30Inclusion/exclusion history
vw_index_constituents_latest851Index universe filter, constituent ranking
Row counts marked ~ are planner estimates rather than exact counts, used where an exact count would mean a full scan. The archive is read on port 5434 — the SSD cluster — and never on 5435, which is the tick collector's write path during market hours. That separation is why loading a page cannot disturb capture.

Sibling projects in this repository

What is imported, what is only copied as a convention, and what is deliberately untouched

portfolio-risk/Yes — imported

api/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 only

Flat 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 read

That 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/No

Automerge and CI tooling — a watch script and a launchd plist.

AutoTrader/Read-only

research/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

every one of these renders plausibly if ignored
AreaTrapWhat it does if ignoredHow it is handled
Equity pricesequity_daily is unadjusted127 fabricated ±40% returns across 110 Nifty 500 symbolsBack-adjusted via corporate_actions.price_factor
Two actions can share an ex-dateAHCL 2026-04-24 is a bonus AND a split: 0.5 × 0.2 = 0.10. Taking one leaves a 5× cliffFactors multiplied per ex-date (33 such dates)
Demergers and rights carry no price_factorReal gaps that cannot be corrected — 78 demergers, 49 of them gapping over 15%Marked differently on the chart; never silently smoothed
Deliveryequity_daily.delivery_pct is NULL before 2020A 10-year delivery study silently becomes a 6-year oneRead from equity_delivery_daily (2002 onward) instead
equity_daily disagrees with its own quantities on 2020-04-131,432 rows wrong — 3IINFOTECH reads 58.91% where its quantities say 67.92%equity_delivery_daily is internally consistent on every one
Joinsequity_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 rowAn inner join drops roughly one symbol in six before any filterLEFT JOIN throughout
Index dataindex_membership is a corrupt reconstructionIts Nifty 50 omits RELIANCE, HDFCBANK, ICICIBANK, ITC, SBIN and includes AARTIINDvw_index_constituents_latest used instead; no point-in-time filter offered
Nifty 500 ships 501 rowsNSE's file carries a DUMMYHEG placeholder with no price historyExcluded explicitly
index_prices keys on index_name text, not instrument_tokenThe only price table in the schema that does not; joins differJoined on index_name; casing differs from other tables ('Nifty 500' vs 'Nifty500')
index_prices has 11 dates equity_daily does notBudget Saturdays and Sunday Muhurat sessions; naive beta misreads those daysCommon calendar joined before any lag()
Sectorssecurities.industry covers 14.2% of traded symbolsIt exists only for Nifty 500 members — 498 of 3,511Coverage published in the API and stated on the control
Derivativesfno_bars_daily has no instrument_tokenKeys on (date, class, underlying, expiry, strike, type) — joins unlike anything elseQueried on its own key
derivative_bars_daily is staleLast row 2026-09-04 while fno_bars_daily is currentfno_bars_daily used throughout
For untraded contracts close is stale and settle is freshMean gap 190 bps on untraded rows; carry can flip sign entirelyNoted; chain shows traded prices and open interest
lot_size is back-derived before 2024Non-integral, flickers 75→76→75, producing 1,739 spurious lot-change eventsNot used for pre-2024 conversions
Participant flowsFour corrupt dates2013-08-22 has labels shifted one row; 2019-12-12's FII row reads 0.103, inside today's plausible rangeExcluded surgically — the other participants on 2019-12-12 are kept
'Total' and 'TOTAL' are the same categoryOne mis-cased row on 2012-01-02, a date with no 'TOTAL' row at allFolded in via upper(); dropping it would hole day one
total_long_contracts disagrees with its own components2017-09-22's short total is exactly double the twelve component columnsTotals summed from components; those columns never read
Contract sizesTwo lot-size changes cut counts overnight2015-10-29 and 2025-01-30 — units changes, not activity changesMarked on every chart showing absolute contract levels
CommoditiesMCX quotes in paise, CDS in rupeesA silent 100× on every commodity priceMCX divided by 100; verified against real market levels
Cross-sectionsseries='EQ' includes ETFs~181 of 2,644 active symbols are not companiesExcluded from breadth, dispersion and the A/D line
Dormant names make lag() span monthsA three-month move reported as one sessionPrevious session required within 7 days
Survivorship5,409 distinct EQ symbols, 2,678 still trading. Against 2018 point-in-time membership only 228 of 500 overlapStated on the quant page rather than solved — the data to solve it is the corrupt table
None of these throw an error. That is what makes them worth cataloguing: each one produces a page that looks entirely normal and is wrong. They were found by querying the data and checking whether the answer was physically plausible, not by anything failing.

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.