A stock trade-recommendation tracker. A SEBI-registered research analyst publishes a trade call — instrument, entry, stop loss and up to three targets — and the system tracks it against live market prices until it hits a target, hits the stop loss, or expires.
The tracking is the product. Everything else is scaffolding around it.
- An analyst creates a recommendation — picks an instrument (equity, F&O or commodity), sets entry (a fixed rate or a range), a stop loss, and up to three targets, plus a validity window.
- The system subscribes to that instrument's live price and watches it.
- It resolves the outcome automatically — target hit, stop loss hit, or expired without triggering — and records P&L at the moment of close.
A recommendation moves through: open → closed, with a result of
tp (target), sl (stop loss), timeout (expired), or manual (closed by the
analyst).
Three services, deliberately decoupled. Prices flow one way, through Redis.
AngelOne SmartAPI (WebSocket2)
│
▼
MarketPriceFetcher ──writes──▶ Redis price cache
│
reads│
▼
OpenTradeRegistry ◀──────▶ GlobalTradeTracker ──▶ MongoDB
(open trades) (SL / TP / expiry)
Holds a single AngelOne WebSocket connection and writes every tick into Redis. It never serves a request directly; it only fills the cache.
Every 10s it re-scans what needs watching and re-subscribes:
- tokens of all currently open trades (read from the Redis registry, not the DB)
- tokens with a pending on-demand CMP request
A REST fallback polls every 5s for tokens the socket hasn't delivered.
This is the only component that talks to the broker API. Centralising it means the tracker can scale to thousands of trades without multiplying API calls.
A Redis-backed set of open trades, bootstrapped from MongoDB once at startup
([OpenTradeRegistry] Bootstrapped: N trades). Adding or closing a trade updates
Redis immediately, so neither the fetcher nor the tracker ever scans the database
in its hot loop.
The loop that decides outcomes. Runs every 1000 ms, processing trades in parallel chunks of 20. It reads prices from Redis and never calls the broker API itself.
Each cycle, for every open trade:
- Entry trigger — a trade posted with an entry away from the market stays untriggered until price reaches the entry level or range.
- Target hit — checks targets in order; a hit records which target and closes
with
result: "tp". - Stop loss hit — closes with
result: "sl". If a target had already been hit, the trade closes at the last hit target instead of the stop. - Direction awareness — buy and sell calls invert every comparison.
Persistence is batched: live LTP and P&L are flushed to MongoDB every 60s for crash recovery, while state changes (trigger, target, stop, close) are written immediately. Once a day an expiry sweep closes trades that passed their validity without ever triggering.
Validity presets (Intraday, BTST, 1 Week … 1 Year, or a custom date) resolve to a concrete expiry date. Market hours are IST:
| Segment | Hours |
|---|---|
| Equity (NSE/BSE) | 09:15 – 15:30 |
| Commodity (MCX) | 09:00 – 23:30 |
An end-of-session sweep runs at 15:16 (equity) and 23:16 (MCX) on weekdays to finalise anything the live tracker missed.
front/ Next.js 15 · App Router · NextAuth · Tailwind · Redux Toolkit
server/ Express · TypeScript · MongoDB (Mongoose) · Redis · BullMQ · Socket.IO
| Route | Purpose |
|---|---|
/ |
redirects to sign-in |
/auth/provider/signin |
phone + OTP sign-in and analyst registration |
/auth/signup/serviceprovider/subprofile/[id] |
team-member invite signup |
/dashboard/serviceprovider/recommendations/create |
the create form |
/dashboard/serviceprovider/recommendations/myrecommendations |
open and closed calls |
/dashboard/serviceprovider/recommendations/performance |
hit rate and P&L |
| Mount | Purpose |
|---|---|
/api/auth |
OTP request/verify, sign-in, analyst signup |
/api/scorecard |
create, modify, close and read recommendations; live CMP |
/api/scripts |
instrument master — symbol, expiry, strike, token lookup |
/api/services |
subscription plans (used by the share-with selector) |
/api/data |
notifications and analyst profile reads |
/api/market-data |
market data helpers |
/health |
liveness probe |
Real-time updates reach the browser over Socket.IO; scorecard state changes are broadcast via Redis pub/sub so multiple server instances stay in sync.
Prerequisites: Node 20+, MongoDB, Redis, and AngelOne SmartAPI credentials.
# Redis (Docker is easiest)
docker run -d --name stocktracker-redis --restart unless-stopped -p 6379:6379 redis:7-alpine
# Backend → http://localhost:8080
cd server && npm install && npm run dev
# Frontend → http://localhost:3000
cd front && npm install && npm run devMONGOOSE_URL=mongodb://localhost:27017/stocktracker
PORT=8080
JWTSECRET=<random 32-byte base64>
NEXTAUTH_URL=http://localhost:3000
NEXT_PUBLIC_BACKEND_URL=http://localhost:8080
# AngelOne SmartAPI — required for live prices and tracking
TOTP=<totp secret>
ANGEL_API_KEY=<key>
ANGEL_ID=<client id>
ANGEL_PASS=<pin>
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_USERNAME=
REDIS_PASSWORD=
REDIS_USE_TLS=falseNEXT_PUBLIC_BACKEND_URL=http://localhost:8080
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=<random 32-byte base64>
NEXT_PUBLIC_APEX_DOMAIN=stocktracker.app # optional; drives subdomain + cookie scopeOn a healthy boot the backend logs:
Server up and running on port :- 8080
ScriptMaster sync: N entries already present
[OpenTradeRegistry] Bootstrapped: N trades
MarketPriceFetcher: Starting with WebSocket2 streaming (subscription refresh every 10s)
GlobalTracker: Starting with 1000ms interval, chunk size 20
If those last three lines are missing, tracking is not running.
- Frontend → Vercel.
NEXT_PUBLIC_BACKEND_URLis left empty so browser calls stay relative and route through the/apiproxy rewrite innext.config.js, avoiding mixed-content blocks. Server-side code needs an absolute origin instead, viaBACKEND_INTERNAL_URL. - Backend → Docker on EC2, deployed by
.github/workflows/deploy-server.ymlon push tomain..envlives on the box and is not in the repo.
docker run --env-fileis stricter than a shell: no spaces around=, no quotes, noexport. A line likeKEY = valuefails the whole container start.
- OTP delivery goes through ProactiveSMS. The message body is bound to a registered DLT template — rewording it (including the sender name) without registering a new template will get every OTP silently rejected by the operator.
- Instrument master syncs daily at 06:00 IST from AngelOne (~123k instruments) and backs the symbol / expiry / strike pickers.
- Sub-profiles let an analyst add team members with per-module permissions; a read-only member can view recommendations but not publish them.