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:
- A spread that widens well above carry means people are paying a premium to be long with leverage. That is crowded long positioning.
- A spread that collapses to zero or goes negative (futures at a discount) usually means heavy shorting in futures, or an expected dividend, or both.
- A spread that stays unusually wide into the last week of the series is often rolled rather than closed, which tells you the position is sticky.
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.
// 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.
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".
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
- Join on date, not on index position. The two series will not line up and the bug will be silent.
- Store the spread as both absolute and percentage. You will want each one at different times, and the second is free to compute.
- Plot the legs next to the difference. A spread chart on its own invites wrong conclusions about which side moved.
- Keep the data source in one module. It is the only part of this kind of tool that rots, and isolating it made the project survive its original source going away.
- Daily data is enough. I started with an intraday version and the extra noise was not worth the extra plumbing.