Skip to content

Repository files navigation

Indian Markets Trading Bot — Zerodha Kite Connect

CI Python 3.11+ License: MIT

An end-to-end, self-hosted algorithmic trading system for Indian markets. It runs unattended on a free-tier cloud VM, places orders through Zerodha Kite Connect, enforces hard risk limits that no strategy can override, and uses an LLM to journal every trade and propose its own parameter changes.

The design goal was one manual step per day — the 60-second Kite login that Zerodha does not permit automating. Everything else is scheduled.

Paper trading is the default. PAPER_TRADING=true simulates fills locally with full Indian charges. Live trading requires explicitly flipping it to false. Read the rollout plan before you do.


Table of contents


What it actually does

Trading

  • Three implemented strategies (swing momentum, intraday opening-range breakout, stock futures momentum) plus stubs for options spreads and MCX trend.
  • A paper adapter that simulates fills against live prices with slippage and the complete Indian retail charge stack — STT, GST, SEBI turnover fee, stamp duty, exchange transaction charges, and brokerage. Simulated P&L is comparable to a real contract note.
  • An order manager with an explicit state machine, plus a reconciler that syncs local state against the broker every 30 seconds.

Risk

  • Global limits hardcoded in Python (not YAML) so a config edit cannot loosen them: daily loss kill-switch, weekly loss cap, per-sleeve monthly drawdown, position count, and margin ceiling.
  • A rolling 20-trade circuit breaker that automatically disables any strategy bleeding beyond 3% of its allocated capital.
  • An "excluded holdings" filter so the bot never trades a symbol you already hold as a long-term investment in the same account.

Automation

  • Nine systemd units covering pre-market, the intraday session, end-of-day, an MCX evening session, weekly reporting, monthly optimization, and daily cleanup.
  • A watchdog daemon that observes process health and pages you on Telegram.
  • A salvage path: if you authenticate late, a systemd path unit notices the token file change and starts whatever part of the session can still be recovered.
  • Log rotation and artifact pruning so a 45 GB disk never fills.

Analysis

  • FIFO round-trip P&L attribution that correctly credits the opening strategy even when a position is closed by the auto-square-off job.
  • Mark-to-market on open positions, an equity curve with drawdown, and a live-vs-backtest drift monitor.
  • Backtesting engines for both intraday and daily strategies, sharing the same charge model as live trading, plus a walk-forward optimizer.
  • An LLM that writes a post-mortem for every closed trade and, weekly, proposes one narrow config change with supporting evidence.

Architecture

                          ┌──────────────────────┐
                          │  Telegram Notifier   │◄──── alerts from everywhere
                          └──────────▲───────────┘
                                     │
┌────────────────────────────────────┴──────────────────────────────────────┐
│                             Orchestrators                                 │
│   pre_market · intraday_loop · eod · mcx_evening · weekly_report          │
│                    (each a systemd timer + service)                       │
└───┬─────────────────┬──────────────────┬───────────────────┬──────────────┘
    │                 │                  │                   │
┌───▼──────────┐ ┌────▼─────────┐ ┌──────▼───────┐  ┌────────▼─────────┐
│  Strategies  │ │  Risk Engine │ │     OMS      │  │    Analytics     │
│              │ │              │ │              │  │                  │
│ swing_mom    │►│ hardcoded    │►│ OrderManager │  │ FIFO P&L         │
│ intraday_orb │ │ limits       │ │ Reconciler   │  │ drift monitor    │
│ stock_fut    │ │ circuit brkr │ │ state machine│  │ equity curve     │
│ (+ stubs)    │ │ sleeve state │ │              │  │ LLM journals     │
└──────────────┘ └──────────────┘ └──────┬───────┘  └────────┬─────────┘
                                         │                   │
                            ┌────────────▼────────┐  ┌───────▼────────┐
                            │      Adapters       │  │  DuckDB store  │
                            │  KiteAdapter (live) │  │ orders, fills, │
                            │  PaperAdapter (sim) │  │ positions,     │
                            └──────────┬──────────┘  │ daily_pnl, ... │
                                       │             └────────────────┘
                                       ▼
                        Zerodha Kite Connect (REST + WebSocket)
                                       ▲
                                       │
                          access_token.txt ◄─── auth.py (daily login)

Detailed module-by-module notes live in docs/ARCHITECTURE.md.


Quick start (local, paper mode)

You can run the whole system on a laptop in paper mode. Nothing here touches real money.

Prerequisites

  • Python 3.11+
  • A Zerodha account with Kite Connect API access. The API is a paid add-on (about ₹2000/month) and is separate from your trading account. Sign up at developers.kite.trade.
  • An OpenAI API key for the journaling and reporting jobs.
  • A Telegram bot for alerts.

