A single-user algorithmic trading platform for Indian equity options.
Signals are scraped from Telegram groups (MTProto userbot), executed via the Kotak Neo API, and monitored through a React dashboard.
┌─────────────────────────────────────────────────────────┐
│ Telegram Groups (3rd-party, no bot access) │
└──────────────────────────┬──────────────────────────────┘
│ MTProto (grammers-client)
▼
┌─────────────────────────────────────────────────────────┐
│ telegram_ingester regex signal parser │
│ → TradeSignal → broadcast channel │
└──────────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ trading_engine stateful OMS (50ms tick) │
│ WaitingForEntry → Active → Target1Hit → Closed │
│ FeeCalculator (STT, SEBI, stamp, GST) │
│ → DbWriteMessage → mpsc channel │
└──────────────┬────────────────────────┬─────────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌────────────────────────────┐
│ kotak_client │ │ SQLite (WAL) │
│ REST: login + │ │ wallet / paper_trades / │
│ place_live_order │ │ system_logs / │
│ WebSocket: HSM LTP │ │ trading_config │
│ feed (mlhsm.*) │ └────────────────────────────┘
└──────────────────────┘ │
▼
┌───────────────────────┐
│ server (Axum :8080) │
│ GET /api/portfolio │
│ GET /api/settings │
│ POST /api/settings │
│ POST /api/webhook/.. │
│ GET /api/logs/stream│
└───────────┬───────────┘
│ SSE + REST
▼
┌───────────────────────┐
│ frontend (Vite/React)│
│ Settings bar │
│ P&L chart (recharts) │
│ Trade table │
│ Live log terminal │
└───────────────────────┘
| Tool | Minimum version | Install |
|---|---|---|
| Rust + Cargo | stable ≥ 1.80 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh |
| Node.js | ≥ 20 | nodejs.org |
| pnpm | ≥ 9 | npm i -g pnpm |
| SQLite | any | shipped with macOS/Linux |
git clone <repo-url> auto-trader
cd auto-trader
# type-check all 5 crates
cargo check
# run the server (SQLite auto-created at trades.db)
cargo run -p serverThe server listens on http://0.0.0.0:8080.
cd frontend
pnpm install
pnpm dev # http://localhost:5173 — proxies /api → :8080Open http://localhost:5173 in your browser.
cd frontend
pnpm build # outputs to frontend/dist/The Axum server already serves ../frontend/dist as a static fallback, so after building you can access the dashboard directly at http://localhost:8080.
This repo now supports a split deployment model:
- Backend on a VM or bare-metal host
- Frontend on Vercel
For the backend host:
cd auto-trader
cargo build --release -p server
cd kotak-bridge && npm installThen run the backend from the repo root so the SQLite database, Telegram session file, and kotak-bridge/ folder all stay in a stable relative layout:
cd auto-trader
./target/release/serverRecommended VM notes:
- Open TCP port
8080, or put Nginx/Caddy in front and expose HTTPS on443 - Keep
trades.dbandsession.jsonon persistent disk - Install Node.js on the VM because the Rust backend launches
kotak-bridge/index.js
For the Vercel frontend:
- Deploy the
frontend/directory - On first load, enter your backend URL or IP in the
Server URL or IP:PORTfield in the Kotak login panel - The frontend stores that server address in browser storage and a cookie, so you do not need to re-enter it every time
Examples:
http://34.93.xx.xx:8080https://api.example.com
The backend reads environment variables from the process environment. It also
auto-loads a .env file from the working directory at startup (via
dotenvy) if one is present — variables already exported in the shell or a
systemd EnvironmentFile= still take priority. So any of the following work:
- export the variables in your shell before starting the server,
- put them in a
.envfile next totrades.db(never commit this — it's gitignored), or - define them in a
systemdunit withEnvironment=orEnvironmentFile=
All backend date-sensitive logic is normalized to Indian Standard Time (IST, UTC+05:30).
That includes expiry-date interpretation, "today" checks, and persisted trade/log timestamps,
so behavior stays aligned with Indian markets even if the server is running in another timezone.
See .env.example for a ready-to-copy template (and
frontend/.env.example for the optional frontend
one). These are the supported variables:
# ── Auth (required) ───────────────────────────────────────
# Gates the whole app — every /api/* route needs a bearer token obtained by
# exchanging PASSKEY at POST /api/auth/verify-passkey, signed with AUTH_SECRET.
PASSKEY=change-me
AUTH_SECRET=change-me-too
# ── SQLite ───────────────────────────────────────────────
DATABASE_URL=sqlite://trades.db # default
# Trading mode (PAPER/LIVE), max trade size, brokerage, and target/SL
# settings all live in the `trading_config` SQLite table, not env vars —
# edit them via the Settings bar in the UI or POST /api/settings.
# ── Telegram MTProto ingester (optional) ─────────────────
# Get these from https://my.telegram.org → API Development Tools
TELEGRAM_API_ID=12345678
TELEGRAM_API_HASH=abcdef1234567890abcdef1234567890
# Comma-separated chat IDs to listen to (use a negative number for groups)
TELEGRAM_CHAT_IDS=-1001234567890,-1009876543210
# ── Kotak Neo WebSocket ───────────────────────────────────
# Scrips to subscribe (pSymbol from scrip master, & separated)
KOTAK_SCRIPS=nse_cm|11536&nse_cm|1594
# ── Kotak Neo auto-login (optional) ───────────────────────
# Set all five to skip the manual Kotak login form. The server will:
# - log in at startup if no valid session is restored from the DB,
# - log in at 09:05 IST each trading day (pre-warm, so the Scrip Master is
# loaded before the 09:15 open), and retry at 09:15 if that failed.
# The 6-digit TOTP is generated from KOTAK_TOTP_SECRET (RFC 6238), so no
# human needs to read a code off an authenticator app each morning.
KOTAK_ACCESS_TOKEN=eyJhbGci... # API Dashboard access token
KOTAK_MOBILE_NUMBER=+91XXXXXXXXXX # registered mobile, with country code
KOTAK_UCC=Y4HAU # Unique Client Code
KOTAK_MPIN=123456 # 6-digit trading MPIN
KOTAK_TOTP_SECRET=JBSWY3DPEHPK3PXP # Base32 secret from TOTP registration (alias: KOTAK_TOTP_HASH)
# KOTAK_AUTO_LOGIN=false # set to disable unattended auto-login even if the above are setAny of access_token / mobile_number / ucc / mpin / totp left blank
in a manual POST /api/auth/kotak request also falls back to these same env
vars, and a blank totp is generated from KOTAK_TOTP_SECRET — so the
Kotak login form still works with only some fields filled in. A dedicated
POST /api/auth/kotak/auto-login (no body) logs in using only the env vars.
Example shell startup:
export PASSKEY=change-me
export AUTH_SECRET=change-me-too
export DATABASE_URL=sqlite:///home/ubuntu/auto-trader/trades.db
export TELEGRAM_API_ID=12345678
export TELEGRAM_API_HASH=abcdef1234567890abcdef1234567890
export TELEGRAM_CHAT_IDS=-1001234567890,-1009876543210
cd ~/auto-trader
./target/release/serverThis section assumes:
- backend on a small Ubuntu/Debian VM (any cloud — GCP, Oracle Cloud, AWS, DigitalOcean, ...)
- frontend on Vercel
- repo uploaded with the same folder layout
Create a small VM (a free/burstable tier such as GCP e2-micro or Oracle Cloud's Always Free shapes is enough) and allow, in your cloud provider's console-level firewall (security list / network security group — this is separate from and in addition to the VM's own OS firewall):
- SSH from your admin IP
- TCP
8080if you want to expose the Rust server directly - TCP
80and443if you will use Nginx/Caddy as a reverse proxy
Gotcha — check the VM's own OS firewall too, and check rule order. Several stock cloud images (Oracle Cloud's Ubuntu images in particular) ship with
iptablespre-configured to accept SSH and reject everything else by default. If you (or a setup script) append newACCEPTrules for8080/443after that catch-allREJECTrule, they are silently dead —iptablesmatches top to bottom, so the reject fires first and the port stays closed to the outside world even thoughiptables -Lshows an "allow" rule for it further down. This is easy to miss because the backend still answers fine on127.0.0.1from inside the VM (loopback traffic skips the rule), so the API looks healthy over SSH while every external caller — including your Vercel frontend — silently times out.Check with
sudo iptables -L INPUT -n --line-numbersand make sure yourACCEPTrules for the ports you need come before any blanketREJECT/DROPrule. If you need to reorder, insert the correct rules withsudo iptables -I INPUT <line-number-before-the-reject> ..., remove the old dead ones, then runsudo netfilter-persistent save(ifiptables-persistentis installed) so the fix survives a reboot. After any change, verify from outside the VM —curl http://<public-ip>:8080/api/healthfrom your own machine, not just from inside an SSH session.
Recommended baseline packages:
sudo apt update
sudo apt install -y curl git nginx
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
node -v
npm -vRust itself is only needed on the VM if you plan to build there; the recommended flow (below) downloads a prebuilt release binary instead, so cargo/rustup are optional.
cd ~
git clone <repo-url> auto-trader
cd auto-trader/kotak-bridge
npm install
cd ..The backend binary itself does not need to be built on the VM — see step 4.
Important runtime note — PATH CONTRACT:
- the server binary must run with
auto-trader/backend/as its working directory, not the repo root — it resolvestrades.db,session.json,../frontend/dist, and../kotak-bridgerelative to the working directory it was started from. Starting it from anywhere else breaks those lookups. In production the binary itself also lives directly insidebackend/(see step 4), so in practice this means:cd ~/auto-trader/backend && ./the-binary. frontend/distdoesn't need to contain a real build in production, since the frontend is served by Vercel, not this server — the static-fallback route just won't have anything to serve if you hit the VM's IP directly, which is expected.
If you are deploying from GitHub, point Vercel at the frontend/ directory.
Vercel settings:
- Framework preset:
Vite - Root directory:
frontend - Build command:
pnpm build - Output directory:
dist
No frontend environment variable is required for the backend URL because the UI now asks for it and stores it in browser storage and a cookie.
Download the latest release binary built by CI (.github/workflows/release-server.yml) rather than building on the VM — this is a small VM that also runs the live 50ms trading-tick loop once it's live, and a cargo build --release here would compete with it for CPU/memory:
cd ~/auto-trader/backend
LATEST_JSON=$(curl -s https://api.github.com/repos/MrImmortal09/auto-trader/releases/latest)
DOWNLOAD_URL=$(echo "$LATEST_JSON" | grep -o '"browser_download_url": *"[^"]*"' | grep 'x86_64-unknown-linux-gnu' | head -n1 | cut -d '"' -f4)
curl -fsSL "$DOWNLOAD_URL" -o "$(basename "$DOWNLOAD_URL")"
chmod +x server-*-x86_64-unknown-linux-gnuPut your env vars in ~/auto-trader/backend/.env (auto-loaded from the working directory at startup — see Environment Variables):
cat > ~/auto-trader/backend/.env <<'EOF'
PASSKEY=change-me
AUTH_SECRET=change-me-too
TELEGRAM_API_ID=12345678
TELEGRAM_API_HASH=abcdef1234567890abcdef1234567890
TELEGRAM_CHAT_IDS=-1001234567890,-1009876543210
KOTAK_SCRIPS=nse_cm|11536&nse_cm|1594
EOFPATH CONTRACT, again: start it from inside backend/, not the repo root — trades.db, .env, session.json, and the ../frontend/dist / ../kotak-bridge lookups are all relative to the working directory:
cd ~/auto-trader/backend
./server-*-x86_64-unknown-linux-gnuExpected behavior:
- server binds to
0.0.0.0:8080 - Kotak bridge starts only after valid Kotak login tokens exist
- Telegram auth state is stored in
session.json
This project runs the backend in the foreground of a dedicated tmux pane rather than under systemd, so a live trading process is never silently backgrounded or auto-restarted mid-position without a human noticing:
tmux new-session -d -s 0
tmux send-keys -t 0:0 "cd ~/auto-trader/backend && ./server-*-x86_64-unknown-linux-gnu" C-m
tmux attach -t 0Session 0, window 0 (i.e. pane 0:0) is what update.sh restarts by default — using the exact names above means it works with no edits. If you'd rather use a different session/window name, update TMUX_PANE at the top of backend/server/update.sh to match.
backend/server/update.sh (already in the repo) handles subsequent updates: it's what POST /api/update_server spawns, or you can run it by hand. It syncs origin/main, downloads the newest release binary, backs up the binary that's currently running, swaps it in, restarts the tmux pane, and health-checks /api/health — rolling back to the backup automatically if the new binary doesn't come up healthy. All output goes to /tmp/update.log on the VM.
If you'd rather run under systemd with auto-restart-on-crash instead of tmux, that's a reasonable alternative for a less hands-on setup — just be aware update.sh as written assumes tmux, so you'd need to adapt its stop/start steps to systemctl restart first.
Directly exposing :8080 works, but a reverse proxy is cleaner and lets you add TLS.
Example Nginx site:
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection '';
proxy_buffering off;
}
}Then enable it:
sudo tee /etc/nginx/sites-available/auto-trader >/dev/null <<'EOF'
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection '';
proxy_buffering off;
}
}
EOF
sudo ln -s /etc/nginx/sites-available/auto-trader /etc/nginx/sites-enabled/auto-trader
sudo nginx -t
sudo systemctl reload nginxIf you have a domain, add HTTPS with Certbot afterward.
After the Vercel site is live:
- Open the frontend in the browser.
- In the Kotak login panel, enter your backend address in
Server URL or IP:PORT. - Use either
http://PUBLIC_IP:8080or your reverse-proxied domain such ashttps://api.example.com. - Continue using the UI normally; all API calls, SSE logs, and downloads will use that stored backend URL.
Examples:
http://34.93.xx.xx:8080https://api.example.com
For Telegram:
- Open the frontend.
- Enter Telegram API ID, API hash, and phone number.
- Request the login code.
- Submit the OTP and 2FA password if prompted.
- Select the chats to monitor.
For Kotak:
- Enter the backend server URL once.
- Enter Kotak access token, mobile number, UCC, TOTP, and MPIN.
- Click
Connect. - On success, the backend fetches the scrip master and starts the Node bridge.
Before considering the deployment ready, verify:
curl http://127.0.0.1:8080/api/settingsreturns JSON from inside the VM (over SSH)curl http://<public-ip>:8080/api/healthreturns JSON from your own machine, outside the VM — this is the check that would have caught the iptables-ordering gotcha from step 1; a pass over SSH alone does not prove the outside world can reach ittmux attach -t trader(or your session name) shows the server running and logging normally- the Vercel frontend can load settings and portfolio data
- live logs connect through
/api/logs/stream - Telegram chat selection works
- Kotak login succeeds and scrip master download works
These files should remain on persistent disk on the VM, inside backend/:
trades.db(+-shm/-walcompanion files)session.json.env- the current release binary,
backend/server-<version>-x86_64-unknown-linux-gnu kotak-bridge/node_modules/
update.sh already keeps one previous binary as backend/.server_prev for rollback and prunes older ones — you don't need to manage binary versions by hand. If you ever move to a fresh VM, copy over trades.db*, session.json, and .env from backend/ before starting the server there.
Kotak session tokens expire daily. The server persists its session (auth
token, sid, base URL) in the kotak_session SQLite table and restores it
automatically at startup if it's still from today — otherwise it needs a
fresh login, which happens one of two ways:
- Manual — log in from the frontend's Kotak login panel (mobile, UCC,
TOTP, MPIN). This is the default if no
KOTAK_*auto-login env vars are set. - Automatic — set
KOTAK_ACCESS_TOKEN/KOTAK_MOBILE_NUMBER/KOTAK_UCC/KOTAK_MPIN/KOTAK_TOTP_SECRET(see Environment Variables) and the server logs in on its own — at startup, at 09:05 IST (pre-warm, so the Scrip Master is loaded before the bell), and again at 09:15 IST if still disconnected. No human action required, including in LIVE mode.
Either way, every step of the login is streamed to the dashboard's log
terminal (KOTAK_LOGIN_START → KOTAK_LOGIN_OK → KOTAK_WS_START →
KOTAK_CONNECTED → SCRIP_FETCH → SCRIP_FETCH_SUCCESS), so you can watch
an unattended login happen. Failures log KOTAK_LOGIN_FAILED in red. Secrets
are never logged — only a masked UCC and whether the TOTP was auto-generated.
Until a session exists, the WebSocket market-data feed will silently fail to connect (the position monitor still works in paper mode using entry_price as the assumed LTP).
On the first run with TELEGRAM_API_ID set, the ingester will prompt you interactively:
Telegram phone number (e.g. +91XXXXXXXXXX): +91XXXXXXXXXX
Login code (sent to your Telegram app): 12345
A session file session.db is created alongside the binary and reused automatically on subsequent runs.
If two-factor authentication is enabled you will also be asked for your 2FA password.
The current implementation stores the Telegram session in session.json in the working directory.
The Telegram parser recognises messages like:
BUY BHEL 425 CE ABOVE 8.25
TARGET :- 9.50 / 11.50
SL :- 5
JULY EXPIRY
| Field | Example | Notes |
|---|---|---|
| Action | BUY / SELL |
Required |
| Instrument | BHEL |
Underlying name |
| Strike | 425 |
Options only |
| Option type | CE / PE |
Options only |
| Entry condition | ABOVE / BELOW |
LTP trigger |
| Entry price | 8.25 |
Trigger price |
| Targets | 9.50 / 11.50 |
/-separated, ordered |
| Stop loss | SL :- 5 |
Initial SL |
| Expiry | JULY EXPIRY |
Optional |
Equity signals (no strike/type) are also supported:
BUY RELIANCE ABOVE 2500
TGT 2600 / 2700
SL 2420
When replying directly to an original trade signal message in Telegram, the ingester recognises:
- SL Update: e.g.,
SL to 4.50,Move SL to 4,sl -> 2.5— updates the active stop-loss for the position. - Exit Command: e.g.,
exit at 610,exit @610,exit @ 610,exit 610,exit all at 610— immediately forces the position to exit all remaining quantities at the specified price.
WaitingForEntry ──LTP crosses entry──▶ Active
│
┌── SL hit ────────────┤
▼ ├── Target 1 (full exit) ──▶ Closed
Closed │
▲ └── Target 1 (partial) ──▶ Target1Hit
│ │
└──── SL (trailed) or Target 2 hit ───────────────────┘
On Target1Hit:
- Sells
target_1_exit_pct %of the position - Trails the SL to
(avg_buy_price + target_2) / 2
Whenever a new trade signal arrives for an instrument (e.g., SENSEX 80000 PE), any existing open position for the same underlying instrument in the opposite direction (e.g., an active SENSEX CE position or opposite side equity trade) is automatically exited with the reason Opposite Signal Exit.
Every completed trade records its explicit exit reason (exit_reason column in SQLite) which is displayed in the UI (Trade History & Daily Reports modal):
SL Hit: Initial Stop-Loss triggeredTrailed SL Hit: Trailing Stop-Loss triggered after Target 1Target 1 Hit/Target 2 Hit: Target profit levels reachedClosed via Frontend: Manually closed using the dashboard Close buttonExit via Telegram Msg: Exited via a direct Telegram reply command (e.g.,exit @ 610)Opposite Signal Exit: Automatically closed due to an opposite trade signal arriving
| Section | URL | Description |
|---|---|---|
| Settings bar | top | Edit brokerage, max trade, targets, PAPER/LIVE toggle. Changes persist to SQLite immediately. |
| P&L chart | middle | Recharts line chart of cumulative realised P&L. |
| Trade table | middle | Full history with gross, charges breakdown, and per-trade P&L. |
| Log terminal | bottom | Live SSE stream of engine events (entry, SL hit, target hit, config changes). |
| Daily Reports | /reports |
Dedicated daily summary grouping trades by calendar date and Telegram signal ID. |
The Daily Reports page provides an aggregated view of trading performance by calendar day (in IST):
- Date-Level Summary: Shows the net realised P&L for each day.
- Signal Grouping: Trades are grouped by their originating Telegram
signal_id, making it easy to track net P&L per strategy or call. Trades without an ID appear under "Legacy Trades". - Trade Details Modal (Info
ibutton): Clicking theibutton next to any signal opens a modal showing:- Original Message: The raw Telegram message text that triggered the trade signal.
- Executions: Itemized BUY/SELL executions with timestamp, executed price, quantity, and net P&L after brokerage and taxes.
| Method | Path | Description |
|---|---|---|
GET |
/api/portfolio |
Wallet balance + last 100 trades |
GET |
/api/settings |
Current TradingConfig as JSON |
POST |
/api/settings |
Update TradingConfig (persisted to DB + in-memory) |
POST |
/api/webhook/telegram |
Inject a TradeSignal JSON manually |
GET |
/api/logs/stream |
SSE stream of engine log events |
auto-trader/
├── Cargo.toml workspace root
├── backend/
│ ├── shared_domain/ domain types (TradeSignal, TradingConfig, etc.)
│ ├── kotak_client/ Kotak Neo REST + WebSocket client
│ ├── telegram_ingester/ MTProto userbot + regex signal parser
│ ├── trading_engine/ stateful OMS + fee calculator
│ └── server/ Axum HTTP server + SQLite writer
├── frontend/ Vite + React 19 + Tailwind v4 dashboard
└── kotak-api-docs/ local copy of official Kotak API docs
All paper trades compute Kotak Neo charges automatically:
| Charge | Rule |
|---|---|
| Brokerage | Flat ₹20 per leg (configurable) |
| SEBI fee | 0.0001% of turnover |
| Exchange fee (NSE) | 0.00297% of turnover |
| STT (options) | 0.05% on SELL side only |
| STT (equity intraday) | 0.025% on SELL side only |
| Stamp duty | 0.003% on BUY side only |
| GST | 18% × (brokerage + SEBI + exchange fee) |