main
md 13.1 KB

NSE canonical sector benchmarks and price history

Identity and classification

Baseline: 076215b5ee757061357aa6a71e5a6e85988d8baa (OHLCV technical features). The audited master contained 2,568 equities and no indices. Stock classification comes from portfolio.nifty500_universe.canonical_sector, populated by Nifty500ReferenceService and NiftyIndustrySectorClassifier from the official Nifty 500 constituent file. The persisted source is NSE_INDICES_NIFTY500. Portfolio/watchlist membership is irrelevant.

SECTOR_BENCHMARK_MAPPING_V1 resolves exact canonical classifications to platform benchmark keys. NSE_BENCHMARK_CATALOG_V1 records the authoritative registration evidence. These are price indices, not total-return series.

Canonical classification Platform key Canonical UUID Verified NSE request symbol
India broad market INDIA_BROAD_PRICE 56623ea0-224c-3481-b6d9-66c448b282a0 NIFTY 500
Technology INDIA_TECHNOLOGY_PRICE 4af7634a-1894-333d-ba68-a1c41a32c71c NIFTY IT
Financials INDIA_FINANCIALS_PRICE 13ba7849-43e2-3e1e-a3f3-d93ee32d3d2f NIFTY FINANCIAL SERVICES
Healthcare INDIA_HEALTHCARE_PRICE 055c4e9f-5592-3be6-8f87-3d05bb25b95a NIFTY HEALTHCARE INDEX

UUIDs use Java UUID.nameUUIDFromBytes(UTF8("aip:benchmark:" + platformKey)); the Python adapter implements the identical UUIDv3 operation. A calculated UUID alone is not registration or identity evidence. Both acquisition and context resolution require the actual master row and exactly one verified NSE mapping. Provider symbols never identify stocks or benchmark instruments.

The existing master service now registers AssetType.INDEX through explicit POST /api/v1/instruments/benchmarks/{benchmarkKey}/register. It uses the existing transaction/advisory-lock, master, and mapping repositories. Unknown keys and conflicting identities fail. Repeating a registration is idempotent. The existing VARCHAR asset-type column accepts INDEX; no migration is required or supplied. GET /api/v1/instruments/benchmarks only reads registered identities and mappings. It does not register instruments or acquire data. Authentication follows the existing global-instrument controller convention.

Acquisition requires ACTIVE / INDEX / IN / NSE / INR master metadata and exactly one NSE mapping with VERIFIED status, matching exchange/currency/request symbol, and catalog resolution source. Inactive, missing, conflicting, or untrusted identity fails closed. Context construction also requires trusted India universe membership before assigning the India broad benchmark.

Mapping evidence and exclusions

  • Nifty 500 is the broad benchmark: its large/mid/small-cap coverage matches the platform's existing Nifty 500 classification universe. There is no Nifty 50 fallback.
  • Nifty IT matches the classifier's Information Technology → Technology mapping.
  • Nifty Financial Services covers banks and other financial services, matching Financial Services → Financials. A bank-only index would exclude part of this classification.
  • Nifty Healthcare matches Healthcare; a pharma-only substitution is not made.

The official NSE /api/allIndices catalog and the captured history responses confirmed these four index contracts on 2026-09-13. Fixtures retain those responses.

Observed unmapped classifications: Communication Services (14), Consumer Discretionary (88), Consumer Staples (28), Energy (17), Industrials (75), Materials (55), Real Estate (11), Utilities (17); 17 records lacked classification. Mapped counts were Financials 101, Healthcare 48, Technology 27. These counts describe the audit snapshot, not an ongoing invariant. Broad compound classifications are not assigned to narrower indices. Raw FINANCIAL SERVICES, Bank, and other aliases are not accepted by the mapping adapter; classification must first come from the canonical classifier. Duplicate classification evidence fails closed.

Official history contract

History version: NSE_INDEX_HISTORY_V1.

GET https://www.nseindia.com/api/historicalOR/indicesHistory with indexType=<verified mapping>, from=DD-MM-YYYY, to=DD-MM-YYYY. Bootstrap: https://www.nseindia.com/report-detail/eq_security. The older /api/historical/indicesHistory returned HTTP 200 HTML during discovery and is deliberately not used. The equity security CSV endpoint is not reused.