1. Clone and install

git clone https://github.com/dhruv7539/zerodha-trading-bot.git
cd zerodha-trading-bot

python3.11 -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install --upgrade pip
pip install -r requirements.txt

2. Create your Kite Connect app

At developers.kite.trade/apps, create an app and set the Redirect URL to exactly:

http://127.0.0.1:5000/callback

This has to match character-for-character or the login callback will fail.

3. Create your Telegram bot

  1. Message @BotFather on Telegram, send /newbot, and copy the token it gives you.
  2. Send any message to your new bot so a chat exists.
  3. After step 4 below, run python scripts/get_telegram_chat_id.py to print your chat ID.

4. Configure

cp .env.example .env

Open .env and fill in every required value. The file documents each one inline. At minimum you need KITE_API_KEY, KITE_API_SECRET, KITE_USER_ID, OPENAI_API_KEY, TELEGRAM_BOT_TOKEN, and TELEGRAM_CHAT_ID. Leave PAPER_TRADING=true.

Then grab your chat ID:

python scripts/get_telegram_chat_id.py
# paste the result into TELEGRAM_CHAT_ID in .env

5. Authenticate

Kite access tokens expire every morning around 06:00 IST, so this is the daily ritual:

python auth.py

This opens the Kite login page in your browser. Log in, and the callback writes access_token.txt (mode 0600, gitignored). The script exits once the token is saved.

6. Protect your existing holdings

If you already hold stocks in this Zerodha account, tell the bot to never trade them. Otherwise an intraday sleeve shorting RELIANCE would actually sell your long-term position.

python scripts/build_safe_universe.py --dry-run   # preview
python scripts/build_safe_universe.py             # write to config/universe.yaml

This reads your live CNC holdings and writes them into excluded_holdings in config/universe.yaml. Re-run it whenever your holdings change. Do not skip this step if you share the account with your investments.

7. Verify everything is wired

python scripts/preflight.py

This checks env vars, token freshness, a live Kite profile call, the OpenAI key format, and sends a Telegram heartbeat. You should get a Telegram message. If you do, the whole chain works.

Run the offline test suite too:

pytest -m "not live"

8. Run something

python -m orchestrator.pre_market      # instrument refresh, universe build, strategy pre-market hooks
python -m orchestrator.intraday_loop   # the main session loop (09:15-15:30 IST)
python -m orchestrator.eod             # reconcile, square off, compute P&L, write LLM journals

In another terminal, watch it live:

python scripts/dashboard.py

Deploy to a server

The bot is built to run unattended on a Linux VM. Oracle Cloud's Always Free tier is a good fit — an Ampere ARM instance with 1 OCPU and 1 GB RAM handles this comfortably and costs nothing.

Full walkthrough, from creating the cloud account to verifying the timers: docs/DEPLOYMENT.md.

The short version, once you have an Ubuntu 22.04 VM and can SSH to it:

export VM_IP=203.0.113.10
export SSH_KEY=~/.ssh/trading_bot
bash deploy/deploy_to_vm.sh

That rsyncs the project to /opt/trading-bot, copies .env over separately with mode 600, and runs deploy/install.sh on the VM. The installer sets the system timezone to Asia/Kolkata, installs Python 3.11, builds the venv, creates runtime directories, installs all systemd units, configures logrotate and sudoers, and enables the timers.

Verify:

ssh -i ~/.ssh/trading_bot ubuntu@$VM_IP 'systemctl list-timers | grep -E "intraday|pre-market|eod"'

Scheduled units

Unit Schedule (IST) What it does
morning-alert Mon–Fri 08:55 Telegram nudge to log in; polls 30 min for the token
pre-market Mon–Fri 09:00 Instrument refresh, universe build, strategy pre-market hooks
intraday-loop Mon–Fri 09:15 Main session: WebSocket ticks, bars, signals, reconciliation
eod Mon–Fri 15:45 Reconcile, square off, compute daily P&L, write LLM journals
mcx-evening Mon–Fri 17:00 MCX commodity session until 23:00
weekly-report Sun 18:00 LLM weekly digest, equity curve PNG, drift report, config suggestion
cleanup-artifacts Daily 20:00 Prune old instrument parquets and journal directories
walk-forward 1st of month 02:00 Re-optimize strategy parameters on rolling windows
watchdog always on Health checks, stale-token and missing-process alerts
late-start.path on token change Salvages the session if you authenticate late

macOS users can schedule locally instead with python scripts/launchd/install_launchd.py install.


