Trading

Charting the cash vs futures spread for NSE stocks

Every NSE stock that has a futures contract trades at two prices at once: the cash price on the spot market and the futures price for the current month. The difference between them is the spread, sometimes called the basis. Most of the time it is a small positive number that reflects the cost of carrying the stock to expiry. When it stops behaving like that, something is going on, and I wanted to see it.

The brokers I used showed the two prices in different windows and never the difference. The exchange publishes both but not side by side over time. So I wrote futures_spread_chart, a small Node and Express app that computes the daily spread and draws it.

Why the spread matters

In theory the futures price equals the cash price plus the cost of carry (interest for the days to expiry) minus any dividend expected before expiry. That is the fair value. In practice the traded spread wanders around fair value because of demand for leverage, short-selling constraints, and plain positioning.

A few patterns are worth watching:

None of this is a signal on its own. It is context that the price chart does not give you, and it is cheap to compute once you have both series.

What I built

The app has three parts. spreadData.js is the module that gathers the cash close and the futures close for a symbol and produces one row per trading day with the spread. An Express server exposes that as JSON. And /chart.html is a static page that fetches the JSON and draws the spread as a line, with the cash and futures prices available on a second axis so you can see whether a move in the spread came from the cash leg or the futures leg.

The structure is intentionally flat. There is no database. The data module is the only thing that knows where prices come from, so swapping the source is a one-file change. Everything else is presentation.

How it works

Computing the spread

The data module takes two series keyed by date and joins them. The join matters more than it sounds: cash and futures do not always have the same set of dates in the source data, so I key both by date and only emit a row where both are present. The spread is stored both in absolute rupees and as a percentage of the cash price, because a 5 rupee spread means something different on a 100 rupee stock than on a 3,000 rupee one.

spreadData.js
// cash and fut: { "YYYY-MM-DD": closePrice }
function buildSpread(cash, fut) {
  const rows = [];
  for (const date of Object.keys(cash).sort()) {
    if (!(date in fut)) continue;        // only days where both legs traded
    const c = cash[date], f = fut[date];
    rows.push({
      date,
      cash: c,
      futures: f,
      spread: +(f - c).toFixed(2),
      spreadPct: +((f - c) / c * 100).toFixed(3)
    });
  }
  return rows;
}

module.exports = { buildSpread };

Serving it

Express does two jobs: serve the static chart page and expose the spread as JSON. The symbol comes in as a query parameter and is validated against a short allow-list, because the symbol ends up in a request to the price source and I did not want to forward arbitrary strings.

server.js
const express = require("express");
const { getSpread } = require("./spreadData");

const app = express();
app.use(express.static("public"));   // serves /chart.html

app.get("/api/spread", async (req, res) => {
  const symbol = String(req.query.symbol || "NIFTY").toUpperCase();
  if (!/^[A-Z0-9&-]{1,20}$/.test(symbol)) return res.status(400).end();
  try {
    res.json(await getSpread(symbol));
  } catch (e) {
    res.status(502).json({ error: "upstream" });
  }
});

app.listen(3000);

Drawing it

/chart.html is a single page with a canvas and a small script. It fetches /api/spread, maps the rows into two datasets, and hands them to a charting library with two y-axes: one for the spread, one for the prices. I did not want to get clever here. A line chart with a zero line is enough to see premium versus discount, and plotting the cash and futures prices alongside it answers the only follow-up question I ever had, which is "which leg moved".

public/chart.html
const rows = await (await fetch("/api/spread?symbol=" + symbol)).json();

const labels  = rows.map(r => r.date);
const spread  = rows.map(r => r.spread);
const cash    = rows.map(r => r.cash);
const futures = rows.map(r => r.futures);

// spread on the left axis, prices on the right; zero line marks premium vs discount
draw({ labels, series: [
  { name: "spread",  data: spread,  axis: "left"  },
  { name: "cash",    data: cash,    axis: "right" },
  { name: "futures", data: futures, axis: "right" }
]});

Reading the chart

Once you have a few months of history on screen, the shape is familiar for most liquid stocks. The spread starts each monthly series near its widest, decays roughly linearly as the days to expiry run down, and snaps back up on the first day of the next series. That saw-tooth is just carry, and you learn to see through it.

What you are looking for is departure from the saw-tooth. A sudden jump in the spread with no change in cash is futures buying. A spread that goes to a discount mid-series while cash is flat is futures selling, or a dividend announcement, and it is worth checking which. A spread that does not decay at all into expiry means the premium is being defended, which is a strong hint about who is on the other side.

Try it

Clone the repo, run npm install and node server.js, and open http://localhost:3000/chart.html. The data module expects a price source; the README describes what it was built against, and if that source has changed since, spreadData.js is the only file you need to edit. If you adapt it to a different exchange or a different data vendor, please send a pull request with the new source behind a flag rather than replacing the old one, so both keep working.

What I learned

Source on GitHub >