Skip to main content
shivam gairola..

OPEN TO SELECT PROJECTS

OPEN TO SELECT PROJECTS

OPEN TO SELECT PROJECTS

OPEN TO SELECT PROJECTS

AI & AgentsLearning

Automated Trading Bot That Trades NIFTY & BANKNIFTY On Its Own

A practical, end-to-end guide to designing an intraday algorithmic trading system in Python from a single candle to a live, risk-managed order based on a bot I built and run on a ₹4/day cloud server.

Jun 10, 202616 min read

Most "algo trading" tutorials stop at a backtest in a Jupyter notebook. This one doesn't. I wanted to understand every hard problem that sits between "I have a trading idea" and "a computer is placing real orders for me, safely, while I'm asleep" — so I built a bot that actually does it.

This post is the guide I wish I'd had. It walks through the anatomy of a real automated trading system: how it gets data, turns indicators into signals, sizes trades by risk, manages its own stops, deals with a broker whose login expires every single day, and runs unattended on a tiny cloud box with a dashboard I can check from my phone.

If you're thinking of building your own, this is the map.

⚠️ A serious word first. Algorithmic trading involves real financial risk — you can lose money faster than you can react. Nothing here is financial advice. Broker APIs and regulations change constantly (this bot already had to adapt to a rule change banning market orders). Treat everything below as an engineering walkthrough, and never run something like this with real capital unless you understand every line of the risk logic and accept full responsibility for the outcome.


Table of Contents


What the bot actually does

