How to Do Algo Trading in Zerodha: Build an Intraday Bot with Kite Connect and Python
"How to do algo trading in Zerodha" is up over 3,000% on Google Trends in India in the last three months. Most results are course ads or a 20-line script that places one order. I've built and run an intraday bot on Kite Connect, and this post covers the parts those scripts skip: the things that decide whether a bot is safe to leave running.
I won't share my strategy's rules or results. They're not the point, and you shouldn't copy someone else's anyway. Every number below that looks like a strategy setting is an example.
Last checked: 19 October 2026. Zerodha and SEBI change the rules often. Verify against the Kite Connect docs and zerodha.com/charges before trading real money.
Not investment advice. Intraday trading loses money for most retail traders. Paper trade first.
What you need
| Item | Cost | Notes |
|---|---|---|
| Zerodha account | Free | With F&O/intraday enabled for the segments you trade |
| Kite Connect app (Connect type) | ₹500/month | Includes live quotes and historical data |
| Kite Connect app (Personal type) | Free | Orders and account APIs only. No market data, so not enough for a bot that reacts to prices |
| A server with a static IP | ₹400–1,000/month | Required for API orders (below) |
Python 3.11+ and kiteconnect |
Free | pip install kiteconnect |
Zerodha cut Connect from ₹2,000 to ₹500 a month in 2025, and order placement has been free since March 2025.
The rules you can't skip (2026)
SEBI's retail algo framework now applies to API trading. In practice, for a personal bot on Zerodha:
Paper trading needs none of the IP setup, which is one more reason to start there.
Step 1: the daily login
Kite Connect has no "log in once" option. The access token expires every morning, so the bot needs a fresh login each trading day:
1. Open the login URL and sign in with your Zerodha credentials and 2FA.
2. Zerodha redirects to your app's redirect URL with a request_token.
3. Exchange it for an access_token using your API secret.
import os
from kiteconnect import KiteConnect
kite = KiteConnect(api_key=os.environ["KITE_API_KEY"])
print("Log in here:", kite.login_url())
request_token = input("Paste request_token from the redirect URL: ").strip()
session = kite.generate_session(request_token, api_secret=os.environ["KITE_API_SECRET"])
# keep the token private: owner-only file permissions
with open(".kite_token", "w") as f:
f.write(session["access_token"])
os.chmod(".kite_token", 0o600)
Zerodha expects this login to be done by a person, so don't script your TOTP. I log in from a small password-protected dashboard each morning: one click, and the redirect lands back on the dashboard, which saves the token.
If you run several accounts, add &redirect_params=account%3D to the login URL. Zerodha sends it back on the redirect, so one callback URL can tell whose login it was.
Check the token before trading
def current_kite():
kite = KiteConnect(api_key=os.environ["KITE_API_KEY"])
kite.set_access_token(open(".kite_token").read().strip())
try:
kite.profile() # cheap call; fails if today's login hasn't happened
except Exception:
return None
return kite
The bot should refuse to start without a valid token, not fail halfway through the morning.
Step 2: respect the rate limits
From the Kite docs:
| Endpoint | Limit |
|---|---|
Quote (ltp, quote) |
1 request/second |
| Historical candles | 3 requests/second |
| Order placement | 10/second, 400/minute, 5,000/day |
| Modifications | 25 per order, then cancel and re-place |
The quote limit is the one you'll hit. Ask for all your symbols in one call rather than one call per stock:
prices = kite.ltp(["NSE:SBIN", "NSE:ITC", "NSE:INFY"])
If you run more than one bot on the same login, they share that 1/second budget. I put a small lock file in front of ltp, so the bots take turns and reuse a result that's under a second old instead of each calling Kite.
Kite also drops idle keep-alive connections now and then ("Remote end closed connection without response"). Retry GET requests automatically, but never auto-retry an order: a retried POST can place the order twice.
from urllib3.util.retry import Retry
pool = {"max_retries": Retry(total=2, backoff_factor=0.2, allowed_methods={"GET"})}
kite = KiteConnect(api_key=key, pool=pool)
Step 3: place orders that protect themselves
An intraday (MIS) entry, then an immediate stop-loss order at Zerodha, not just in your code:
entry_id = kite.place_order(
variety=kite.VARIETY_REGULAR, exchange=kite.EXCHANGE_NSE,
tradingsymbol="SBIN", transaction_type=kite.TRANSACTION_TYPE_BUY,
quantity=qty, product=kite.PRODUCT_MIS,
order_type=kite.ORDER_TYPE_MARKET, market_protection=-1,
)
stop_id = kite.place_order(
variety=kite.VARIETY_REGULAR, exchange=kite.EXCHANGE_NSE,
tradingsymbol="SBIN", transaction_type=kite.TRANSACTION_TYPE_SELL,
quantity=qty, product=kite.PRODUCT_MIS,
order_type=kite.ORDER_TYPE_SLM, trigger_price=stop_price, market_protection=-1,
)
market_protection=-1 lets Zerodha apply automatic protection, so a market order can't fill at a wild price in a thin moment.
Why the stop goes to Zerodha: if your server crashes, loses network, or the Python process dies, a stop that only lives in your code does nothing. A stop at the exchange still fires.
The safety rules I'd treat as the minimum:
kite.positions()["day"]. If they disagree, refuse to trade until a human looks.Targets: use a LIMIT order
If you watch the price and send a market sell when it touches your target, you'll often fill below it. In my first live trade the market exit filled ₹2.16 a share under the target, which cost about 60% of the planned profit. A LIMIT order at the target price fixes that.
You can't simply rest a target and a stop together on MIS: GTT OCO isn't available for MIS, and a second resting sell needs its own margin. So the usual pattern is a resting SL-M at Zerodha plus your code placing the LIMIT target when the price gets close.
Step 4: do the charge math before anything else
This is where most intraday ideas die. Zerodha's MIS charges per round trip (one buy + one sell) on NSE equity:
| Charge | Rate |
|---|---|
| Brokerage | 0.03% per order, max ₹20 |
| STT | 0.025% on the sell side |
| NSE transaction charges | 0.00307% both sides |
| SEBI fee | ₹10 per crore |
| Stamp duty | 0.003% on the buy side |
| GST | 18% on brokerage + exchange + SEBI |
As code (keep the rates in config, because budgets change them):
def round_trip(buy, sell):
brokerage = sum(min(v * 0.0003, 20) for v in (buy, sell))
exchange = (buy + sell) * 0.0000307
sebi = (buy + sell) * 10 / 1e7
stt = sell * 0.00025
stamp = buy * 0.00003
gst = (brokerage + exchange + sebi) * 0.18
return round(brokerage + exchange + sebi + stt + stamp + gst, 2)
Example: a ₹2,00,000 position with a 0.2% target.
Gross profit at target ₹400
Charges (round trip) ₹118
Net ₹282 → charges ate ~30%
And with a 1% stop on the same trade:
Gross loss −₹2,000
Charges −₹118
Net −₹2,118
With those numbers, one stop-loss wipes out about 7–8 winning trades. A small-target strategy needs a very high win rate just to break even. Put this function in your code before you write a single strategy rule.
Intraday profit is taxed as speculative business income at your slab rate on the year's net total, not per trade. Talk to a CA.
Step 5: paper trade, record everything, replay
Have the bot run in paper mode by default, with live trading needing two switches (mode: live in config and a --live flag). That way a typo can't put money in the market.
While paper trading, write every price the bot sees to a CSV (prices_YYYYMMDD.csv). Then you can replay real days through new code:
python -m bot.replay logs/prices_*.csv --set target_pct=0.3
Lessons from doing this for weeks:
Step 6: run it on a VPS, on IST
The bot needs a machine that's on at 9:15 AM IST with the registered static IP. A ₹400–1,000/month Linux VPS works. Most VPS clocks run on UTC, so convert carefully:
# trading-bot.timer — 09:02 IST = 03:32 UTC, weekdays
[Timer]
OnCalendar=Mon..Fri 03:32 UTC
[Install]
WantedBy=timers.target
The bot itself should skip NSE holidays. Keep them in a holidays.yaml you update every year, and decide what happens on special sessions like Muhurat trading:
def closed_reason(day):
if day.weekday() >= 5:
return day.strftime("%A")
return HOLIDAYS.get(day) # loaded from holidays.yaml
Two more things worth doing:
Checklist before real money
kite.positions()Want a bot like this built and hosted for your own Zerodha account, with your rules? Get in touch.
Related Posts
Built something similar or want to talk through the architecture? Get in touch.