Retail option analytics in India has a data problem. Licensed feeds cost real money, brokers gate their API
behind accounts, and the free tools that exist hand you a wall of numbers and stop there. Meanwhile the
exchanges publish option chains on their own public websites - which anyone can read, and which nobody will
support you when it changes.
I built Strike & Hedge on exactly those public endpoints: live
NSE and BSE chains, Black-Scholes Greeks solved locally, PCR, max pain, OI walls, expected move, and a
rule-based trade plan. No API key. MIT. 45 offline tests.
This post is the field notes - the parts that are not in any tutorial because they only show up when a
government exchange quietly retires an endpoint on a Tuesday.
The anti-bot wall comes first
Both exchanges sit behind Akamai. Plain requests gets 403s, and no amount of header cosplay fixes it,
because the TLS handshake itself is the fingerprint. What works:
# copilot/nse.py, copilot/bse.py
# curl_cffi impersonates a real Chrome TLS/JA3 profile; warm cookies from the homepage first
from curl_cffi import requests as session # falls back to `requests` if curl_cffi is unavailable
Keep the fallback. curl_cffi is a compiled dependency and it will not install on some machine you care
about; the tool should degrade to "this source did not load" rather than "this tool does not start".
Endpoints are a moving target, so write down what moved
The specific things that bit me, as of October 2026:
| Need | What is actually there |
|---|---|
| NSE chains |
option-chain-v3 + option-chain-contract-info. The old option-chain-indices endpoint is retired
|
| BSE (SENSEX) chain |
DerivOptionChain, which wants the expiry formatted as 15 Oct 2026
|
| BSE expiry list | not published. Upcoming Thursdays get probed until one answers |
| NSE futures quotes |
gone. quote-derivative, quote-v2 and last-price-v5 all 404 |
| FINNIFTY daily history | niftyindices.com - Yahoo delisted ^CNXFINANCE, and NSE's own chart API returns empty for every index |
| FINNIFTY intraday | no public source exists |
Two of those are lies rather than gaps, and they are the dangerous kind:
-
BSE reports
Absolute_Change_OIas 0 intraday. Not "flat" - not published yet. Rendered as a real number, it tells you writers are not active when the exchange simply has not said. -
BSE sends no IV. So IV gets solved from the premium. And when a deep-ITM quote will not solve, the row
shows
—. A solver forced to always answer will happily hand you a 500% IV, and the Greeks downstream will believe it.
When a number cannot be had, derive it and label it
With NSE's futures quote endpoints retired, a futures card has no honest price. The tool derives the futures
level from put-call parity on the ATM strike - from copilot/chain.py:
def synthetic_futures(chain, r=0.065):
"""Futures level implied by put-call parity on the ATM strike: F = K·e^(-rT) + CE - PE."""
ltp = k * math.exp(-r * t) + c - p
...
"source": "put-call parity on ATM strike",
Then three things matter more than the formula:
- the card is labelled as derived, and says it can be tens of points off your broker's price
- the parity basis is never scored as a signal - last-traded option quotes put that much noise into it
- there is no futures OI or buildup anywhere in the score, because that data does not exist
Same principle for the FINNIFTY intraday panel: it falls back to recent daily bars and says that it did.
Silent fallback is where dashboards go to die. A tool that narrates its own blind spots gets used; one that
hides them gets believed, and then gets abandoned.
And at the close: signals come from a chain snapshot, not tick data, so near 15:30 IST the settled expiry is
dropped. Otherwise you get a ₹0.05 premium with nonsense Greeks, printed as if it were actionable.
Split the deterministic engine from the LLM
The pipeline is deliberately two halves:
NSE option-chain-v3 ─┐
BSE DerivOptionChain ─┤→ chain.py normalise + Black-Scholes Greeks / IV
niftyindices.com ─────┤ ↓
Yahoo quotes/VIX ─────┘ analytics.py PCR · max pain · OI walls · expected move
↓
signals.py bias + entry / stop / target / exit / lots
↓
dataflows.py → agents.py → graph.py (optional AI desk)
↓
reporting.py markdown report + decision log
signals.py needs no key at all and produces BULLISH / BEARISH / RANGE / AVOID with a score and
written reasons, plus a concrete plan: instrument, entry, stop, target, R:R, lots, max loss in ₹, exit rule.
Stops and targets come from ATR and OI walls on the underlying, converted to premium through delta - so
they react to real price geometry instead of a fixed percentage. Never naked selling: bullish → BUY CE beyond
ATM (plus a futures long on the derived level), range → defined-risk credit spreads, sell near the OI wall,
hedge two strikes further out.
The risk model has one behaviour I am stubborn about:
Type in capital and risk % - if one lot already risks more than your capital × risk %, the tool refuses
to round up. It tells you the position does not fit, instead of quietly showing "1 lot".
The AI desk (chain / volatility / flow / news analysts → bull vs bear debate → risk → portfolio manager) sits
on top of the same numbers, across 8 backends: Anthropic, OpenAI, Gemini, DeepSeek, Groq, OpenRouter,
Ollama (fully local, no key), or any OpenAI-compatible endpoint. Two guards make it behave:
- Grounding check. The LLM is given the rule engine's numbers. If its quoted entry is far from the live premium - the classic mistake of stating a strike as a premium - the rule-engine level is shown instead, with a warning.
- Decision log. Every AI call is scored later against the underlying's actual move with a hit/miss flag, and those lessons feed into later runs. An agent loop with no outcome tracking is a random number generator with good grammar.
Here is the engineering ask buried in that design: keep the rule engine separate. If you only want chains,
Greeks and signals, delete agents.py and graph.py entirely and the rest still works.
Make failure debuggable, and make tests offline
When a chain will not load, the answer is one request away:
curl http://localhost:8000/api/diag/NIFTY # what each price source actually returned
Endpoints change and rate limits bite without notice, so a debug endpoint that shows raw per-source responses
turns "it's broken" into "NSE is 403ing and BSE is fine".
The test suite is offline on purpose - 45 tests, no network, no keys, so CI stays green whether or not an
exchange is having a day:
python -m pytest tests -q
python tests/ui_harness.py # full UI on synthetic data → http://127.0.0.1:8765
That synthetic harness is the other half of the trick: it lets a reviewer see the entire UI without a broker
account, a key, or market hours. No frontend build step either - web/ is plain HTML/CSS/JS served by FastAPI.
git clone https://github.com/85599/Strike-and-Hedge.git
cd Strike-and-Hedge && pip install -r requirements.txt
uvicorn server:app --port 8000 # open http://localhost:8000
The part you should read before you scrape
This is a scraper, not a licensed feed. It reads public endpoints at request time and caches briefly; it is
not a redistribution right, and exchanges can change terms whenever they like. Lot sizes in the repo are
fallbacks - SEBI revises them, so verify with your broker. And publishing buy/sell calls publicly can
require SEBI Research Analyst registration, which is worth knowing before you share output anywhere. It is an
educational research tool, not investment advice, and not issued by a registered research analyst.
Which Indian exchange endpoint has wasted the most of your afternoon - and is anyone tracking a working
futures-quote source now that NSE's are 404ing? If you want the whole pipeline to read or break:
85599/Strike-and-Hedge. PRs for endpoint changes, lot sizes and
strike steps are the ones I merge fastest.
Top comments (0)