A real-time open-interest intelligence terminal for the Indian derivatives market.
Built on the official Zerodha Kite Connect API, it reads the entire option chain of the NSE F&O universe tick by tick, works out where institutional money is actually being committed rather than merely traded, and narrows roughly nine thousand live contracts down to the handful of names being accumulated in one direction right now — while the session is still running.
docker compose up and it is on screen.
| ~8,800 | ~160 | ~3.3 M | microseconds |
| contracts streamed live | F&O underlyings tracked | instrument-minutes recorded per session | from OI tick to fired signal |
| 8 | 60 s | 100 % | 1 |
| decision boards on screen | full-universe sweep cadence | of every session replayable offline | machine, one API key |
Understanding it Why most OI screeners fail · Design principles · System architecture · One minute of the engine · The conviction funnel · Rules or a learned model · The boards
Running it Data source — Kite Connect · Repository layout · Data model · API surface · Getting started · Running with Docker · Daily operating procedure · Verifying the connection · Troubleshooting · Replay and validation · Scale and performance
The rest Contributing · Note on the published source · Disclaimer
Open interest is the only public number that tells you how much money is committed to a position rather than merely passing through it. Volume says a trade happened. Open interest says somebody is still standing there, exposed, overnight.
The catch is that the exchange publishes it as a raw total, per contract, with no direction attached. Every open contract has a buyer and a seller. So a screener that ranks stocks by "OI change" is ranking a number whose sign means nothing — and one that ranks by absolute rupee flow will return the same five index-heavyweight names every single day, because they are large, not because anything is happening in them.
Clavis Terminal treats open interest as a flow problem instead of a level problem.
For every contract in the universe it builds one-minute bars, measures the change in open interest against that contract's own recent behaviour, pairs the change with the simultaneous price change to infer which side initiated it, converts it to rupee notional, and aggregates it per stock across four separate channels:
| channel | what it means | reads as |
|---|---|---|
| Call buying | fresh long calls, OI up with premium up | bullish commitment |
| Put writing | fresh short puts, OI up with premium down | bullish commitment |
| Put buying | fresh long puts, OI up with premium up | bearish commitment |
| Call writing | fresh short calls, OI up with premium down | bearish commitment |
| Short covering | OI down, price up — trapped shorts buying back | exit fuel, directional |
| Long unwinding | OI down, price down — longs giving up | exit fuel, directional |
That decomposition is the foundation the entire terminal is built on. Everything above it — the ranking, the radars, the funnel — is an opinion about the same underlying quantity: where is committed money going, right now, in this stock, relative to how this stock normally behaves.
The second half of the answer matters as much as the first. A mid-cap building four times its own typical minute-flow is a genuine event. The same rupee figure in a large private bank is an ordinary Tuesday. Every magnitude in this system is normalised against the stock's own trailing baseline before it is allowed to rank — which is precisely why the board surfaces names that a flat rupee ranking structurally cannot find.
|
Entry-time features do not separate the names that run from the names that fade. Build size, flow dominance and radar strength look statistically identical at the entry minute for both groups. So the system does not forecast. It opens small positions and makes each one earn promotion by continuing to be right. |
Every candidate opens at reduced size, under a per-side and per-day budget that shrinks as the tape gets choppier, behind a hard stop and a forced close before the bell. The asymmetry is the strategy: many small, bounded rejections; a few concentrated, fully-ridden survivors. |
Every signal on screen carries the gate it passed, the threshold it crossed and the value that crossed it. Nothing fires that a human cannot read, argue with, and override. That constraint is not a limitation of the design — it is the design. |
|
Every session is recorded in full. Any recorded day can be pushed back through the exact same engine objects — same state machine, same ranking pass, same funnel — with no market and no broker connection. A change is not "an improvement" until it has been replayed against a day it was never fitted to. |
Feeds stall. Networks drop. Processes die at 11:04. A material share of this codebase is watchdogs, gap repair, crash-safe writes, warm restart and single-instance locking — because a screener that goes quietly stale is worse than no screener at all. |
Absolute rupee flow ranks the largest companies. Flow relative to a stock's own trailing behaviour ranks the unusual ones. Every board in this system divides by the stock's own baseline before sorting. That single decision is what makes the output actionable instead of decorative. |
Four layers, deliberately decoupled.
The engine never knows that a dashboard exists. It publishes to an in-process pub/sub bus, and the console printers, the SQLite store and the API server are all just subscribers. Adding a Telegram alerter, a webhook, or a broker order-router is a new subscriber — not an engine change, not a refactor, not a risk to the hot path.
Concurrency is handled by removing it. Broker WebSocket callbacks run on their own threads and do exactly one thing: hand ticks to a thread-safe queue. A single consumer thread owns all mutable state. That eliminates an entire category of bug that would otherwise be very hard to reproduce and very expensive to discover live.
The cost of the hot path is kept flat on purpose. Z-scores are computed against cached rolling mean and standard deviation, so the per-tick check is O(1) rather than a window recomputation. That is what makes instant detection across nine thousand contracts affordable on a single desktop machine.
Two detection paths run side by side.
The instant path fires the moment an OI tick lands. It does not wait for the minute to close, it does not batch, and it does not queue behind the ranking sweep. A contract whose open interest jumps abnormally against its own baseline is on the screen before the minute it happened in has ended.
The minute sweep does the work that needs the whole universe in one consistent snapshot: close every bar, carry forward the contracts that traded nothing, decompose flow four ways, rank all stocks in rupee notional, read the day regime, then run the funnel and every supporting board — in that order, with the outputs of each feeding the next.
One honest limit, stated up front: the exchange disseminates open interest with a lag of one to three minutes. That is the floor for any OI-based screener, this one included. It is also exactly why the instant path exists — the software contributes microseconds to a delay the exchange has already spent minutes on, and there is no version of this problem where adding batching helps.
This is the central design decision, and the one most worth understanding.
The system does not try to pick winners at the moment of entry, because that does not work. At the entry minute, the names that go on to trend and the names that immediately fade have indistinguishable distributions on every feature worth measuring — build magnitude, flow dominance, near-spot participation. The losers often score better on dominance.
So the funnel stops trying to be a prediction layer and becomes a confirmation layer.
A stock that clears the entry gates opens as a probe: small, cheap, tightly stopped, counted against a daily budget. From there it is not judged on what it looked like — it is judged on what it keeps doing. Four legs, all of which must hold at the same minute:
| leg | question | why this one |
|---|---|---|
| 01 | Has it survived long enough without hitting its stop? | Weak setups eliminate themselves fast. Time alone removes a large share of them at minimal cost. |
| 02 | Has it printed a fresh session extreme since entry? | Distinguishes a position that is still going from one that stalled the minute you joined it. |
| 03 | Is cumulative flow still backing the same side? | Confirms the money that caused the move has not started leaving through the door it came in. |
| 04 | Is the ride actually in profit past the floor? | The market's own verdict, and the only leg that cannot be argued with. |
Clear all four and the probe is promoted to the Top Board — sticky for the day, held to the close. The board is small on purpose. Reaching it is meant to be difficult.
Two live meters make the state legible while it happens: a promotion energy bar showing how many legs a probe has satisfied, and an exit-risk meter showing how close a ride is to the nearest of its stop, its trail, or the closing bell. Both are read-outs. Neither drives a decision — the rules do that, and the meters just let a human see the rules thinking.
Above all of it sits the day-regime meter, reading the cleanliness breadth of the live futures tape and classifying the session as TREND, MIXED or CHOP. The regime sets the daily entry budget per side, the advisory size multiplier and the hard-stop distance — and on a chop day it restricts entries to structural setups only. The single most reliable way to improve a day's outcome is to take fewer positions on the days that punish them, and that is a decision the system makes on its own, from the tape, before most of the damage is available to be done.
This question comes up immediately, so here is the position in full.
The hard part of this problem was never classification. It was search.
Deciding whether one particular setup is good is a small question. Finding which few names, out of nine thousand live contracts across a hundred and sixty stocks, are being accumulated one-directionally at 09:47 — that is a search problem over live flow, and it is the part that actually determines whether the day works.
The rule engine performs that search exhaustively, every minute, over the entire universe, with a reason attached to every hit. Nothing is sampled, nothing is approximated, and no candidate is skipped because a model assigned it a low prior. A learned ranker would be asked to approximate the same ordering, from far less data, and would hand back a score instead of an explanation.
Two structural facts make that trade a poor one at this stage of the system's life:
- The data does not exist yet in the quantity a model wants. Minute-level option open interest is not sold by vendors. It has to be self-recorded, one session at a time. A rule that measures each stock against its own trailing baseline works correctly on day one; a model that has to learn cross-sectional structure from scratch needs hundreds of sessions before its estimates separate from noise.
- Silent failure is the expensive kind. When a rule stops passing, the board goes quiet, and quiet is information. When a learned model's relationship decays, its confidence frequently does not — it keeps producing well-formed, high-scoring, wrong answers, and the first honest signal is the drawdown.
None of that says learning has no place here — it says learning belongs on top of the search, not in place of it. A model that re-ranks an exhaustive, explainable candidate set is strictly better positioned than one asked to do the search itself: it inherits the full coverage, keeps the audit trail, and cannot silently drop a name it never scored.
The groundwork for that already exists as a side effect of how the system is built. Every per-minute metric the engine computes is recorded to disk, which means the archive is a labelled dataset, growing every session, leak-free by construction because replay can reproduce any day exactly as it was seen live.
The search runs on rules. Anything learned re-ranks what the search already found.
Every number in this terminal comes from the official Zerodha Kite Connect API. There is no scraping, no unofficial endpoint, no third-party data vendor and no delayed public feed anywhere in the pipeline. The system is built directly against the broker's own market-data interface and respects its documented limits by design rather than by luck.
Exactly four Kite capabilities are used:
| Kite capability | Used for | Where |
|---|---|---|
| KiteTicker WebSocket full mode |
The live tick stream — last price, traded volume and open interest on every contract in the universe. | feed/ticker_manager.py, feed/tick_queue.py |
| Instruments dump REST |
The daily contract master: strikes, expiries, lot sizes and instrument tokens, used to assemble each stock's option chain and near-month future. | universe.py |
| Historical data REST |
Minute-bar backfill, used only to repair a window the live socket missed. Never used as a primary source. | feed/backfill.py |
| Session / login REST |
The daily request_token → access_token exchange, written straight back into .env. |
auth.py |
| Kite constraint | How Clavis Terminal handles it |
|---|---|
| 3 WebSocket connections per API key | ticker_manager.py runs a pool of exactly three, never more, and treats the pool as a single logical feed. |
| 3,000 instruments per connection | universe.py enforces a hard capacity budget below 3 × 3,000 and trims the furthest out-of-the-money strikes first, so the near-spot chain — where the signal lives — is never sacrificed. |
| ~3 requests/second on historical REST | backfill.py is rate-limited below that ceiling and only ever runs after a detected gap. |
| Access token expires daily (~07:30 IST) | auth.py performs the exchange once each morning and writes the token to .env; the live loop re-checks token validity before blaming the network for a stall. |
| Only one process may hold the three sockets | A PID-checked single-instance lock refuses a second screener rather than letting two terminals show different partial universes. |
You need your own Kite Connect subscription to run this live. It is a paid developer product from Zerodha, separate from a trading account. Sign up at kite.trade — setup is covered step by step in Getting started below. Without one, the dashboard still runs in demo mode against generated fixtures, and
replay.pystill runs against any recording you have.
Eight surfaces, each answering one question, all recomputed every minute and all persisted.
| Board | What it answers |
|---|---|
| Conviction Funnel | Which stocks are being accumulated one-directionally right now, which of those have proven themselves, and how close each is to promotion or removal. |
| OI Build-up Ranking | Which stocks lead the universe on buy-side and write-side option flow this minute, in rupee crore, with the dominant strike on each side. |
| Big Player Radar | Where near-spot strike OI is building faster than that contract's own normal pace — positioning, before it becomes price. |
| Position Radar | Per stock, where in the chain size is accumulating, and whether the name is loaded but still quiet, building, or already moving. |
| OI Build-up Multiple | Today's fresh open interest measured against the prior session's close, ranked by multiple rather than by absolute size — the small-base breakouts. |
| Sector Leaders | The strongest NSE sector groups and the one-way leaders inside them, for rotation that starts at the sector and ends at a single name. |
| Relative Strength Leaders | The day's top names by strength against an equal-weight basket, snapshotted once at a fixed decision time. |
| Signal Feed | The raw spike stream, tick-instant, with z-score and OI percentage on every line. |
clavis-terminal/
├── docker-compose.yml engine + API + terminal, one command
├── .github/
│ ├── workflows/ci.yml lint, tests, both builds, image builds, secret scan
│ ├── ISSUE_TEMPLATE/ bug report + feature request forms
│ └── PULL_REQUEST_TEMPLATE.md
├── backend/
│ ├── Dockerfile python:3.11-slim, data and .env bind-mounted
│ ├── pyproject.toml ruff + pytest configuration
│ ├── main.py orchestrator: auth → universe → feed → loop → EOD
│ ├── api.py FastAPI REST + WebSocket bridge
│ ├── auth.py daily Kite login, token written to .env
│ ├── config.py every path, window and threshold
│ ├── universe.py option chain + futures resolution, capacity guard
│ ├── replay.py offline replay of a recorded session
│ ├── stocks.txt the monitored symbol list
│ ├── engine/
│ │ ├── market_state.py per-instrument live state, session extremes, VWAP
│ │ ├── bar_builder.py one-minute OHLCV + OI bars
│ │ ├── oi_engine.py rolling OI deltas and cached z-scores
│ │ ├── classifier.py OI × price → build-up / unwinding types
│ │ ├── spike_detector.py composite spike signals, both paths
│ │ ├── ranking.py four-way flow, roll-aware netting, rupee notional
│ │ ├── filters.py straddle pin, OI wall runway, VWAP, relative volume
│ │ ├── regime.py TREND / MIXED / CHOP day meter
│ │ ├── oneway_momentum.py the conviction funnel
│ │ ├── big_player_radar.py near-spot strike build pace
│ │ ├── position_radar.py per-stock accumulation map
│ │ ├── oi_multiple.py today's OI vs prior close, ranked by multiple
│ │ ├── rs_leaders.py relative-strength leaders (shadow board)
│ │ └── sector_leaders.py sector rotation leaders (shadow board)
│ ├── feed/
│ │ ├── ticker_manager.py three-socket pool, reconnect, stall watchdog
│ │ ├── tick_queue.py thread-safe callback → engine bridge
│ │ └── backfill.py historical REST gap repair
│ ├── sinks/
│ │ ├── signal_bus.py pub/sub hub + console printers
│ │ ├── storage.py Parquet bar writer + SQLite store with migrations
│ │ └── baselines.py cross-day per-stock flow baselines
│ └── tests/
│ └── test_oneway.py end-to-end smoke test of the funnel
├── dashboard/
│ ├── Dockerfile multi-stage build → Next.js standalone runtime
│ ├── app/ Next.js 14 App Router entry, layout, global styles
│ ├── components/ one component per board
│ ├── hooks/
│ │ ├── useWebSocket.ts reconnecting client with backoff
│ │ └── useLiveData.ts reducer mapping socket frames → app state
│ └── lib/ shared types and demo-mode fixtures
└── docs/assets/ architecture, funnel and pipeline diagrams
backend/data/bars/{YYYY-MM-DD}/part_####.parquet
Written by a buffered background thread into numbered part files, so an unexpected shutdown at any point costs at most one flush interval and never corrupts an existing part. Each row is one instrument-minute:
| column | meaning |
|---|---|
minute |
bar timestamp |
token, tradingsymbol, underlying, kind, strike, expiry |
instrument identity |
open, high, low, close |
price |
volume, oi, oi_delta |
traded volume, open interest, change in open interest |
backfilled |
set when the row was repaired from historical REST rather than the live feed |
backend/data/signals/signals.sqlite
| table | contents |
|---|---|
signals |
every fired spike, with score, z-score and OI percentage |
rankings |
the per-minute per-stock flow ranking snapshot |
oneway_movers |
funnel rows: entries, open minutes, promotions and exits |
big_player_radar |
near-spot build-pace flags |
position_builds |
per-stock accumulation state |
oi_multiple_board |
OI multiple rows against the prior close base |
sector_leaders, rs_leaders |
shadow board snapshots |
trade_ideas, momentum_picks, early_entries, best_picks |
earlier generations of the decision surface, retained for audit |
feed_gaps |
recorded outage windows, so later analysis knows which minutes are artifact |
Schema changes ship as additive PRAGMA table_info + ALTER TABLE migrations that run at
startup. An existing database is never rebuilt, never dropped, never lost.
backend/data/meta/ holds the daily instrument snapshot, per-token day references (previous
close and exchange open, learned from live ticks), the prior-session end-of-day OI snapshot, the
single-instance lock, and flow_baseline.csv — a trailing-window median of each stock's own
typical minute flow.
That last file is what makes "unusual for this stock" a computable statement rather than a slogan.
FastAPI, started in-process with the screener via --api.
| method | route | returns |
|---|---|---|
GET |
/api/status |
market open, instrument count, signals today, feed health |
GET |
/api/signals/today |
today's spike signals |
GET |
/api/rankings/latest |
most recent per-stock flow ranking |
GET |
/api/oneway/latest |
current funnel state — top board and probes |
GET |
/api/radar/latest |
near-spot build-pace flags |
GET |
/api/position/latest |
per-stock accumulation map |
GET |
/api/oimult/latest |
OI multiple board |
GET |
/api/sector/latest |
sector leader board |
ws://localhost:8000/ws/live
Sends a full state snapshot on connect, then incremental frames. The bridge moves rows from the synchronous engine thread onto an asyncio queue and fans them out to every connected browser at a fixed tick, with a periodic status frame so the dashboard's LIVE indicator flips at the open and the close without needing a reconnect.
Two ways to run it. Docker is one command and needs nothing installed but Docker itself. The native path below gives you a live Python environment for editing rules and running replays.
flowchart LR
A["Kite Connect<br/>app created"] --> B["backend/.env<br/>key + secret"]
B --> C["python auth.py<br/>daily token"]
C --> D["python main.py --api<br/>engine + API on :8000"]
D --> E["npm run start<br/>terminal on :3000"]
E --> F["browser opens<br/>ws://localhost:8000/ws/live"]
style A fill:#3d2a12,stroke:#ffa028,color:#ffd7a3
style D fill:#12302a,stroke:#4af6c3,color:#a9f5e0
style F fill:#1a2438,stroke:#6db3f2,color:#bcd9f7
| Python | 3.11 or newer |
| Node.js | 18 or newer |
| Kite Connect | a paid developer subscription with API key and secret — kite.trade |
| OS | Windows, macOS or Linux — the engine is pure Python, the dashboard is pure Node |
This is the only step that happens outside the repository, and it is a one-time setup.
- Go to developers.kite.trade and sign in with your Zerodha account.
- Create a new app. Type: Connect. Give it any name.
- Set the Redirect URL. Kite sends you here after login with the
request_tokenattached. Anything you can read the address bar of works —http://127.0.0.1/is the simplest choice for a local setup, and nothing needs to be listening on it. - Subscribe to the app. Kite Connect is billed monthly and is separate from your trading account.
- Copy the API key and API secret from the app page. These two values never change and never leave your machine.
A note on how the daily login works. Kite issues a short-lived
access_tokenthat expires around 07:30 IST every morning. You cannot skip this, and it cannot be automated away — the exchange requires an interactive login.auth.pyreduces it to: open a URL, log in, paste one string back. Roughly fifteen seconds, once a day.
git clone https://github.com/<your-user>/Clavis-Terminal.git
cd Clavis-Terminal/backend
python -m venv .venv
# Windows
.venv\Scripts\activate
# macOS / Linux
source .venv/bin/activate
pip install -r requirements.txtCreate backend/.env from the template:
cp .env.example .envKITE_API_KEY=your_api_key
KITE_API_SECRET=your_api_secret
KITE_ACCESS_TOKEN=
.envis git-ignored and must stay that way. LeaveKITE_ACCESS_TOKENblank —auth.pywrites it for you and rewrites it every morning.
Edit backend/stocks.txt — one NSE F&O underlying per line, comments with #:
RELIANCE
HDFCBANK
TATAMOTORS
# DRREDDY <- commented out, will be skipped
On boot the universe builder resolves each symbol against Kite's instrument dump, assembles its nearest-expiry option chain plus near-month future, and reports anything it could not find. If the resulting universe would exceed the 3 × 3,000 instrument ceiling, it trims the furthest out-of-the-money strikes until it fits — the near-spot chain is never trimmed.
python -m tests.test_onewayExpected output — nine checks, all passing, no market connection required:
[PASS] the clean riser is entered
[PASS] the choppy name is rejected
[PASS] entries are directional
[PASS] every mover carries a score
[PASS] open rides report live P&L
[PASS] the promotion meter stays in range
[PASS] baseline samples are collected
[PASS] a sustained winner reaches the top board
[PASS] promotion never precedes its P&L floor
9/9 checks passed
python auth.py--- Kite daily login ---
1. Open this URL in your browser and log in:
https://kite.zerodha.com/connect/login?api_key=...&v=3
2. After login you land on your redirect URL. Copy the
`request_token` query parameter from that URL.
Paste request_token here: _
After logging in, your browser lands on the redirect URL you configured, and the address bar will look like this:
http://127.0.0.1/?action=login&type=login&status=success&request_token=AbCdEf123456
└──────── copy this ────────┘
Paste it back. You should see:
Login OK - welcome <your name>.
Access token saved to .../backend/.env
That token is now good until roughly 07:30 tomorrow.
python main.py --api| flag | effect |
|---|---|
--api |
start the FastAPI REST + WebSocket server in-process on port 8000 |
--now |
begin immediately instead of waiting for the 09:15 bell |
--api-port 8000 |
serve the API on a different port |
--no-warm |
skip rebuilding state from bars already recorded today |
A healthy boot log looks like this:
INFO Token valid - logged in as <your name>
INFO Universe: 163 stocks, 8,812 instruments (3 connections)
INFO Flow baselines loaded: 98 stocks (all-strikes), 98 (near-spot)
INFO Single-instance lock acquired (PID 24188)
INFO API server listening on http://0.0.0.0:8000
INFO Waiting for market open 09:15 ...
cd ../dashboard
npm install
cp .env.local.example .env.localNEXT_PUBLIC_DEMO=false
NEXT_PUBLIC_API_WS=ws://localhost:8000/ws/livenpm run build
npm run start # http://localhost:3000Open http://localhost:3000. The connection dot in the header turns green when the browser's WebSocket attaches, and the pulsing red LIVE badge appears only when the socket is connected and the market is genuinely open — it cannot be faked, which is the point.
No Kite subscription yet? Set
NEXT_PUBLIC_DEMO=trueand runnpm run starton its own. The entire interface renders against generated fixtures with no backend at all.
Editing the interface? Use
npm run dev. For live sessions always use the production build — development mode ships an unoptimised React build and visibly lags at this row throughput.
The whole stack — engine, API and terminal — comes up with one command. Nothing needs to be installed except Docker.
git clone https://github.com/<your-user>/Clavis-Terminal.git
cd Clavis-Terminal
cp backend/.env.example backend/.env
# put your KITE_API_KEY and KITE_API_SECRET in backend/.envauth.py is interactive, so run it as a one-off with a TTY attached. The token it writes lands in
your host backend/.env because that file is bind-mounted:
docker compose run --rm backend python auth.pydocker compose up -d --build| service | port | what it is |
|---|---|---|
backend |
8000 | engine + FastAPI REST + WebSocket |
dashboard |
3000 | the terminal interface |
Then open http://localhost:3000.
docker compose logs -f backend # follow the engine log
docker compose logs -f dashboard # follow the web server
docker compose restart backend # restart the engine, keep recordings
docker compose down # stop everything (the data volume survives)
docker compose down -v # stop and DELETE all recordings
# run a replay inside the container
docker compose run --rm backend python replay.py --date 2026-01-15
# run the smoke test
docker compose run --rm backend python -m tests.test_oneway| mount | reason |
|---|---|
./backend/.env → /app/.env |
auth.py writes the daily token here; it must persist on the host and never enter the image |
clavis-data volume → /app/data |
Parquet recordings, the SQLite database and session metadata survive rebuilds |
./backend/stocks.txt → /app/stocks.txt (read-only) |
change the watchlist without rebuilding the image |
One gotcha worth knowing.
NEXT_PUBLIC_API_WSis baked into the browser bundle at build time, and the socket is opened by your browser, not by the dashboard container. So it must be an address the browser can reach —ws://localhost:8000/ws/live, notws://backend:8000/.... Serving from another host? Rebuild with your own value:NEXT_PUBLIC_API_WS=ws://192.168.1.50:8000/ws/live docker compose up -d --build dashboard
Demo mode in Docker — no Kite subscription required:
NEXT_PUBLIC_DEMO=true docker compose up -d --build dashboard
gantt
title Trading day
dateFormat HH:mm
axisFormat %H:%M
section Pre-market
python auth.py :done, a, 08:45, 5m
python main.py --api :active, b, 09:05, 10m
npm run start :active, c, 09:05, 10m
section Session
Feed live, boards update :d, 09:15, 375m
Prime detection window :crit, e, 09:25, 55m
section Post-market
EOD flush + finalize :f, 15:30, 5m
Replay and review :g, 15:40, 30m
# 1. Broker access tokens expire daily, around 07:30 IST.
# Log in once and paste the request_token back into the prompt.
python auth.py
# 2. Start the screener before the bell. It waits for 09:15 by itself.
python main.py --api # add --now to start immediately
python main.py --api --api-port 8000 # explicit port
python main.py --api --no-warm # skip rebuilding state from today's bars
# 3. Serve the dashboard from the production build.
cd ../dashboard && npm run startStart before 09:10. Option contracts cannot be backfilled once the session is running, so a late start permanently loses the opening window — which is exactly the window the earliest entry paths depend on.
main.pywarns loudly on a late start, and the regime meter caps the day at MIXED rather than reporting a trend it never actually observed.
Run one instance. A single screener consumes all three WebSocket connections the broker allows per API key. A second process would silently split the universe between two terminals showing different partial data, so startup takes a single-instance lock and refuses to run twice. A stale lock from a crashed run is detected and taken over automatically.
Six checks. If all six pass, the pipeline is healthy end to end.
| # | Check | Where | What you want to see |
|---|---|---|---|
| 1 | Broker token accepted | engine log | Token valid - logged in as <name> |
| 2 | Universe assembled | engine log | Universe: N stocks, ~8,800 instruments (3 connections) |
| 3 | API answering | curl http://localhost:8000/api/status |
JSON with "instruments" non-zero |
| 4 | Ticks arriving | engine log | per-minute ranking tables printing after 09:15 |
| 5 | Browser attached | dashboard header | connection dot green, not amber |
| 6 | Genuinely live | dashboard header | pulsing red LIVE badge — shown only when the socket is connected and the market is open |
# quick end-to-end probe from any terminal
curl -s http://localhost:8000/api/status
# {"market_open":true,"instruments":8812,"signals_today":147,"feed_down":false}
curl -s http://localhost:8000/api/rankings/latest | head -c 400
curl -s http://localhost:8000/api/oneway/latest | head -c 400| Symptom | Cause and fix |
|---|---|
KITE_API_KEY / KITE_API_SECRET missing |
backend/.env is absent or empty. Copy it from .env.example and fill both values. Under Docker, confirm the bind mount by running docker compose run --rm backend cat /app/.env. |
Saved access token is expired/invalid |
Normal — tokens die daily around 07:30 IST. Run python auth.py again. This is not an error, it is the design of the broker API. |
| Login page loads but the redirect goes nowhere | Expected. Nothing needs to be listening on your redirect URL — you are only reading request_token out of the address bar. A browser error page still shows the URL. |
| Engine refuses to start, mentions a lock | Another screener already holds the three sockets. Stop it. If a previous run crashed, the stale lock is detected and taken over automatically — delete data/meta/screener.lock only if it genuinely is not. |
| Dashboard shows Backend disconnected | The engine is not running, is on a different port, or NEXT_PUBLIC_API_WS points somewhere the browser cannot reach. Confirm with curl http://localhost:8000/api/status first. |
| Dashboard looks alive but numbers never change | You are probably in demo mode. Set NEXT_PUBLIC_DEMO=false and rebuild — the value is inlined into the client bundle at build time, so a restart alone will not pick it up. |
Red feed down banner mid-session |
The watchdog saw no ticks past the stale threshold and is forcing reconnects. It re-checks the token before blaming the network. Once the feed returns, the missed window is backfilled and flagged. |
| Fewer instruments than expected | The capacity guard trimmed the universe to fit 3 × 3,000. Shorten stocks.txt. Far out-of-the-money strikes are dropped first, so the near-spot chain is intact. |
| Some symbols missing entirely | They did not resolve in Kite's instrument dump — usually a rename, or a name no longer in the F&O segment. The boot log names each one. |
| Browser tab crawls during a busy session | You are on npm run dev. Run npm run build && npm run start. |
Every session is recorded in full, which means the engine can be re-run over any recorded day with no market and no broker connection.
python replay.py --date 2026-01-15 # replay a recorded day
python replay.py --date 2026-01-15 --rank-every 5 # ranking cadence, minutes
python replay.py --date 2026-01-15 --save # persist replayed output
python replay.py --date 2026-01-15 --save-baseline # rebuild flow baselinesReplay drives exactly the same engine objects as live trading — the same market_state, the
same ranking pass, the same funnel. A threshold change is therefore evaluated against real
recorded market behaviour, not against a simulation of it.
That distinction is not academic. A synthetic feed can ramp open interest in ways real strikes physically cannot, and a rule that passes a synthetic test can turn out to be mathematically impossible on live data — a mistake that is very cheap to make and very expensive to discover during a session. The standing rule in this project is that detection logic is validated on real recordings, and synthetic data is used only to prove that plumbing is wired.
Replay also produces a forward-return report, measuring what price actually did in the window after each signal fired.
| Dimension | Figure |
|---|---|
| Instruments streamed concurrently | ~8,800 — three sockets × 3,000, with headroom |
| Underlyings tracked | ~160 NSE F&O names |
| Bars produced per session | ~3.3 million instrument-minutes |
| Spike detection latency | microseconds after an OI tick lands, intrabar |
| Ranking and funnel cadence | every minute, full universe, no sampling |
| Dashboard fan-out | fixed-rate WebSocket push to all connected clients |
| Hardware | a single desktop machine |
Single-process by design. The broker's WebSocket callbacks run on their own threads and hand ticks to a queue; one consumer thread owns every piece of mutable state. Rolling statistics are cached rather than recomputed, keeping the per-tick check constant-time — which is the specific reason instant detection across the entire chain is possible without a cluster behind it.
Contributions are genuinely welcome, and you do not need to know anything about options trading to make a good one. Most of this repository is ordinary high-quality systems work — concurrency, storage, resilience, APIs, interface engineering — and most of it can be developed without a broker subscription at all: the engine runs against synthetic input in tests, and the dashboard renders the entire interface from fixtures in demo mode.
|
Unsure which side an idea falls on? Open an issue and ask before you build. Always faster than finding out in review. |
git clone https://github.com/<your-fork>/Clavis-Terminal.git
cd Clavis-Terminal/backend && pip install -r requirements.txt && pip install ruff
python -m tests.test_oneway # 9 checks, no market, no API keyThen run what CI runs before opening a PR:
cd backend && ruff check . && python -m compileall -q . && python -m tests.test_oneway
cd ../dashboard && npx tsc --noEmit && npm run buildCI runs the same checks across Python 3.11/3.12 and Node 18/20, builds both container images, and
fails the build if a .env file or a credential-shaped string ever appears in the tree.
📖 CONTRIBUTING.md — full guide, code style, and the stricter review standard that applies to anything changing what appears on a board 🛡️ SECURITY.md — report privately, never in a public issue 🤝 CODE_OF_CONDUCT.md
The core decision logic of this system is intentionally not published here.
This repository is the architecture, the infrastructure and the analytics platform — not the strategy. To state it plainly, so nobody has to guess:
-
The production entry, exit and promotion logic is withheld. Every decision engine in
engine/ships as a reference implementation: same public interface, same output shape, fully runnable, deliberately simpler. The rules that actually decide what gets bought and when are not in this code.module what the reference version omits engine/oneway_momentum.pythe alternative entry paths that open the opening window, the structure machine behind them, structural stop placement, and the ordering and interaction of the gates engine/big_player_radar.pystrike weighting, pace normalisation and the strength curve engine/position_radar.pythe flow-acceleration breakout rule, its agreement conditions and re-arm behaviour engine/oi_multiple.pybaseline selection, near-spot weighting, the writer read and the counter-tape flag engine/rs_leaders.pyqualification gates and snapshot timing engine/sector_leaders.pysector-strength weighting and leader qualification -
The calibrated thresholds are withheld. Every parameter name is present so the design reads correctly and the code runs, but the numbers are neutral reference defaults — not the values fitted on real recorded sessions.
-
The recorded market data is withheld. No session recordings, no baselines, no research notebooks, no trade logs, no backtest output.
-
No credentials of any kind appear anywhere in this repository, in any commit, at any point in its history.
What is complete and genuinely runnable: live ingestion across the full option chain, universe construction under the broker's connection limits, bar building, the four-way OI flow decomposition, spike detection, ranking, persistence and migrations, offline replay, the REST and WebSocket API, and the entire terminal interface. Clone it, add your own API key and your own rules, and it runs.
The withheld parts were built and validated over live trading sessions. They stay private. Everything published here is offered as-is under the MIT licence.
This software is a market-analysis and research tool. It is not investment advice, it does not place orders, and nothing in this repository is a recommendation to buy or sell any security. Derivatives trading carries substantial risk of loss. Anyone running this code does so on their own account and at their own risk.
Clavis Terminal · Built for the Indian F&O market MIT licensed — see LICENSE
{ "type": "status", "data": { "market_open": true, "instruments": 8812, "feed_down": false } } { "type": "ranking", "data": [ /* per-stock flow rows */ ] } { "type": "oneway", "data": [ /* funnel rows */ ] } { "type": "signal", "data": { /* one spike */ } } { "type": "radar", "data": [ /* near-spot flags */ ] } { "type": "sector", "data": [ /* sector leaders */ ] }