Trading

Building an NSE option chain analyser in Node.js

In 2019 I was spending too much of every trading morning staring at the NSE option chain page. The page itself is fine. The problem is that the information I wanted, which strikes are seeing fresh positions being built and in which direction, is not shown anywhere. You have to compare today's open interest with yesterday's, compare it against the price move, and do that for forty or fifty strikes on both the call and put side. I did it by hand for a few weeks, then wrote option_chain_analysis to do it for me.

The project is small, which is partly why it found an audience. It currently sits at 106 stars and 77 forks on GitHub, and most of the forks are people pointing it at a different index or a different data source. This post is about how it works and the decisions that went into it.

What I built

The app is a Node.js server that renders a single page. On every visit it fetches the current Nifty option chain from NSE, walks the rows, and classifies each strike using two signals: the change in option price and the change in open interest. The result is a table with a colour-coded label next to every call and put, plus a suggested action for the overall picture. There is an expiry selector at the top so you can look at the weekly or the monthly series, and a switch in app.js to point the whole thing at FINNIFTY instead of Nifty.

The stack is deliberately boring. Node runs the server, Handlebars renders the table on the server side, and Bootstrap makes it readable on a phone. I hosted it on a free Heroku dyno in 2019 so that I could open it from anywhere. It is an education-only tool and the README says so; it tells you what the chain is doing, not what to trade.

How it works

Fetching the chain

The first version used Node's built-in https module and then the request package. Both were blocked. The NSE endpoint looked at the request fingerprint and returned nothing useful, even with the same headers a browser sends. After some trial and error I found that a plain cURL call from the same machine went through, so I shelled out to cURL from Node and parsed what came back. It is not elegant, but it was reliable, and the shape of the code is simple enough that anyone who forks the project can swap the fetch layer out.

fetch.js
const { exec } = require("child_process");

// Node request/https were blocked by NSE; cURL from the same box was not.
function fetchChain(url, cb) {
  const cmd = `curl -s -H "Accept: */*" -H "User-Agent: Mozilla/5.0" "${url}"`;
  exec(cmd, { maxBuffer: 1024 * 1024 * 10 }, (err, stdout) => {
    if (err) return cb(err);
    cb(null, stdout);
  });
}

The maxBuffer option matters. The full chain for a monthly expiry is a large payload and the default buffer in exec is far too small, which shows up as a confusing truncated-output error rather than a clear one.

Classifying a strike

The core of the project is a four-row table. Take the price change and the OI change for a contract and you land in exactly one cell:

In code this is a pair of sign checks. I kept it as a pure function with no dependencies so it is trivial to unit test and easy to read for people who are new to both JavaScript and options.

classify.js
// priceChg and oiChg are deltas versus the previous close
function classify(priceChg, oiChg) {
  if (priceChg > 0 && oiChg > 0) return { label: "Long Buildup",      bias: "bullish" };
  if (priceChg < 0 && oiChg > 0) return { label: "Short Buildup",     bias: "bearish" };
  if (priceChg > 0 && oiChg < 0) return { label: "Short Covering",    bias: "bullish" };
  if (priceChg < 0 && oiChg < 0) return { label: "Long Liquidation",  bias: "bearish" };
  return { label: "Neutral", bias: "neutral" };
}

module.exports = { classify };

The suggested-action line at the top of the page follows from the bias. If the chain reads bullish overall, the page suggests buying calls or selling puts. If it reads bearish, it suggests selling calls or buying puts. That is the whole rule set. I resisted adding weightings, scores or thresholds because every one of those is an opinion, and the point of the tool is to show the raw read so you can form your own.

Rendering

Each row of the NSE response becomes one Handlebars context object with the strike, both legs and their classifications. The template loops over the rows and applies a Bootstrap contextual class based on the bias. The expiry selector is a plain form that reloads the page with a query parameter, and the server passes that through to the fetch URL. No client-side framework, no state, no build step.

views/chain.hbs
{{#each rows}}
<tr>
  <td class="{{ce.bias}}">{{ce.oi}} <small>{{ce.label}}</small></td>
  <td class="strike">{{strike}}</td>
  <td class="{{pe.bias}}">{{pe.oi}} <small>{{pe.label}}</small></td>
</tr>
{{/each}}

Switching indices

When FINNIFTY options were introduced, people asked whether the tool could read that chain too. The NSE payload has the same shape for every index, so the switch is a single constant in app.js that selects which symbol goes into the fetch URL. Several of the forks do the same thing for Bank Nifty or for individual stocks. If you want to add a symbol, that constant is where to start.

What I learned

If you want the longer explanation of the four buildup types and how to read them against each other, I wrote that up separately in How to read open interest.

Source on GitHub >