NEPSE's floorsheet, the record of every executed contract, is the only dataset here with
no second chance. The API serves the current session and nothing else: it ignores the
businessDate parameter entirely, and the per-company route is nginx-403'd. A day the poller
misses is not recoverable from NEPSE, from a vendor, or from anywhere.
It is also the most valuable table in the warehouse. A normal session is 42,709 contracts,
each carrying a microsecond tradeTime and both the buyer and seller broker IDs.
Most exchanges never expose broker identity at all.
KeepAlive, and why a failed run is never allowed to look like an empty one.
DuckDB is the working warehouse; Parquet is the durable export. At roughly 12 GB a year the whole thing fits on one machine, so Spark or Kafka here would be cost with no benefit. Making Parquet the source of truth is also what lets the pipeline run on a CI runner that is destroyed minutes later, a warehouse rebuilt from Parquet alone reproduced all five tables exactly, floorsheet turnover included, to the paisa.
Every row below was measured against the live endpoint. Several are load-bearing design constraints rather than trivia, the ones about pagination and the trading week are silent data-loss bugs if you assume the documented behaviour.
| Measured fact | Consequence |
|---|---|
| No streaming feed exists; the site polls REST, and the backend refreshes about every 24s. | “Live” means polling. Faster than ~20s buys nothing. |
A browser User-Agent is mandatory. Without one the connection completes TLS and then hangs forever. | No 403 and no error, every client must set it or die silently. |
Auth is a WASM-derived token: the server returns 5 salts, NEPSE's own css.wasm turns them into character offsets, TTL is 45 seconds. | Isolated behind one token source so the rest of the code never sees it. |
sort=contractId,desc is not honoured across pages. Page 10 was observed starting at one id and ending above it. | A watermark “stop at a known id” would silently skip trades. Not used. |
Pages intermittently return a bare [], then yield 500 rows on retry. | Treated as transient, never as end-of-data. |
| All history endpoints clamp to a rolling 365 days, server-side, even given an explicit older start date. | A hard floor on backfill, and the reason for a second source. |
| The intraday index series is wiped a few hours after close: 610 points at 19:30 NPT, zero by 20:25. | Must be captured during or right after the session. |
| NEPSE traded Sunday–Thursday until about 2026-04-06, then Monday–Friday. | A weekday() < 5 weekend filter silently drops ~33 real trading days. |
Bad data fails loudly. It is never dropped and never let through.
extra="forbid", a new upstream field raises, so schema drift surfaces immediately instead of as a column of nulls months later.qty × rate == amount within one rupee, positive prices, trade_time matching business_date, high >= low.totalElements.NEPSE's 365-day floor is hard, so a second source (ShareSansar) supplies the daily series much further back. It is landed in a separate table, tagged with its origin, and never merged into the NEPSE tables, 516,620 rows across 653 symbols, from 1995-07-20 to 2026-08-19, in a single 10.3 MB Parquet file.
The wider universe is the point. A symbol list drawn from the rolling window alone is survivorship-biased: 175 symbols exist only in the second source, delisted mutual funds, converted promoter shares, long-suspended names, and would otherwise be invisible.
The 225-day overlap is the only place a second source can be checked against an authoritative one:
| Check | Result |
|---|---|
| Overlapping rows | 75,671, every NEPSE row has a match |
| Volume exact | 75,671 / 75,671, 100.00% |
| Close exact | 75,657 / 75,671, 99.98% |
Fourteen closes disagree: four are debentures already carrying the out-of-range flag, and the rest differ by under 1% on a single session, which looks like a last-trade-versus-close convention rather than corruption. Volume agreeing on every single row is the stronger signal, the two sources are reporting the same trades. That earns confidence in the mechanism, not a guarantee about 1998.
The notebooks are committed with their outputs, so the findings can be read without running anything.
| Notebook | Finding |
|---|---|
| Who is actually trading this stock? | Large broker positions repeat direction 71.8% of the time next session, with no next-day price edge detectable yet. |
| The corporate actions trap | NEPSE's ~10% circuit limit doubles as a corporate-action detector; 747 candidates recovered from prices alone. |
| The symbols that disappeared | 36% of NEPSE symbols are delisted; survivorship bias measured at +0.74 pts/year. |
Capture runs on GitHub Actions and publishes to Hugging Face, both free at this scale. The job runs after close rather than as a live poller: NEPSE keeps serving the current session's floorsheet for hours afterwards, measured complete at +2h and again at +5.5h, wiped by +5.9h, so one short run captures the same contracts a four-hour poller would at a twentieth of the compute. The intraday index sets the deadline, since it disappears about 4h after close.
The workflow fires twice because scheduled GitHub jobs are routinely delayed and occasionally dropped. Every write is an upsert, so the second run is a no-op when the first succeeded.
Stated plainly, because a dataset is only usable if you know where it stops being trustworthy.