Redis keys and streams¶
Redis is where the layers meet. Every broker script writes its results under keys that start with the broker's name, every unified script writes under unified:, and the REST API reads almost everything it serves from there. This page lists every key and stream the code uses, who writes it and who reads it.
How the key names are built¶
Key names follow one pattern, <owner>:<subject>:<thing>, so the name alone says which layer wrote it. The table below explains the prefixes.
| Prefix | Owner | Example |
|---|---|---|
<broker>: |
That broker's scripts in bin/<broker>/ |
zerodha:orders:orders |
unified: |
The unified scripts in bin/unified/, the REST API and the order engine |
unified:quotes:live |
ubi:login… |
The cross-process login lock in stock_brokers/api/utilities/session.py |
ubi:login:zerodha |
| no prefix | Shared by every broker, one field per broker | last_login, settings |
Everything is stored as JSON text, because the Redis client is created with decode_responses=True. A hash holds one JSON value per field, and a plain key holds one JSON document.
Who writes and who reads each stream¶
Streams are the queues between the layers. The diagram below shows, for every stream, the script that writes it and the consumer groups that read it. Each consumer group receives every entry of its stream, independently of the others.
flowchart LR
BQ["bin/<broker>/instruments/<br/>websocket_quotes"] --> QS[["<broker>:quotes:stream"]]
BO["bin/<broker>/orders/<br/>websocket_order_details"] --> OS[["<broker>:order-updates:stream"]]
BO --> PS[["<broker>:positions_updates:stream<br/>fyers, groww, kotak, wisdom_capital"]]
QS -->|persist| BQP["store_quotes_to_db"]
OS -->|persist| BOP["store_orders_to_db"]
PS -->|persist| BPP["store_positions_to_db"]
QS -->|unified| UQ["bin/unified/instruments/<br/>websocket_quotes"]
OS -->|unified| UO["bin/unified/orders/<br/>websocket_order_details"]
PS -->|unified| UO
UQ --> UQS[["unified:quotes:stream"]]
UO --> UOS[["unified:order-updates:stream"]]
UO --> UPS[["unified:positions_updates:stream"]]
UQS -->|persist| UQP["unified store_quotes_to_db"]
UQS -.->|plain XREAD| VB["virtual_book"]
UOS -->|persist| UOP["unified store_orders_to_db"]
UOS -->|engine| OE["order_engine"]
UPS -->|persist| UPP["unified store_positions_to_db"]
API["REST API workers"] --> IS[["unified:orders:intents:stream"]]
IS -->|engine| OE
Streams¶
Every stream is trimmed approximately (MAXLEN ~) as entries are added, so it holds about its cap and never grows without limit. Entries trimmed before a group read them are lost to that group. The table below lists every stream, the field each entry carries, its cap and its readers.
| Stream | Field | Written by | Cap (about) | Consumer groups |
|---|---|---|---|---|
<broker>:quotes:stream, all ten brokers |
tick |
bin/<broker>/instruments/websocket_quotes |
1,000,000 | persist: bin/<broker>/instruments/store_quotes_to_db; unified: bin/unified/instruments/websocket_quotes |
<broker>:order-updates:stream, all ten brokers |
update |
bin/<broker>/orders/websocket_order_details |
20,000 | persist: bin/<broker>/orders/store_orders_to_db; unified: bin/unified/orders/websocket_order_details |
<broker>:positions_updates:stream, for fyers, groww, kotak, wisdom_capital |
position |
bin/<broker>/orders/websocket_order_details |
20,000 | persist: bin/<broker>/portfolio/store_positions_to_db; unified: bin/unified/orders/websocket_order_details |
unified:quotes:stream |
quote |
bin/unified/instruments/websocket_quotes |
1,000,000 | persist: bin/unified/instruments/store_quotes_to_db; also read without a group by bin/unified/orders/virtual_book |
unified:order-updates:stream |
update |
bin/unified/orders/websocket_order_details |
50,000 | persist: bin/unified/orders/store_orders_to_db; engine: bin/unified/orders/order_engine |
unified:positions_updates:stream |
position |
bin/unified/orders/websocket_order_details |
50,000 | persist: bin/unified/portfolio/store_positions_to_db |
unified:orders:intents:stream |
intent |
REST API workers, for every order placed | 10,000 | engine: bin/unified/orders/order_engine |
The caps come from STREAM_MAX_LENGTH in each writer, and QUOTES_STREAM_MAX_LENGTH in bin/unified/instruments/websocket_quotes. Where each group starts reading the first time differs:
- Every
persistgroup is created at the start of its stream, so a persister's first run archives everything already there. - The
unifiedgroup on the broker quote streams is created at the end, because a live quote cache has no use for history. Theunifiedgroup on the order and position streams starts at the beginning. - The order engine's
enginegroup starts at the beginning of the intent stream, because an intent written while the engine was down is an order somebody is still owed an answer for, and at the end of the order update stream, because older updates are about orders placed before the engine existed.
To see how far a group has fallen behind, ask Redis for the stream's groups.
Per-broker keys¶
Each broker's scripts write the keys below, with the broker's name in place of <broker>. Not every broker has every key; the notes column says which do not.
| Key | Type | Written by | Read by | Notes |
|---|---|---|---|---|
<broker>:session:status |
string | bin/<broker>/session/connect and disconnect |
People and the login units' logs | {"status", "access-token", "last_login"}, replaced on every run |
<broker>:user:details |
string | bin/<broker>/user/details |
bin/unified/user/details |
Kotak has no profile endpoint, so KotakAPI writes kotak:user:details itself when it logs in |
<broker>:quotes:live |
hash | bin/<broker>/instruments/websocket_quotes |
Nothing in the pipeline; it is for looking a tick up by hand | The latest tick per instrument, keyed by its name; not cleared at start |
<broker>:quotes:instruments |
hash | bin/<broker>/instruments/websocket_quotes |
People | Every subscribed token and its name; replaced whole at start |
<broker>:quotes:subscriptions |
set | You, with redis-cli SADD |
bin/<broker>/instruments/websocket_quotes |
The instruments to subscribe to; Zerodha has none, because its feed subscribes to the whole instrument master |
<broker>:instruments:master |
hash | bin/<broker>/instruments/daily_feed |
Zerodha's, INDmoney's and Stoxkart's websocket_quotes; bin/stoxkart/portfolio/positions |
The day's instrument master, one row per instrument as a JSON array |
<broker>:instruments:master:staging |
hash | bin/<broker>/instruments/daily_feed |
Nothing | Built first, then renamed over the master, so a reader never sees half a file |
<broker>:instruments:master:previous |
hash | bin/<broker>/instruments/daily_feed |
Nothing | The previous master, unlinked once the swap is done |
<broker>:instruments:meta |
string | bin/<broker>/instruments/daily_feed |
The same readers as the master | download_date, rows, columns (the order the arrays follow) and written_at |
<broker>:orders:orders |
hash | bin/<broker>/orders/api_order_details and websocket_order_details, through one Lua merge script |
bin/unified/orders/api_order_details; the REST API's modify, cancel and flatten routes; the order engine |
One entry per order: {"observed_at", "source", "order", "data"}; expires at the next 06:00 IST |
<broker>:orders:orders:polled_at |
string | bin/<broker>/orders/api_order_details |
bin/unified/orders/api_order_details; the order engine's recovery |
When the book was last read, so an empty book can be told apart from a stopped poller |
<broker>:orders:trades |
string | bin/<broker>/orders/api_trade_details |
bin/unified/orders/api_trade_details |
The day's trades |
<broker>:portfolio:positions |
hash | bin/<broker>/portfolio/positions, and the order update socket for the four brokers that stream positions |
bin/unified/portfolio/positions; the REST API's flatten route |
One entry per position: {"observed_at", "source", "position", "data"}; expires at the next 06:00 IST |
<broker>:portfolio:positions:polled_at |
string | bin/<broker>/portfolio/positions |
bin/unified/portfolio/positions |
When the positions were last read |
<broker>:portfolio:holdings |
string | bin/<broker>/portfolio/holdings |
bin/unified/portfolio/holdings |
The broker's holdings |
<broker>:portfolio:funds |
string | bin/<broker>/portfolio/funds |
bin/unified/portfolio/funds |
{"timestamp", "status", "code", "data"} |
wisdom_capital:session:marketdata:lock |
string | WisdomCapitalAPI |
WisdomCapitalAPI |
The pid of the process logging in to Wisdom Capital's market data API; expires after 120 seconds |
Shared hashes¶
Two hashes hold one field per broker and are read by every layer. They carry credentials and tokens, so treat Redis as holding secrets.
| Key | Type | Written by | Read by |
|---|---|---|---|
last_login |
hash, one field per broker_name |
Every broker login, after it writes MongoDB; the REST API's connect and disconnect; bin/unified/session/*. BrokerAPI._current_login fills an empty field from MongoDB with HSETNX, so it can never overwrite a newer login. |
BrokerAPI._current_login on every broker request; the REST API's token check and order routes; the order engine |
settings |
hash, one field per broker_name |
BrokerAPI.__init__, which copies the broker's MongoDB settings document every time a broker class is constructed |
The REST API's order routes and the order engine, which need each broker's keys to build an order request |
One more key without a prefix is shared by every broker. Each broker class's _request appends a line to the list broker_api_calls with RPUSH whenever it is called with verbose=True, recording the request it is about to send, headers included. Nothing reads the list, it has no cap and no expiry, and because the headers carry access tokens it holds secrets too.
Login lock keys¶
ensure_session keeps three keys per broker so that only one process logs a broker in at a time and failed logins are not retried too often. Sessions and logins explains how they are used.
| Key | Type | Holds | Lifetime |
|---|---|---|---|
ubi:login:<broker> |
string | The pid of the process logging the broker in | Set with NX and expires after 300 seconds; deleted when the login ends, or earlier when its holder has died |
ubi:login-attempt:<broker> |
string | Epoch of the last login attempt | Kept |
ubi:login-ok:<broker> |
string | Epoch of the last confirmed working session | Kept |
Unified keys¶
The unified scripts, the REST API and the order engine write the keys below. They fall into four groups: the instrument mapping, market data, orders and portfolio, and the smaller documents.
Instrument mapping¶
bin/unified/instruments/map writes the day's mapping into Redis after it has written the unified tables. The four hashes are built under :staging keys and swapped in together with the meta key, so a reader sees yesterday's complete cache or today's, never a mix.
| Key | Type | Holds | Read by |
|---|---|---|---|
unified:instruments |
hash | Every instrument mapped on the date, keyed by instrument_id, each a JSON array in the column order the meta key gives |
Unified quote, order, trade, holding and position combiners |
unified:broker_mappings |
hash | The date's mappings keyed broker:instrument_id, each [broker_token, broker_symbol, order_symbol, lot_size, tick_size] |
bin/unified/instruments/websocket_quotes, to turn a Fyers symbol into its token |
unified:broker_tokens |
hash | The reverse lookup, keyed broker:broker_token, each a list of the instrument ids that token names |
Every unified combiner that resolves a broker token |
unified:instrument_symbols |
hash | Securities by symbol, keyed segment:SYMBOL, for brokers that send no token |
Order, trade, holding and position combiners |
unified:mapping:meta |
string | mapping_date, the four counts, columns and written_at |
Every reader of the four hashes |
The same run warms the REST API's instrument catalogue under unified:catalogue:, and clears every other date's keys there. The table below lists those keys, where <date> is the ISO mapping date.
| Key | Type | Holds |
|---|---|---|
unified:catalogue:current_date |
string | The latest warmed mapping date |
unified:catalogue:warm_identifier |
string | A value that changes on every warm, so a process holding catalogue data in memory knows when to drop it |
unified:catalogue:<date>:identity |
hash | Each instrument's identity, by instrument id |
unified:catalogue:<date>:tokens:<broker> |
hash | One broker's tokens, by broker token |
unified:catalogue:<date>:order_handles |
hash | What each broker needs to place an order on the instrument, by instrument id |
unified:catalogue:<date>:contract_sizes |
hash | The day's contract size decision for currency and commodity derivatives, by instrument id |
unified:catalogue:<date>:underlyings |
hash | The day's resolved underlying of each future and option: the derivative's instrument id to its underlying's |
unified:catalogue:<date>:additional_attributes |
hash | Extra broker attributes, kept apart so the order path stays small |
unified:catalogue:<date>:segments |
hash | How many instruments each segment has |
unified:catalogue:<date>:catalogue:<segment> |
sorted set | One segment's instruments in name order |
unified:catalogue:<date>:names:<segment> |
sorted set | One segment's distinct names, which the search scans |
unified:catalogue:<date>:seen |
hash | Each instrument's first and last seen dates |
Market data¶
The unified quote combiner, the REST API and the price history job write these keys.
| Key | Type | Written by | Read by | Notes |
|---|---|---|---|---|
unified:quotes:live |
hash | bin/unified/instruments/websocket_quotes |
The REST API's ltp, ohlc and quote; bin/unified/portfolio/holdings and positions; the order engine |
One unified quote per instrument_id; quotes stale for a week are removed |
unified:quotes:fetched |
hash | The REST API | The REST API | Quotes fetched from a broker when no fresh one was cached; each field expires after two days |
unified:quotes:stats |
string | bin/unified/instruments/websocket_quotes |
People | Counts: received, unresolved, out_of_session, not_owner, no_price, duplicate, written |
unified:quotes:unresolved |
hash | bin/unified/instruments/websocket_quotes |
People | A count per broker:token:reason that did not resolve |
unified:prices:last_run |
string | bin/unified/instruments/price_history |
The REST API's candle cache | What the last run did and when it finished |
unified:prices:cache:<instrument_id>:<interval>:<basis>:<known_as_of> |
string | The REST API | The REST API | One candle series already read from the database; <known_as_of> is latest when not given; expires after a day |
Orders and portfolio¶
The combiners rewrite each document every half second, except where noted.
| Key | Type | Written by | Read by | Notes |
|---|---|---|---|---|
unified:orders:orders |
string | bin/unified/orders/api_order_details |
GET /api/orders/details |
Every broker's orders in one document |
unified:orders:trades |
string | bin/unified/orders/api_trade_details |
GET /api/orders/trades |
Every broker's trades |
unified:order-updates |
hash | bin/unified/orders/websocket_order_details |
The order engine's square-off | Latest update per order, keyed broker:order_id; expires at the next 06:00 IST |
unified:positions_updates |
hash | bin/unified/orders/websocket_order_details |
People | Latest streamed position, keyed broker:position_key; expires at the next 06:00 IST |
unified:portfolio:positions |
string | bin/unified/portfolio/positions |
GET /api/portfolio/positions; the order engine |
Every broker's positions |
unified:portfolio:holdings |
string | bin/unified/portfolio/holdings |
GET /api/portfolio/holdings |
Holdings, combined and priced; rewritten every minute |
unified:portfolio:funds |
string | bin/unified/portfolio/funds |
GET /api/portfolio/funds; the engine's daily loss check and its account_conditional orders; the lowest-cost selector's funds check, on every placement |
Every figure summed across brokers, plus each broker's own available_balance and pools |
Details, profiles and the session¶
The smaller documents are refreshed every minute or written once per login.
| Key | Type | Written by | Read by |
|---|---|---|---|
unified:user:details |
string | bin/unified/user/details, from every <broker>:user:details |
GET /api/users/details |
unified:details:users |
string | bin/unified/user/unified_details, from MongoDB user_details |
GET /api/users/details |
unified:details:brokers |
string | bin/unified/brokers/unified_details, from MongoDB broker_details |
GET /api/brokers/details |
unified:details:exchanges |
string | bin/unified/exchanges/unified_details, from MongoDB exchange_details |
GET /api/exchanges/details |
unified:session:status |
string | bin/unified/session/connect and disconnect |
People |
Order engine keys¶
The order engine and the REST API's order routes share the keys below. The engine's own caches are rebuilt from unified.synthetic_order_events on start, so losing them costs a slower start rather than a lost order.
| Key | Type | Written by | Read by | Lifetime |
|---|---|---|---|---|
unified:orders:round_robin |
string (counter) | The round-robin broker selector, with INCR on each order, when it is the configured selector |
The same selector | Kept |
unified:orders:rate:<broker> |
sorted set | The rate budget, in the order engine and the REST API, before each order message | The same budget, and the lowest-cost selector's counts script | One member per message sent in the last second, scored by Redis server time in microseconds; expires two seconds after the last message |
unified:orders:rate:<broker>:minute |
sorted set | The rate budget, for a broker whose row in unified.broker_order_costs sets orders_per_minute |
The same budget, and the lowest-cost selector's counts script | One member per message sent in the last minute; expires 61 seconds after the last message |
unified:orders:rate:<broker>:hour |
sorted set | The rate budget, for a broker whose row sets orders_per_hour |
The same budget, and the lowest-cost selector's counts script | One member per message sent in the last hour; expires an hour and a second after the last message |
unified:orders:rate:all |
sorted set | The rate budget, only when UNIFIED_BROKER_INTERFACE_API_ORDER_RATE_PER_SECOND is above zero |
The same budget | The same, across every broker |
unified:orders:daily_count:<broker> |
string (counter) | Every placement, modification and cancellation sent to a broker with a daily cap, from an API worker or the engine | The same code, before sending, and the lowest-cost selector's counts script | Expires at the next 06:00 IST |
unified:orders:engine:lock |
string | bin/unified/orders/order_engine |
A second engine, which then exits | The engine's pid; 30 seconds, refreshed every 10 |
unified:orders:intents:reply:<request_id> |
list | The order engine, with RPUSH, one answer per order of a listed placement, each naming its request_index |
The waiting API worker, with BLPOP until every order is answered |
UNIFIED_BROKER_INTERFACE_API_ORDER_ENGINE_RESULT_TTL_SECONDS |
unified:orders:intents:answer:<intent_id> |
string | The order engine, with SET NX as it answers each intent |
GET /api/orders/intents/<intent_id> |
UNIFIED_BROKER_INTERFACE_API_ORDER_ENGINE_RESULT_TTL_SECONDS |
unified:orders:intents:result:<intent_id> |
list | The order engine, with RPUSH |
The waiting API worker, with BLPOP |
UNIFIED_BROKER_INTERFACE_API_ORDER_ENGINE_RESULT_TTL_SECONDS, 300 by default |
unified:orders:parents |
hash | The order engine | The engine; bin/unified/orders/virtual_book; GET /api/orders/parents; bin/unified/orders/api_order_details and api_trade_details, which join each order and trade to its parent |
Every parent order, by id; expires at the next 06:00 IST |
unified:orders:parents:open |
set | The order engine | The engine, virtual_book and GET /api/orders/parents |
The ids of parents not yet finished; expires at the next 06:00 IST |
unified:orders:children |
hash | The order engine | The engine; the REST API's modify and cancel routes; bin/unified/orders/api_order_details and api_trade_details |
broker:broker_order_id to its parent; expires at the next 06:00 IST |
unified:orders:parents:intents |
hash | The order engine | The engine, before placing an intent | Intent id to the parent it started; expires at the next 06:00 IST |
unified:orders:virtual_queue |
hash | bin/unified/orders/virtual_book |
The engine's virtual_limit orders and plan orders on a limit_marketable trigger |
One queue estimate per held order, by parent id, or <parent id>/<path> for a plan's order; removed when the parent is no longer open |
Do not delete the engine lock by hand while an engine runs
unified:orders:engine:lock is what stops two engines reading the same intents. Two engines would each hold pending intents and could place the same order twice, with real money behind both.
Why so many keys expire at 06:00 IST¶
The order and position hashes, the engine's caches and the daily order counts all expire at the next 06:00 IST, and every write moves the expiry forward to the next 06:00. That time sits between the end of the evening commodity session and the next morning's logins, so yesterday's orders stay readable overnight and the first write of the day starts afresh. The timeline below shows where 06:00 falls in the day.
timeline
title One trading day, IST
06.00 : order, position and engine keys expire
07.00 : broker logins
07.45 : instrument download and mapping
09.00 : NSE and BSE pre-open
15.30 : NSE and BSE equity close
23.30 : MCX close