Market quotes¶
The market quote routes return the live price of one instrument, at three levels of detail. All three read the same unified quote document, which is built from whichever broker is currently streaming that instrument, so the answer looks the same whichever broker it came from.
The table below lists the three routes on this page. Each also takes a list of instruments by POST.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/instruments/ltp |
The last traded price, with the instrument's identity and when the price was received |
| POST | /api/instruments/ltp |
The same for a list of instruments in one request |
| GET | /api/instruments/ohlc |
The day's open, high, low, close and volume, and nothing else |
| POST | /api/instruments/ohlc |
The same for a list of instruments in one request |
| GET | /api/instruments/quote |
The whole unified quote document, including volume, open interest and five levels of market depth |
| POST | /api/instruments/quote |
The same for a list of instruments in one request |
All three take the instrument the same way as the instrument routes: either instrument_id, or exchange, segment and the identity fields. The POST form takes a list of them in a JSON body, as described under Several instruments at once.
Glossary of constants¶
The routes on this page return one enumerated value of their own, listed in the table below.
| Parameter | Values | Meaning |
|---|---|---|
source |
cache |
The quote came from Redis: the live feed, or a quote fetched from a broker a little earlier |
source |
broker |
No cached quote was good enough, so a broker's REST API was asked while you waited |
Where a quote comes from¶
Most of the time a quote comes straight from Redis, where bin/unified/instruments/websocket_quotes keeps one live quote per instrument from the ten brokers' websocket feeds. Only when that copy is too old does the API ask a broker directly.
The flowchart below shows the decision, step by step. "The trading window" is the instrument's session as the tick pipeline defines it, on the exchanges' trading days.
flowchart TD
A["Request for one instrument"] --> B["Read unified:quotes:live<br/>and unified:quotes:fetched"]
B --> C["Take whichever was<br/>received more recently"]
C --> D{"a quote exists and<br/>is not marked stale?"}
D -- no --> F
D -- yes --> E{"received in the<br/>last 5 minutes?"}
E -- yes --> OK1["Answer with it<br/>source: cache"]
E -- no --> W{"its trading window closed<br/>since, and has not reopened?"}
W -- yes --> OK1
W -- no --> F{"any broker with a REST quote<br/>module lists this instrument?"}
F -- no --> E1["503 no recent quote is cached, and no broker<br/>that serves quotes carries this instrument"]
F -- yes --> G["Try each such broker<br/>in priority order"]
G --> H{"one answered<br/>with a usable price?"}
H -- yes --> S["Store in unified:quotes:fetched<br/>for two days"] --> OK2["Answer with it<br/>source: broker"]
H -- no --> E2["503 no recent quote is cached,<br/>and every broker failed - ..."]
The two freshness rules exist because most instruments trade thinly. A quote that is five minutes old is usually still the market, and once a session is over the last quote stays right until the next session opens, so nothing is fetched after hours for an instrument that was quoted during the day.
Which brokers can answer while you wait¶
Six brokers have a REST quote module in service. The table below lists every broker and whether it can be asked for a quote while you wait. The live websocket feed is a separate matter, covered in Market data.
| Broker | REST quote fallback | Why |
|---|---|---|
| zerodha | In service | |
| dhan | In service | |
| kotak | In service | |
| flattrade | In service | |
| shoonya | In service | |
| indmoney | In service | |
| fyers | A module exists but is held back. Its field mapping is unverified, because every probe met Fyers' request limit. | |
| groww | A module exists but is held back. The account is not entitled to live data, and every quote request is refused with 403. | |
| stoxkart | No REST quote module | |
| wisdom_capital | No REST quote module |
The brokers that list the instrument and are in service are tried in the unified tick layer's priority order, with verified brokers first. Zerodha is the only verified broker today. After it, the order is Dhan, Kotak, Flattrade, Shoonya and INDmoney on NSE, BSE and MCX. On NCDEX the priority list names only Shoonya and Wisdom Capital, so Shoonya is tried first there and the others follow in no preferred order. The first broker that returns a usable last price wins. If a broker's session has died, the API logs that broker in again once and retries.
A fetched quote is normalized by the same code that normalizes the live feed, so it has exactly the same fields. It is stored in unified:quotes:fetched, never in unified:quotes:live, and each entry expires after two days.
A broker fallback costs time
A source: broker answer includes a round trip to a broker, and possibly a login. A client that polls prices for many instruments should rely on the live feed covering them, and treat source: broker as the exception.
Several instruments in one request¶
A POST runs the same decision for every instrument in its list, but it reads Redis for all of them at once. One pipeline reads both hashes for every instrument, and one read fetches the broker handles of the instruments whose cached quote was not good enough. Only those instruments are then asked of a broker.
When more than one instrument needs a broker, up to four are fetched at the same time, each trying its brokers in the priority order above. A list with several quiet instruments therefore waits for roughly its slowest broker call rather than for every call in turn. An instrument whose brokers all fail gets its own 503 entry, and the rest of the list is still answered.
The unified quote document¶
All three routes answer from one document shape, which the quote route returns whole. The table below lists every field, in the order the document holds them. ltp returns a subset, marked in the last column. ohlc returns its own five fields built from this document, described under OHLC.
| Field | Type | Description | ltp |
|---|---|---|---|
instrument_id |
string | The instrument's UUID | |
broker |
string | The broker this quote came from | |
broker_token |
string | That broker's token for the instrument | |
exchange |
string | The exchange | |
segment |
string | The exchange-prefixed segment | |
shape |
string | security, future or option |
|
symbol |
string or null | The symbol, for a security | |
underlying_symbol |
string or null | The underlying, for a derivative | |
expiry_date |
string or null | The expiry, YYYY-MM-DD |
|
strike_price |
number or null | The strike, for an option | |
option_type |
string or null | CE or PE |
|
lot_size |
integer or null | Units of the underlying in one lot | |
last_price |
number | The last traded price | |
average_price |
number or null | The day's volume-weighted average price, as the broker reports it | |
ohlc |
object | The day's open, high and low. There is no close in it. |
|
previous_close |
number or null | The previous session's close | |
change_percent |
number or null | The change from previous_close, as a percentage rounded to four places, recomputed by the project rather than taken from the broker |
|
last_quantity |
integer or null | The size of the last trade, in units | |
volume |
integer or null | The day's traded volume, in units | |
buy_quantity |
integer or null | Total quantity bid, in units | |
sell_quantity |
integer or null | Total quantity offered, in units | |
oi |
integer or null | Open interest, for a derivative | |
oi_day_high |
integer or null | The day's highest open interest | |
oi_day_low |
integer or null | The day's lowest open interest | |
depth |
object | buy and sell, each a list of up to five levels of price, quantity and orders, with empty levels dropped |
|
last_trade_time |
number or null | When the last trade happened, in epoch seconds | |
exchange_time |
number or null | The exchange's timestamp on the update, in epoch seconds | |
received_at |
number | When the project received the tick, in epoch seconds | |
unified_at |
number | When the quote document was built, in epoch seconds | |
stale |
boolean | true when the owning broker's feed went silent and no healthy backup took over |
|
stale_since |
number or null | When the quote became stale, in epoch seconds | |
source |
string | cache or broker, added by the API |
Quantities are always in units, not lots. A broker that reports quantities in lots has them multiplied up to units before they are stored. Prices are rounded to four decimal places for currency instruments and two for everything else.
A cached quote marked stale is never served. The API goes to a broker instead, so a stale field you receive is always false.
LTP¶
GET /api/instruments/ltpaccess-token
This route returns the last traded price with the instrument's identity and the two timestamps that say how fresh it is. Use it when the price is all you need.
Request parameters¶
| Name | In | Type | Required | Description |
|---|---|---|---|---|
access-token |
header | string | Yes | The token from connect |
instrument_id |
query | string | One spelling | The instrument's UUID |
exchange, segment and the identity fields |
query | string | The other spelling | See Naming an instrument |
Example¶
Response¶
The id below is a placeholder, and the price and times are illustrative.
{
"instrument_id": "11111111-1111-5111-8111-000000000002",
"exchange": "nse",
"segment": "nse_equities",
"shape": "security",
"symbol": "INFY",
"underlying_symbol": null,
"expiry_date": null,
"strike_price": null,
"option_type": null,
"last_price": 1521.4,
"last_trade_time": 1790410198.0,
"received_at": 1790410198.412,
"source": "cache"
}
Response attributes¶
Every field is described in The unified quote document. The route returns exactly the fields marked in its ltp column.
Status codes¶
| Status | When |
|---|---|
| 200 | A quote was found, in Redis or at a broker. |
| 400 | A parameter could not be used, with the messages listed under instrument details. |
| 401 | Access token is required, Invalid access token or Access token has expired. |
| 404 | no instrument <id or fields> is mapped on <mapping date>. |
| 503 | no recent quote is cached, and no broker that serves quotes carries this instrument, no recent quote is cached, and every broker failed - <broker>: <reason>; ..., or no instruments have been mapped yet. |
LTP for several instruments¶
POST /api/instruments/ltpaccess-token
The POST form takes a list of instruments in a JSON body and answers {"results": [...]}, one entry per instrument in request order, with each answered entry's data exactly as above. Several instruments at once describes the body, the entries and the statuses.
curl -X POST "http://127.0.0.1:8080/api/instruments/ltp" \
-H "access-token: $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"instruments": [
{"instrument_id": "11111111-1111-5111-8111-000000000002"},
{"exchange": "nse", "segment": "equities", "symbol": "RELIANCE"},
{"exchange": "nse", "segment": "equity_index_options", "underlying_symbol": "NIFTY", "expiry_date": "2026-10-27", "strike_price": 25000, "option_type": "CE"}
]
}'
import os
import requests
response = requests.post(
'http://127.0.0.1:8080/api/instruments/ltp',
headers={
'access-token': os.environ['ACCESS_TOKEN'],
},
json={
'instruments': [
{
'instrument_id': '11111111-1111-5111-8111-000000000002',
},
{
'exchange': 'nse',
'segment': 'equities',
'symbol': 'RELIANCE',
},
],
},
timeout=30,
)
for result in response.json()['results']:
if result['status'] == 200:
print(result['data']['symbol'], result['data']['last_price'], result['data']['source'])
else:
print(result['request_index'], result['status'], result['error'])
The example below shortens each data object to a few of its keys; a real one carries every key the GET form returns. The values are illustrative.
{
"results": [
{"request_index": 0, "status": 200, "data": {"instrument_id": "11111111-1111-5111-8111-000000000002", "symbol": "INFY", "last_price": 1521.4, "source": "cache", "...": "..."}},
{"request_index": 1, "status": 200, "data": {"instrument_id": "11111111-1111-5111-8111-000000000003", "symbol": "RELIANCE", "last_price": 2875.5, "source": "broker", "...": "..."}},
{"request_index": 2, "status": 503, "error": "no recent quote is cached, and every broker failed - zerodha: Kite returned no quote"}
]
}
A POST can take longer than a GET when several instruments need a broker, so allow a longer client timeout than for one instrument.
Under the hood
- Route:
InstrumentsBlueprint.ltpinunified_broker_interface/blueprints/instruments.py, which keeps only the keys in_LTP_KEYS. - Quote:
QuoteService.quoteinunified_broker_interface/utilities/broker_quotes/utilities/service.py, which passes a list of one toQuoteService.quotes. APOSTcallsquoteswith the whole list, andQUOTE_FETCH_THREADS(4) caps how many broker fetches run at once. - Redis: reads
unified:quotes:liveandunified:quotes:fetchedin one pipeline, and writesunified:quotes:fetchedwith a per-field expiry of two days after a broker fetch. - Broker modules:
unified_broker_interface/utilities/broker_quotes/<broker>.py, one per broker that has one, each subclassingBrokerQuoteSource.
OHLC¶
GET /api/instruments/ohlcaccess-token
This route returns only the day's open, high, low, close and volume. It carries no identity, timestamps or source, so use quote when you need those. The quote document has no field for the day's close, so close is the last traded price, which becomes the day's close once the market has shut.
Request parameters¶
| Name | In | Type | Required | Description |
|---|---|---|---|---|
access-token |
header | string | Yes | The token from connect |
instrument_id |
query | string | One spelling | The instrument's UUID |
exchange, segment and the identity fields |
query | string | The other spelling | See Naming an instrument |
Example¶
import os
import requests
response = requests.get(
'http://127.0.0.1:8080/api/instruments/ohlc',
headers={
'access-token': os.environ['ACCESS_TOKEN'],
},
params={
'instrument_id': '11111111-1111-5111-8111-000000000002',
},
timeout=10,
)
candle = response.json()
print(candle['open'], candle['high'], candle['low'], candle['close'], candle['volume'])
Response¶
The id below is a placeholder, and the prices and times are illustrative.
Response attributes¶
| Field | Type | Description |
|---|---|---|
open |
number or null | The day's open, from the quote document's ohlc |
high |
number or null | The day's high, from the quote document's ohlc |
low |
number or null | The day's low, from the quote document's ohlc |
close |
number or null | The last traded price, from the quote document's last_price |
volume |
integer or null | The day's traded volume in units, from the quote document's volume |
Status codes¶
The status codes are the same as for ltp.
OHLC for several instruments¶
POST /api/instruments/ohlcaccess-token
The POST form takes a list of instruments in a JSON body and answers {"results": [...]}, with each answered entry's data exactly as above. It works exactly like ltp for several instruments, and Several instruments at once describes the body, the entries and the statuses.
Under the hood
- Route:
InstrumentsBlueprint.ohlc, which builds its answer from the whole quote document with_ohlcv_values. - When a broker's own previous close is missing, a fetched quote keeps the previous close of the cached quote it replaces, as long as both are from the same India day.
Quote¶
GET /api/instruments/quoteaccess-token
This route returns the whole unified quote document: every field in the table above, including which broker supplied it, volume, open interest and five levels of market depth.
Request parameters¶
| Name | In | Type | Required | Description |
|---|---|---|---|---|
access-token |
header | string | Yes | The token from connect |
instrument_id |
query | string | One spelling | The instrument's UUID |
exchange, segment and the identity fields |
query | string | The other spelling | See Naming an instrument |
Example¶
import os
import requests
response = requests.get(
'http://127.0.0.1:8080/api/instruments/quote',
headers={
'access-token': os.environ['ACCESS_TOKEN'],
},
params={
'exchange': 'mcx',
'segment': 'commodity_futures',
'underlying_symbol': 'CRUDEOIL',
'expiry_date': '2026-10-19',
},
timeout=10,
)
depth = response.json()['depth']
print(depth['buy'][0], depth['sell'][0])
Response¶
The example below is shortened to two depth levels a side. The id and token are placeholders, and the prices, quantities and times are illustrative.
{
"instrument_id": "11111111-1111-5111-8111-000000000001",
"broker": "zerodha",
"broker_token": "123456",
"exchange": "mcx",
"segment": "mcx_commodity_futures",
"shape": "future",
"symbol": null,
"underlying_symbol": "CRUDEOIL",
"expiry_date": "2026-10-19",
"strike_price": null,
"option_type": null,
"lot_size": 100,
"last_price": 5712.0,
"average_price": 5698.35,
"ohlc": {"open": 5680.0, "high": 5725.0, "low": 5671.0},
"previous_close": 5690.0,
"change_percent": 0.3866,
"last_quantity": 100,
"volume": 1523400,
"buy_quantity": 41200,
"sell_quantity": 39800,
"oi": 1842300,
"oi_day_high": 1861000,
"oi_day_low": 1822100,
"depth": {
"buy": [
{"price": 5711.0, "quantity": 800, "orders": 6},
{"price": 5710.0, "quantity": 1500, "orders": 11}
],
"sell": [
{"price": 5712.0, "quantity": 600, "orders": 4},
{"price": 5713.0, "quantity": 1300, "orders": 9}
]
},
"last_trade_time": 1790436598.0,
"exchange_time": 1790436598.0,
"received_at": 1790436598.231,
"unified_at": 1790436598.233,
"stale": false,
"stale_since": null,
"source": "cache"
}
Response attributes¶
Every field is described in The unified quote document.
Status codes¶
The status codes are the same as for ltp.
Quote for several instruments¶
POST /api/instruments/quoteaccess-token
The POST form takes a list of instruments in a JSON body and answers {"results": [...]}, with each answered entry's data exactly as above. It works exactly like ltp for several instruments, and Several instruments at once describes the body, the entries and the statuses.
Under the hood
- Route:
InstrumentsBlueprint.quote, which returns the document fromQuoteService.quoteunchanged apart fromsource. - The document layout is written in one place,
quote_documentinstock_brokers/instruments/ticks/utilities/pipeline.py, which both the live feed and the broker fallback call. That is why a fetched quote and a streamed one differ only insource. - Which broker owns a streamed instrument, and when a backup takes over, is described in Market data.