Daily operation

The only thing you must do each trading day is authenticate.

On a local machine:

python auth.py

On a remote VM — the login callback is served on the VM's localhost, so tunnel port 5000 to it:

ssh -L 5000:localhost:5000 -i ~/.ssh/trading_bot ubuntu@$VM_IP
# then, inside that SSH session:
bash /opt/trading-bot/deploy/auth_remote.sh

Open the printed URL in your local browser, log in, and the token lands on the VM. If you set VM_HOST in .env, the 08:55 Telegram reminder includes this exact command so you can copy-paste it from your phone.

If you miss the window: authenticate anyway. The late-start.path unit watches access_token.txt and, when it changes, starts the intraday loop if there's still session left (between 09:31 and 14:30) and runs the Monday swing rebalance if applicable. A late start is much better than a skipped day.

Why isn't login automated?

Zerodha requires an interactive login with 2FA for every session token. Scripting it with Selenium violates their terms of service and risks having your API access revoked. This project deliberately does not ship that. Sixty seconds a day is the cost of staying compliant.


Monitoring

Live dashboard

A terminal UI that reads an atomic JSON snapshot written by the running orchestrator. It's read-only and never touches the DuckDB write lock.

python scripts/dashboard.py                 # refreshes every 2s
python scripts/dashboard.py --once          # render once and exit
python scripts/dashboard.py --interval 5    # slower refresh

Against a remote VM:

export VM_HOST=203.0.113.10
bash deploy/watch.sh

It shows today's realized P&L, open positions with live mark-to-market, recent signals and fills, per-sleeve enable/disable state, token age, and writer health.

Telegram alerts

You get paged for: stale token at market open, circuit-breaker trips, risk-limit kill switches, crashed orchestrators, and the EOD and weekly summaries. Alerts are throttled and deduplicated — a stale token suppresses the redundant "process missing" alerts it would otherwise cause.

Sleeve control

python scripts/sleeve.py status
python scripts/sleeve.py disable intraday_orb "underperforming, see W21 drift"
python scripts/sleeve.py enable intraday_orb

State persists to data/store/sleeve_state.json. It's a JSON file rather than a DuckDB table specifically so you can flip a sleeve while the bot is running and holding the database write lock.

Logs