The index provider composes the existing NSE provider's transport/session, browser headers, cookies, lock, request spacing and bounded retry behavior; it does not call its equity identity gate or CSV parser. One transport/session is reused sequentially per explicit benchmark population invocation and closed in finally. There is no global cookie pool, parallel fetch, proxy, or bypass. This small composition uses the existing private transport methods; a future transport refactor must preserve the tested shared-session contract.

JSON is { "data": [ ... ] }. Required fields: EOD_INDEX_NAME, EOD_TIMESTAMP, EOD_OPEN_INDEX_VAL, EOD_HIGH_INDEX_VAL, EOD_LOW_INDEX_VAL, EOD_CLOSE_INDEX_VAL.

Verified response-name aliases, confined to this parser:

Requested symbol EOD_INDEX_NAME
NIFTY 500 NIFTY 500
NIFTY IT NIFTY IT
NIFTY FINANCIAL SERVICES NIFTY FIN SERVICE
NIFTY HEALTHCARE INDEX NIFTY HEALTHCARE

Case/whitespace normalization is explicit; no other symbol equivalence is inferred. EOD_TIMESTAMP (for example 11-SEP-2026) is the exchange-local DATE. HI_TIMESTAMP can be the previous UTC day and is never used for trading dates. JSON decimals are decoded directly to Decimal; domain OHLC validation applies. Missing fields, malformed values, out-of-window dates, duplicate dates, or wrong index names reject the complete response before persistence. Empty responses and HTTP 200 HTML are explicit failures, not zero-valued observations.

HIT_TRADED_QTY and HIT_TURN_OVER were observed, but their index aggregation and units are not normalized in this phase. Volume, turnover, and previous close remain null. Actual OHLC values are index points; INR is the trusted index market denomination, not an assertion that each point is a tradable currency amount.

Population and persistence

An explicit worker calls IndiaMarketDataPopulationJobs.populate_benchmark_history with registered canonical IDs and one bounded inclusive start/end window. It uses ResearchRepository.upsert_daily_market_bars_async via persist_daily_result. Primary identity remains (global_instrument_id, trading_date, provider='NSE'). Corrections update that key; another provider remains separate. No deletes, fabricated OHLC, or close-history dual-writes occur.

The existing nse_historical_request_window_days defaults to 30 inclusive calendar days. This is an operational bound, not an NSE guaranteed maximum. Invocations are capped by market_data_population_batch_size, ordered by UUID, and persist per successful window. This explicit bounded operation does not schedule a broad backfill or automatically extend lookbacks.

Spacing uses market_data_population_request_interval_seconds (default 0.20). The existing default two retries apply to transient network/5xx/429 failures; ordinary 4xx, including 403, are not retried. Exponential backoff and Retry-After handling are inherited. Failure cooldown uses the existing population cooldown; 429 also inhibits subsequent instruments in the worker. Cooldowns are process local, like the existing population jobs. Results reuse NseHistoricalResult, including HTTP status, failure reason, accepted/rejected counts and persisted rows.

Computation and batching

Prepare contexts with await orchestrator.sector_benchmark_contexts(candidate_ids, identity_headers=...), then pass them to GlobalScanner.enrich_candidates(..., sector_contexts=contexts). This keeps the existing explicit Stage-B dependency boundary. No dashboard, watchlist, portfolio, scanner GET, or feature computation invokes acquisition.

The context adapter reads the existing paged canonical universe and one benchmark catalog response. Portfolio-service batches master and mapping reads. Stage B collects distinct canonical stock/benchmark IDs and performs the existing bounded close-history read plus one NSE daily-bar read, grouping in memory. With the default batch bound, history SELECT counts are 0 for empty, 2 for one stock, 2 for 18 same-sector stocks, and 2 for 18 stocks across the three mapped sectors. Canonical metadata preparation is separate from these history query counts.