Every 15 minutes during market hours (09:15–15:20 IST, Mon–Fri), the bot runs one loop:

  1. Pulls fresh candles for each instrument (historical + today's forming candles, stitched together).
  2. Runs a strategy on the latest closed candle to produce a BUY / SELL / NONE signal.
  3. Runs pre-trade risk checks — already in this position? At max positions? Over the daily loss limit? Taking a correlated bet?
  4. Sizes the trade so a stop-out costs at most 1% of capital.
  5. Places a marketable-limit order, waits for the fill, and records the position with its stop and target.
  6. Manages open positions — exits the instant a stop or target is touched.
  7. Squares off everything at 15:25 so it's always flat overnight (it trades intraday only).

Around that, it sends a morning briefing, an evening P&L summary, handles its daily broker login with one tap on a Telegram link, and serves a live dashboard. On a normal day, the only thing I do is tap that login link.

Now let's build it up, one piece at a time.


Step 1 — Think in modules, not scripts

The single most important early decision is structure. A trading bot has many distinct jobs — data, strategy, risk, execution, state, reporting — and mashing them into one file guarantees pain. Each concern gets its own module with a clear boundary.

Here's the architecture I landed on. It's a single Python process: a scheduler drives the trading logic, and a Flask dashboard runs in a background thread of the same process, sharing state through a thread-safe singleton (no database or message queue needed).

                          ┌─────────────────────────────────────────┐
                          │              main.py                                                                                        │  
                          │   APScheduler (Asia/Kolkata cron jobs)                                         │
                          └──────┬───────────────────────┬──────────┘
                                              │                                                                  │
                    every 15 min  │                                                                  │  08:00 / 21:00 / 15:25 / 08:45
                                             ▼                                                                 ▼
                          ┌───────────────┐      ┌──────────────────┐
                          │ trading_loop                │       │ briefings /                              │
                          │                                           │       │ square-off /                          │
                          │                                           │       │ token refresh                       │
                          └──┬────────────┘      └──────────────────┘
                                   │
       ┌──────────────┬─────────────┬───────────┬───────────┬─────  ─────┐
       ▼                                      ▼                                    ▼                              ▼                              ▼                             ▼
 ┌──────────┐ ┌───────────┐ ┌─────────┐ ┌──────────┐ ┌───────────┐ ┌──────────┐
 │  data/                │ │  strategies/        │ │  risk/                │ │ execution/      │ │ portfolio/           │  │ reporting/       │
 │ market              │ │  signal                  │ │ sizing,              │ │ orders,             │  │  state +               │  │ Telegram         │
 │  data                  │ │ generation         │ │ stops,             │ │ fills,                    │  │  CSV                     │  │ / email              │
 │                             │ │                                │ │ limits               │ │ retries              │  │ persist                │  │                            │
 └────┬─────┘ └───────────┘ └─────────┘ └────┬─────┘ └───────────┘ └──────────┘
               │                                                                                                       │
               └──────► Broker API v2/v3   ◄───────────────┘
                                          ▲
                                           │ daily OAuth token
                    ┌───────┴────────┐
                    │  auth/ (OAuth)                 │◄──── /callback ──── web/dashboard.py (Flask)
                    └────────────────┘                          ▲
                                                                                                  │  live UI, controls,
                                                                       web/state.py │  SSE log stream
                                                                     (shared state)┘
ModuleResponsibility
mainEntry point. Logging, the scheduler (trading loop + briefings + square-off + token refresh), launches the dashboard thread.
configSingle source of truth: credentials, instruments, strategy params, risk rules, market hours.
authOAuth 2.0 token lifecycle and the one-tap login flow.
dataCandles, live quotes, front-month contract resolution, optional WebSocket stream.
strategiesPluggable strategies behind one common interface.
riskPosition sizing, stop/target calculation, correlation filter, daily-loss circuit breaker.
executionOrder placement, fill polling, cancels, retry-on-exit, bulk square-off.
portfolioIn-memory positions + P&L, with CSV persistence that survives restarts.
reportingMorning and evening briefings over Telegram.
webFlask dashboard, JSON APIs, live log stream, controls, and the public OAuth callback.

One rule guides all of it: the main loop never crashes. Every scheduled job and every per-instrument evaluation is individually wrapped in try/except with logging, so one bad API response or one malformed candle can't take down the whole bot mid-session.


Step 2 — Getting reliable market data

Everything downstream depends on clean candles, and this is trickier than it sounds. The subtle problem: broker "historical candle" endpoints usually return only completed days — they don't include today's forming candles. If you only use historical data, your bot is effectively trading yesterday.

The fix is to stitch two sources together:

  • Historical candles for the completed prior days.
  • Intraday candles for today's live, forming session.

Then concatenate them, de-duplicate on the candle timestamp, sort, and take the most recent N. And crucially — make the intraday fetch best-effort: if it fails (pre-market, holiday, a network blip), fall back to the historical tail rather than crashing the loop.

Two more data lessons worth knowing up front:

  • Futures contracts expire. You can't hardcode an instrument key — the "front-month" contract rolls every expiry. The bot downloads the exchange's instruments master file each morning and resolves the current nearest-expiry, not-yet-expired contract dynamically.
  • Decide on closed vs. live candles. This bot makes decisions on the last closed candle (no repainting, no acting on a half-formed bar). A live WebSocket price stream is a nice-to-have for tighter stop monitoring, not a hard dependency — so it degrades to simple REST polling if the streaming SDK isn't available.

Step 3 — Turning data into a signal (the strategy)

Keep strategies pluggable behind a single interface. Every strategy takes a candle DataFrame and returns the same shape:

def generate_signal(self, df) -> dict:
    return {"signal": "BUY" | "SELL" | "NONE",
            "atr":    float,   # latest ATR — the risk manager uses this for sizing & stops
            "reason": str}     # plain-English explanation

That reason string is a small decision that pays off enormously: every trade the bot takes — and every one it declines — is explainable in plain English, and that explanation flows all the way to the dashboard and the evening summary. When it doesn't trade, I can see exactly why.

A tiny registry maps a strategy's name in config to its class, so adding a new one is: write the class → register it → point an instrument at it. No other code changes.

The two strategies I implemented:

1. Mean Reversion (active on NIFTY & BANKNIFTY, 15-min candles) — fades overextensions:

  • BUY when price closes below the lower Bollinger Band and RSI < 35 (oversold → bounce)
  • SELL when price closes above the upper Bollinger Band and RSI > 65 (overbought → fade)

Requiring both a band break and an RSI extreme is the whole trick — either signal alone fires far too often.

2. Trend Following (EMA crossover, for trending instruments, 4-hour candles):

  • BUY when EMA(20) crosses above EMA(50) and ADX > 25
  • SELL on the opposite cross with ADX > 25

The ADX filter is what stops it getting chopped up in sideways markets — no trade unless a genuine trend is confirmed.

Notice neither strategy is exotic. The edge isn't in a magic indicator — it's in the discipline of what surrounds it, which is the next step.


Step 4 — Risk management: the part that keeps you alive

Anyone can generate a signal. Whether you survive is decided by the risk engine, so this deserves the most care. My rule was: risk limits must be things the code physically cannot bypass, not suggestions.

1. Fixed-fractional position sizing (the 1% rule). Position size isn't a fixed number of lots — it's derived so that being stopped out costs at most 1% of capital, no matter how volatile the instrument is:

risk_amount        = capital × 1%
per-lot loss @ stop = ATR × stop_multiplier(1.5) × lot_size
lots               = floor(risk_amount / per-lot loss)

Volatile day → wider ATR → smaller position. Calm day → bigger position. The risk stays constant; the size adapts. This was the single most valuable concept I took from the whole project.

2. ATR-based stops and targets (2:1 reward-to-risk). Stop = entry ± (1.5 × ATR); target = entry ± (3.0 × ATR). Stops scale with real volatility instead of arbitrary fixed points, and a 2:1 reward-to-risk means the strategy can be right less than half the time and still make money.

3. Correlation filter. NIFTY and BANKNIFTY move together. If the bot is already long one, it won't stack a long on the other — it recognizes that as doubling the same bet, not diversifying.

4. Daily loss circuit breaker. If the day's P&L hits −3% of capital, all new entries halt for the rest of the day. Existing positions stay protected, but the bot stops digging. This is the "walk away from the table" rule, enforced in code.

5. Never widen a stop. Getting out is non-negotiable. The exit logic will retry with a wider price buffer to guarantee it gets flat — but it will never move a stop further away to avoid taking the loss.


Step 5 — Placing and managing orders

Signals and sizing are decisions; execution is where they meet reality, and reality is messy.

  • Fills aren't instant or guaranteed. After placing an order, the bot polls its status until it's complete, or cancels it if it doesn't fill within a timeout. It never assumes an order worked.
  • Use marketable-limit orders, not raw market orders. Instead of a plain market order (which exposes you to unlimited slippage — and which is now banned for API trading anyway, see below), the bot places a LIMIT order priced just across the spread using a small buffer — 0.05% for entries, a wider 0.25% for forced exits. It fills like a market order in normal conditions but caps how bad the price can get.
  • Exits get a retry policy. If an exit order fails or doesn't fill, it retries once with a wide buffer (effectively market) to guarantee getting flat — because an unclosed losing position is the one thing you can't tolerate.
  • State is persisted. Every open and closed position lives in memory but is flushed to a CSV ledger, so a crash or restart never loses the trade history — the bot re-hydrates today's trades from the CSV on boot.

Step 6 — The daily login problem (and a clever fix)

This was the most interesting engineering problem in the whole build.

The broker's access tokens expire every single day, with no refresh token — the OAuth flow mandates a human login. For an unattended bot, that's a real obstacle. My solution reduces the daily friction to a single tap:

  1. Each morning, the bot checks its token. If it's stale, it sends a Telegram message with the login link.
  2. I tap it on my phone and log in.
  3. The broker redirects to https://<my-server>/callback — a route on the bot's own dashboard, served over HTTPS.
  4. That route exchanges the auth code for a token, saves it, and the bot's wait-loop notices the fresh token and resumes automatically.
  5. If I don't log in, it re-sends a reminder every 15 minutes for up to 45 minutes.

The neat sub-problem was free HTTPS. The broker requires an HTTPS redirect URL, but I didn't want to buy a domain for a hobby project. The solution:

  • sslip.io — a free service that turns any IP into a hostname (65-2-100-76.sslip.io65.2.100.76), no domain purchase.
  • Caddy as a reverse proxy that automatically obtains and renews a Let's Encrypt certificate.

Total cost for a valid HTTPS callback: ₹0.


Step 7 — Scheduling the trading day

A trading bot is fundamentally a scheduler with strong opinions about time. I used APScheduler with cron triggers pinned to the exchange's timezone (Asia/Kolkata), with misfire-grace and coalescing so a momentary hiccup never fires duplicate loops or skips a candle.

Time (IST)JobWhat happens
08:45morning prepRefresh the token (send login link if stale) and resolve today's front-month contracts.
08:00morning briefingPre-market Telegram: previous closes, overnight cues, carried positions, today's risk budget.
09:15–15:20trading loop (every 15 min)Manage open positions, then evaluate each instrument for new entries.
15:25square-offForce-close everything and reconcile state. The bot ends the day flat.
21:00evening briefingEOD Telegram: trades, win rate, P&L, best/worst, month-to-date.

Aligning the loop to candle closes (:00, :15, :30, :45) matters — you want the strategy to run just after a candle completes, on real closed data.


Step 8 — Observability: a dashboard and daily briefings

The difference between "I hope it's working" and "I can see exactly what it's doing" is observability — and it's what finally made me trust the bot.

I built a single-page dashboard (dark theme, mobile-friendly, no build step — inline HTML/CSS/JS) that runs in a background thread of the bot process and reads shared state through a lock-guarded singleton:

  • Live P&L (today + month-to-date) against the daily loss limit
  • Open positions with their stops and targets
  • Latest signals per instrument, each with its plain-English reason
  • Recent trades from the persisted ledger
  • A live log stream via Server-Sent Events — I can watch the bot think in real time
  • Controls: pause/resume trading, and an emergency "square off ALL" button

On top of that, the Telegram briefings mean I get a pre-market plan every morning and a full P&L recap every evening without opening anything. Observability isn't a nice-to-have for an unattended system — it's the thing that lets you leave it unattended.


Step 9 — Deploying it cheaply and reliably

The workload is I/O-light — a few API calls every 15 minutes — so it runs comfortably on the cheapest cloud VM with a static IP (I used AWS Lightsail). The deployment recipe:

  • A static IP so the callback URL is stable (and can be registered with the broker).
  • Caddy for automatic HTTPS in front of the dashboard (ports 80/443 open; the dashboard's own port stays firewalled and is only reached through the proxy).
  • A @reboot cron entry so the bot restarts itself if the server reboots.
  • Server timezone set to IST, matching the scheduler.

Deliberately no database and no message queue — for a single-process, single-user bot they'd be complexity without benefit. Flat files (a token file + a CSV trade ledger + rotating logs) are enough, and the CSV doubles as crash-recovery.


Adapting to regulations

Building against a live, regulated broker API means the ground shifts under you. Two real changes this bot had to absorb mid-project:

  • Market orders were banned for API trading. The fix was already described in Step 5 — every order became a marketable-limit order with a price-protection buffer. Because I'd kept the execution layer thin and in one place, this was a contained change.
  • A whole exchange's API was temporarily paused. The strategies for those instruments simply got a config flag flipped to disabled — no code deleted, ready to re-enable when the API returns.

The lesson: keep the broker-facing layer thin and swappable, and drive instrument selection from config. Regulations will change; your architecture should absorb it without a rewrite.


What I learned

This project was as much about systems engineering as it was about trading. The biggest takeaways:

  • Risk is code, not vibes. The most valuable thing I built wasn't a strategy — it was a risk engine that makes ruinous trades structurally impossible. Sizing off ATR so risk stays constant across instruments was the key insight.
  • Robustness beats cleverness. A bot that runs unattended must assume every external call can fail. Wrapping every job, degrading gracefully, and never letting one error kill the process mattered far more than any indicator tuning.
  • Auth and infra are half the battle. The daily-token problem, free HTTPS, timezone-correct scheduling, auto-restart — none of it is glamorous, but the bot is useless without it.
  • Observability earns trust. Once I had the dashboard and the briefings, I actually believed what the bot was doing, because I could always see it.
  • Explainability by design. Threading a plain-English reason through every signal made the whole system debuggable and honest with itself.
  • Real APIs and regulations move. Keeping the broker layer thin let me adapt to a market-order ban without touching the strategy or risk code.

If you take one thing from this: don't start with the strategy. Start with the risk engine and the plumbing that keeps the thing alive. The strategy is the easy part — surviving long enough to let it play out is the hard part, and that's an engineering problem.


Built as a learning project to understand automated trading end-to-end — from a candle to a filled order, safely.

Building something in this space?

I take on select builds when the work is worth doing right.

Start a conversation