An async Rust library for fetching live market data from the National Stock Exchange of India (NSE) — no API key or account required.
Provides live equity quotes, index quotes, structured option chains, futures, intraday & historical candles (stocks, indices, futures, options), continuous futures series, polling feed loops, and EOD bhavcopy archives — with built-in rate limiting so you don't get your IP blocked.
- Live equity quotes — flat
NseQuotewith LTP, OHLCV, change, volume - Live index quotes — NIFTY 50, NIFTY BANK, INDIA VIX etc.; one or many indices per request
- Structured option chain —
OptionChaingrouped by expiry date → strike → CE/PE - Futures — all contracts for a symbol filtered from derivatives
- Continuous futures — daily front-month series stitched across expiries (expiry / days-before / OI / volume roll; ratio or difference back-adjustment), back to 2010
- Historical candles — 1/3/5/15/30/60 min intraday (30-day window) or D/W/M (25+ years) for stocks, indices, live futures and options, with correct per-bar volume
- Polling feed —
poll_quote(),poll_index()andpoll_indices()loops with error backoff - Built-in rate limiting — paced, concurrency-capped requests with client-wide cool-down when NSE pushes back
- EOD bhavcopy — equity and F&O archives parsed into typed structs
- Script token cache — symbol → charting token cached in memory; repeat candle calls cost one request
- Auto session retry — one shared cookie refresh on 403/decode failures, backoff on 429/5xx, disk-cached for 1 hour
- No OpenSSL — uses
rustlswith bundled webpki roots; cross-compiles cleanly (incl. Android)
[dependencies]
nse-rs = { git = "https://github.com/ratan00/nse-rs.git" }
tokio = { version = "1", features = ["full"] }
chrono = "0.4"use nse_rs::NseClient;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = NseClient::new();
client.init_session().await?;
// Flat live quote
let quote = client.get_stock_quote("RELIANCE").await?;
println!("{}: LTP ₹{:.2} vol {}", quote.symbol, quote.ltp, quote.volume);
// Index spot price
let nifty = client.get_index_quote("NIFTY 50").await?;
println!("NIFTY 50: {:.2} ({:+.2}%)", nifty.last, nifty.change_pct);
// Structured option chain
let chain = client.get_option_chain("NIFTY").await?;
for (expiry, rows) in &chain.expiries {
println!("=== {} ===", expiry);
for row in rows.iter().take(3) {
println!(" {:>8.0} CE {:.2} PE {:.2}",
row.strike, row.ce.ltp, row.pe.ltp);
}
}
Ok(())
}Create with NseClient::new(), then call init_session().await? before any data fetch.
Every request (including session refreshes and archive downloads) goes through a client-wide limiter. Defaults: 3 requests/s, 4 in flight. Tune with NseConfig:
use nse_rs::{NseClient, NseConfig};
let client = NseClient::with_config(NseConfig {
requests_per_sec: 10.0, // <= 0 disables pacing
max_concurrent: 8,
..NseConfig::default()
});NSE publishes no limit and tolerance varies by IP. Measure yours with cargo run --example stress -- [max_rate] [requests_per_step].
If NSE still answers 403 after a fresh session, the client pauses all requests for 15 s instead of retrying.
| Method | Returns | Description |
|---|---|---|
get_stock_quote(symbol) |
NseQuote |
Flat live quote for an equity (e.g. "SBIN") |
get_index_quote(index_name) |
NseIndexQuote |
Spot for an index (e.g. "NIFTY 50", "NIFTY BANK") |
get_index_quotes(&[names]) |
Vec<NseIndexQuote> |
Several indices from one request |
get_all_indices() |
Vec<NseIndexQuote> |
Every index NSE publishes, one request |
get_option_chain(symbol) |
OptionChain |
All options grouped by expiry/strike with CE+PE |
get_futures(symbol) |
Vec<DerivativeContract> |
All futures contracts |
get_option_contracts(symbol) |
Vec<DerivativeContract> |
Raw option contracts (unstructured) |
get_derivatives_quote(symbol) |
NextApiDerivativesResponse |
Full raw derivatives response |
get_market_status() |
MarketStatusResponse |
Open / Closed / Pre-market |
use tokio::sync::mpsc;
let (tx, mut rx) = mpsc::channel(64);
// Spawn a polling loop — sends a NseQuote every 3 seconds
tokio::spawn(async move {
client.poll_quote("INFY", 3_000, tx).await;
});
while let Some(q) = rx.recv().await {
println!("{}: ₹{:.2}", q.symbol, q.ltp);
}poll_index("NIFTY 50", interval_ms, tx) works the same way for indices. To follow several indices use poll_indices(&["NIFTY 50", "NIFTY BANK"], interval_ms, tx) — one request per tick instead of one per index.
The minimum interval is 500 ms. Slow responses never trigger catch-up bursts, and errors back off exponentially (up to 60 s). Loops stop automatically when the receiver is dropped.
use chrono::Utc;
let end = Utc::now();
let start = end - chrono::Duration::days(5);
// 5-minute intraday candles (market hours only, auto-filtered)
let candles = client.get_historical_candles("SBIN", start, end, "5").await?;
// Daily candles going back years
let daily = client.get_historical_candles("NIFTY", start, end, "D").await?;
// Live futures and option contracts, and India VIX
let fut = client.get_historical_candles("NIFTY26OCTFUT", start, end, "5").await?;
let opt = client.get_historical_candles("NIFTY26OCT22500CE", start, end, "5").await?; // monthly
let wk = client.get_historical_candles("NIFTY26O1322250CE", start, end, "5").await?; // weekly, 13-Oct
let vix = client.get_historical_candles("INDIA VIX", start, end, "5").await?;Symbol formats:
| Instrument | Format | Example |
|---|---|---|
| Stock / index | NSE name | "SBIN", "NIFTY 50", "NIFTY BANK" |
| Future | SYMBOL + YY + MON + FUT |
NIFTY26OCTFUT, BANKNIFTY26NOVFUT |
| Monthly option | SYMBOL + YY + MON + strike + CE/PE |
NIFTY26OCT22500CE |
| Weekly option | SYMBOL + YY + month code + DD + strike + CE/PE |
NIFTY26O1322250CE |
The weekly month code is 1–9 for Jan–Sep, then O, N, D. Only currently listed contracts are available — NSE drops expired contracts from the charting API (use continuous futures for history).
Supported intervals: "1" "3" "5" "15" "30" "60" (minutes, max 30-day window) or "D" "W" "M" (unlimited history).
Intraday candles are automatically filtered to 09:15–15:30 IST, so pre-open volume is excluded.
Volume by instrument:
- Stocks, futures, options: real per-bar volume, in units (shares / quantity, not lots).
- Indices (NIFTY 50, NIFTY BANK, …): intraday volume is always 0 — an index isn't traded. Daily candles do carry volume. For an intraday volume proxy use the near-month future.
- INDIA VIX: volume 0, and charting prices come back ×100 (
1412.5= 14.125).
Stock intraday volume: NSE's raw feed puts (roughly) the whole day's volume in the final bar of each day; the library detects that bar and replaces its volume with the day total minus the other bars, clamped at 0. Treat the last bar's volume of each day as an estimate. Summed bar volume lands close to the daily candle's volume, minus post-close session trades.
use chrono::NaiveDate;
use nse_rs::{Adjustment, ContinuousOptions, RollRule};
let from = NaiveDate::from_ymd_opt(2025, 1, 1).unwrap();
let to = NaiveDate::from_ymd_opt(2026, 10, 8).unwrap();
let series = client.get_continuous_futures("NIFTY", from, to, ContinuousOptions {
roll: RollRule::OpenInterest, // or Expiry, DaysBeforeExpiry(n), Volume
adjustment: Adjustment::Ratio, // or Difference, None
}).await?;
for b in &series.bars {
println!("{} {:.2} (raw {:.2}) contract {}", b.date, b.close, b.raw_close, b.contract);
}
for r in &series.rolls {
println!("roll {}: {} → {} gap {:+.2}", r.date, r.from, r.to, r.to_close - r.from_close);
}- Roll rules:
Expiryholds each contract through expiry day.DaysBeforeExpiry(n)switches once fewer thanntrading days remain.OpenInterestandVolumeswitch the first day the next contract overtakes the current one. - Adjustment: older segments are shifted so the latest segment keeps real prices.
Ratiopreserves % returns;Differencepreserves point moves.raw_close,volumeandoiare always the held contract's own values. - Raw contracts:
get_futures_daily_history(symbol, from, to)returns every contract's daily OHLC / settle / volume / OI (volume and OI in units; divide bylot_sizefor contracts). - Cost: data comes from NSE's
foCPVendpoint in 20-day windows (NSE caps each response at 70 rows), about 13 requests per year of history. - Daily only: NSE serves no intraday candles for expired contracts, so intraday series can't be stitched.
Run it: cargo run --example continuous_futures -- NIFTY 2025-01-01 2026-10-08
use chrono::NaiveDate;
let date = NaiveDate::from_ymd_opt(2025, 6, 20).unwrap();
// Equity bhavcopy
let records = client.fetch_full_bhavcopy(date).await?;
// All actively trading symbol names for a date
let symbols = client.fetch_symbol_list(date).await?;
// F&O bhavcopy — typed structs, both pre/post July-2024 formats
let fo = client.fetch_fo_bhavcopy(date).await?;
for rec in fo.iter().take(5) {
println!("{} {} {} @ {:.2} OI {}", rec.symbol, rec.expiry, rec.option_type, rec.strike, rec.oi);
}pub struct NseQuote {
pub symbol: String,
pub company_name: String,
pub ltp: f64,
pub open: f64,
pub high: f64,
pub low: f64,
pub prev_close: f64,
pub close: f64,
pub change: f64,
pub change_pct: f64,
pub volume: f64,
pub traded_value: f64,
pub year_high: f64,
pub year_low: f64,
pub last_update: String,
}pub struct OptionChain {
pub symbol: String,
// expiry date string → rows sorted by strike ascending
pub expiries: BTreeMap<String, Vec<OptionChainRow>>,
}
pub struct OptionChainRow {
pub strike: f64,
pub ce: OptionSide,
pub pe: OptionSide,
}
pub struct OptionSide {
pub ltp: f64,
pub oi: f64,
pub change_in_oi: f64,
pub volume: f64,
}pub struct ContinuousFutures {
pub symbol: String,
pub bars: Vec<ContinuousBar>,
pub rolls: Vec<RollEvent>,
}
pub struct ContinuousBar {
pub date: NaiveDate,
pub open: f64, // adjusted
pub high: f64,
pub low: f64,
pub close: f64,
pub settle: f64,
pub volume: f64, // held contract's own value, units
pub oi: f64, // held contract's own value, units
pub contract: NaiveDate, // expiry of the held contract
pub raw_close: f64, // unadjusted close
}
pub struct RollEvent {
pub date: NaiveDate, // first day on the new contract
pub from: NaiveDate,
pub to: NaiveDate,
pub from_close: f64,
pub to_close: f64,
}pub struct FoBhavRecord {
pub symbol: String,
pub expiry: String,
pub instrument_type: String, // "FUTIDX", "OPTIDX", "FUTSTK", "OPTSTK"
pub option_type: String, // "CE", "PE", or "-"
pub strike: f64,
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub settle_price: f64,
pub contracts: u64,
pub oi: u64,
pub change_in_oi: i64,
}NSE's web APIs require browser cookies (nsit, nseappid, etc.) obtained by hitting their landing page. nse-rs handles this transparently:
- Disk cache at
~/.cache/nse-rs/session.json— reused for up to 1 hour across process restarts - Auto-refresh — if a request returns a 401/403 or a decode error, the session is refreshed once (shared by every task that hit the same failure) and the request retried. 429, 5xx and network errors are retried up to twice with jittered backoff, without touching the session
- Force refresh — call
client.force_refresh_session().await?to discard and re-fetch
www.nseindia.com (live quotes, option chains, charting) enforces strict firewall rules:
- Geo-blocking — requests from IPs outside India are frequently rejected with 403 or TCP resets
- Cloud IP blocking — AWS, GCP, Azure, DigitalOcean and similar data-centre ranges are blocked even within India
Run from a residential Indian internet connection. Residential proxies are an alternative. Blocking isn't uniform — some cloud IPs get through to the JSON APIs even when the homepage returns 403 — so test from your actual host (cargo run --example stress).
The archive domain nsearchives.nseindia.com (bhavcopy downloads) does not have these restrictions and works globally.
cargo run --example demo # quotes, derivatives, option chain, candles, archives
cargo run --example continuous_futures -- NIFTY 2025-01-01 2026-10-08
cargo run --example stress -- 20 40 # find your IP's request-rate ceiling
cargo run --example fetch_symbols # all trading symbols from the bhavcopy
cargo run --example test_market_statusCredits: inspired by Python's jugaad-data and nsemine — rewritten in Rust for type safety, zero-cost abstractions, and no runtime overhead.
MIT