The existing SectorRelativeStrengthEngine consumes persisted NSE daily closes with DATE semantics, selecting a coherent provider series. Existing close-only fallback remains supported. Stock short/stale OHLC fallback follows the existing technical input policy; individual dates/providers are never spliced together.

Lookbacks preserve the existing 5 / 21 / 63 / 126 stock observations for 1W / 1M / 3M / 6M. Both exact stock endpoints must exist in the benchmark history. Returns are (end / start - 1) * 100. Weekends, holidays, and missing intermediate dates are not synthesized. No forward/back fill, nearest-date matching, or calendar-session guessing is performed. A missing endpoint makes that horizon unavailable; at least two horizons are needed for the existing score/state rules. Thresholds, score weights, Technical Features V2 and Rule Engine V1 are unchanged.

Snapshots expose mapping version, canonical benchmark IDs, history source and explicit benchmark states: NO_SECTOR_CLASSIFICATION, UNMAPPED_SECTOR_BENCHMARK, BENCHMARK_IDENTITY_UNAVAILABLE, BENCHMARK_HISTORY_UNAVAILABLE, INSUFFICIENT_OVERLAP, STALE_BENCHMARK_HISTORY, AVAILABLE. Per-horizon diagnostics remain available even when another horizon has overlap. Missing evidence is never zero or a weak-sector conclusion. Existing seven-day freshness policy remains.

Controlled runtime validation and limits

One four-day Nifty 500 probe established the JSON contract. Subsequently just four 30-day requests (2026-08-13 through 2026-09-11, one per registered index) returned HTTP 200 and 22 rows each. No all-sector or all-equity backfill ran. The captured responses were replayed locally without additional provider calls.

The real master service registered/read back all four identities in the existing H2 test schema; its public metadata fed the Python runtime. The existing repository persisted 88 captured rows to local SQLite. Repeat upserts left 88 rows. Real stock classification and close history were copied read-only from local PostgreSQL:

Stock Verified canonical sector Actual overlapping dates Result
HDFCBANK Financials 22 Sector and market evidence available
SUNPHARMA Healthcare 22 Sector and market evidence available
TCS Technology 22 Sector and market evidence available
LT Industrials 22 with market Sector explicitly unmapped

Repeated/reversed-input results matched, with networking blocked during compute. The small benchmark capture supports short horizons only; 3M/6M formulas are validated independently in tests, not claimed as available from this smoke. These outputs are engineering diagnostics, not production investment conclusions.

The deployed PostgreSQL instance lacks the pre-existing daily-bar table. No migration was created/applied, and no benchmark was directly inserted into that master by SQL. Deployment of the prior daily-bar schema plus this canonical registration path remains necessary before deployed acquisition can work.

Unresolved: unmapped sectors, deployed PostgreSQL validation, Global Opportunity Ranker, short/long action model, recommendation history/lifecycle, news/macro, prediction/backtesting. None is implemented by this slice.

Validation results

Final research-engine suite: 992 passed, one existing dependency deprecation warning. Included: 64 new benchmark tests, 21 sector-relative tests, 25 technical tests, 42 OHLCV tests, 35 daily-bar persistence tests, 45 backfill tests, 55 NSE equity provider tests, 22 population tests, 31 Phase-1/batch scanner tests, and 29 Rule Engine V1 tests.

Java benchmark registration/instrument-master tests: 19 passed, including the H2 registration/read-back integration test. The broader portfolio reactor run reported 194 tests with two failures: CanonicalIdentityBootstrapTest assumes an empty shared H2 database (passes when rerun alone), and AppUserProvisionerTest expects an obsolete conflict target/SQL shape. Their implementation/test files are unchanged by this phase; no unrelated fixes were included. The benchmark H2 test rolls its writes back after exporting public metadata for the runtime smoke.

git diff --check passed. No changes to Technical Features V2, Rule Engine V1, NSE equity provider/backfill, frontend, or migrations. platform.ps1 is unchanged by this phase (phase-start SHA256 4577DA738957A6C35A8035812586344E76A557DFE41A7355F5A4D5F3E9D2E3AF). Nothing staged, committed, or pushed. smtp.password was not opened.