logs/pre_market.log        logs/intraday_loop.log      logs/eod.log
logs/mcx_evening.log       logs/watchdog.log           logs/weekly_report.log
run/*.pid                  # live PIDs, read by the watchdog

Rotated daily by logrotate with 14 days of compressed retention.


Strategies

Sleeve Type Status Summary
swing_momentum Multi-day CNC Implemented, validated Top-3 concentrated portfolio from a Nifty-100 universe, ranked by 60-day return, filtered on positive 20-day return and price above the 50-day SMA. 15% hard stop, Monday rebalance with a rank-6 hold band.
intraday_orb Intraday MIS Implemented, on probation Opening-range breakout on the 09:15–09:30 range across the 8 most liquid F&O names. Volume-confirmed entries, target at 1.5× the range, optional regime filter gating on Nifty direction and an ATR volatility floor.
stock_futures_momentum Short-hold Implemented, unvalidated 5-day momentum on liquid F&O underlyings. Two concurrent positions, 5% hard stop, 3-day minimum and 10-day maximum hold, Tuesday rebalance. Currently trades cash equivalent.
options_spreads Defined-risk F&O Stub Intended for weekly NIFTY/BANKNIFTY defined-risk spreads.
mcx_trend Commodity Stub Intended for CRUDEOIL and NATURALGAS evening trend-following.
smallcase_drift, gold_etf, intl_etf, reit Monitors Alert-only Never place orders; emit rebalance and drift alerts.

Every strategy inherits BaseStrategy and must route orders through self.risk_engine.check_and_place(...). There is no path from a strategy to the broker that bypasses the risk engine.

Parameter details and the reasoning behind each value are in docs/STRATEGIES.md.


Risk engine

Global limits are constants in risk/limits.py, deliberately not in YAML, so no config change can loosen them.

Limit Value
Daily loss kill-switch ₹3,500
Weekly loss limit ₹7,000
Monthly drawdown per sleeve 25% → auto-disable
Max concurrent open positions 8
Total margin used cap ₹70,000
MIS auto-square-off 15:15 IST
MCX new-entry cutoff / square-off 22:30 / 23:00 IST
Earnings blackout (futures, options) 3 trading days

Circuit breaker, on 5-minute rolling windows:

Symptom Threshold
Order rejections > 3
WebSocket disconnects ≥ 2
Broker API errors ≥ 5

Separately, a rolling 20-trade P&L check auto-disables any sleeve that loses more than 3% of its allocated capital over its last 20 trades.

These numbers are sized for a ₹1,00,000 account. Edit capital in config/settings.yaml and the constants in risk/limits.py to match your own account before going live.


Backtesting

Both engines reuse the live charge and slippage model, so backtested P&L is directly comparable to paper and live results. Bars are cached in a separate DuckDB file to avoid contending with the live database.

# Intraday opening-range breakout
python -m backtest.run_orb --start 2026-03-01 --end 2026-04-30
python -m backtest.run_orb --start 2026-03-01 --end 2026-04-30 --grid
python -m backtest.run_orb --start 2026-03-01 --end 2026-04-30 --regime-filter

# Swing momentum
python -m backtest.run_swing --start 2024-01-01 --end 2026-01-01
python -m backtest.run_swing --start 2024-01-01 --end 2026-01-01 --grid --export trades.csv

Both require a fresh access_token.txt, since historical bars come from the Kite API.

The monthly walk-forward optimizer runs grid searches over rolling train/test splits and writes results to pending_changes/:

python scripts/run_walkforward.py
python scripts/run_walkforward.py --orb-only

A word of caution learned the hard way here: an early parameter change looked great on a 5-trade sample and was strictly worse across 84 trades. Treat anything under ~30 round-trips as noise, and prefer walk-forward results over single-window grids.


The LLM feedback loop

Four places call OpenAI, all unattended:

Job When Output
News parser Pre-market Structured signals into the signals table
Trade journal EOD, per closed round-trip data/journal/YYYY-MM-DD/<strategy>__<symbol>.md
Weekly report Sunday 18:00 data/journal/weekly/<iso-week>.md + Telegram
Config review Sunday 18:00 pending_changes/<iso-week>.json

The journal gets full trade context including direction, entry and exit prices, charges, and a granular exit reason parsed from the order tag (exit_stop vs exit_target vs auto_squareoff_mis), so the model can tell a stop-out from a target hit and reads short trades correctly.

The config reviewer reads recent journals, P&L, and the drift report, then proposes exactly one narrow parameter change with evidence and a confidence score. It writes a JSON file and a Telegram summary.

It never edits config/settings.yaml itself. Applying a suggestion is a deliberate human action. Given that an LLM reading a 16-trade sample confidently recommended disabling a strategy two weeks running — based on trades from before a filter change that had already invalidated them — that boundary matters.

The drift monitor compares live expectancy, win rate, and profit factor against each strategy's backtest_baseline. Set evaluated_since in that baseline when you materially change a strategy so pre-change trades stop polluting the comparison.


Configuration reference

File Purpose Edit?
.env Secrets and the paper/live switch Yes, required
config/settings.yaml Capital allocation, per-strategy tunables, charges, session times, slippage Yes
config/universe.yaml Tradable symbol lists and your excluded holdings Via build_safe_universe.py
config/risk_limits.yaml Documentation mirror of the hard limits No — reference only; the real values are in risk/limits.py

The first thing to change is capital in config/settings.yaml. It ships sized for a ₹1,00,000 account split across sleeves.

Environment variables, all read via .env:

Variable Required Default
KITE_API_KEY, KITE_API_SECRET, KITE_USER_ID Yes
OPENAI_API_KEY Yes
OPENAI_MODEL No gpt-5.5
OPENAI_REASONING_EFFORT No low
TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID Yes
PAPER_TRADING No true
VM_HOST, VM_USER, VM_SSH_KEY, REMOTE_PROJECT_ROOT No used only in alert text

Project layout

.
├── auth.py                     # Daily Kite login (Flask callback on 127.0.0.1:5000)
├── adapters/                   # KiteAdapter (live) + PaperAdapter (simulated fills)
├── orchestrator/               # 5 runnable entry points + shared bootstrap
├── strategies/
│   ├── base.py                 # BaseStrategy lifecycle hooks
│   ├── executes/               # Order-placing sleeves
│   └── monitors/               # Alert-only sleeves
├── risk/                       # Hardcoded limits, circuit breaker, sizing, sleeve state
├── oms/                        # Order state machine + broker reconciliation
├── data/                       # DuckDB store, instrument cache, universe builder
├── analytics/                  # FIFO P&L, drift, equity curve, journaler, LLM review
├── backtest/                   # ORB + swing engines, shared metrics and data loaders
├── llm/                        # OpenAI client, news parser, journal, weekly report
├── monitoring/                 # Dashboard snapshot writer
├── notifications/              # Telegram alerter
├── scripts/                    # Operational CLIs (preflight, dashboard, watchdog, ...)
├── deploy/                     # systemd units, installer, logrotate, deploy scripts
├── config/                     # settings.yaml, universe.yaml, risk_limits.yaml
├── docs/                       # Architecture, deployment, strategy notes
└── tests/                      # ~100 tests, offline by default

Runtime artifacts (all gitignored): access_token.txt, data/store/, data/journal/, pending_changes/, logs/, run/.


Testing and code quality

pytest -m "not live"     # offline suite, no network, no credentials needed
pytest                   # includes live read-only Kite smoke tests
ruff check .
black .

The live smoke tests in tests/smoke_test.py hit the real Kite API for a profile lookup, an instrument list, and an LTP query. They are read-only and never place orders, and they skip automatically when access_token.txt is missing. Use -m "not live" in CI.


Phased rollout

Do not skip these. The system is only as safe as the discipline applied to turning it on.

Phase Window What runs At risk
0 — wiring Days 0–2 Auth, instrument refresh, pre-market and intraday loop. Verify Telegram and the dashboard. ₹0
1 — full paper Weeks 1–4 Every sleeve in paper. Confirm fill simulation, daily P&L, circuit-breaker behaviour, EOD reconciliation. ₹0
2 — one live sleeve Week 5 One strategy live at 50% sizing, everything else still paper. ≤ ₹15k
3 — graduated expansion Week 6+ Add one sleeve at a time. Each needs 4+ weeks of clean paper P&L, and its first live week runs at half size. Up to the ₹70k margin cap

Before promoting any sleeve from paper to live:

  • 4+ weeks of paper trades in daily_pnl, aggregate flat or positive
  • Zero unexplained reconciliation discrepancies in the last 5 sessions
  • You have received and acted on a real Telegram alert (do a circuit-breaker drill)
  • build_safe_universe.py has been run and your holdings are excluded
  • Sizing halved for the first live week

Troubleshooting

TokenException: Incorrect api_key or access_token — the token expired. Run python auth.py. Tokens die daily around 06:00 IST.

Login callback never fires — the redirect URL in your Kite app must be exactly http://127.0.0.1:5000/callback. On a remote VM you also need the tunnel: ssh -L 5000:localhost:5000 ....

IOException: Could not set lock on file ... trading.duckdb — DuckDB allows one writer. An orchestrator is running. Use a read-only connection for analysis, or wait for the session to end. The dashboard already does this correctly.

Dashboard shows P&L of ₹0.00 despite fills — P&L only materializes on closed round-trips. If a strategy holds positions across days, check open positions instead. To rebuild history: python scripts/backfill_pnl.py.

No Telegram messages — run python scripts/preflight.py. Most often the chat ID is wrong: send your bot a message first, then re-run python scripts/get_telegram_chat_id.py.

Strategy places no orders — check python scripts/sleeve.py status (it may be auto-disabled by the rolling-loss breaker), confirm the symbols aren't in excluded_holdings, and grep the logs for RISK DENY.

Systemd service fails immediatelyjournalctl -u <service> -n 50 --no-pager. Usually a stale token, since preflight is a dependency and exits non-zero.


Cost

Item Monthly
Zerodha Kite Connect API ₹2,000
Oracle Cloud Always Free VM ₹0
OpenAI API (~30 trades/week) ₹250–400
Total ≈ ₹2,300

The API subscription dominates and is fixed regardless of how much you trade, which is worth weighing against your expected returns.


Contributing

Issues and pull requests are welcome. Please keep the existing conventions: type hints on public functions, loguru rather than stdlib logging, no magic numbers in trading paths, and every new strategy inheriting BaseStrategy and routing through the risk engine. Run ruff check ., black ., and pytest -m "not live" before opening a PR.


Disclaimer

This software is provided for educational purposes only. It is not financial advice.

Trading Indian equities, futures, options, and commodities carries substantial risk of loss. Algorithmic trading adds failure modes that manual trading does not have: bugs, stale data, connectivity loss, and unattended execution against a moving market.

The authors accept no liability for financial losses arising from use of this software. You are solely responsible for orders placed through your account.

Independently verify the charge, brokerage, and tax assumptions in config/settings.yaml against your own contract notes before treating simulated P&L as realistic. Run in paper mode for weeks. Start small. Never deploy capital you cannot afford to lose.


License

MIT — see LICENSE.

About

Multi-strategy algorithmic trading bot for Indian markets via Zerodha Kite Connect. Paper + live adapters, hardcoded risk engine, backtesting, systemd automation, and an LLM feedback loop.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages