This is the whole thing end to end — project setup, strategy, paper trading, going live, and deploying somewhere it keeps running when you close your laptop. By the end you will have a bot on a server, logging everything it does.
It is a learning project, not a money-making one. The strategy is deliberately simple so you can focus on the plumbing, which is the part that actually matters.
What you need first
- Python 3.10 or newer
- A Zerodha account with Kite Connect — ₹2,000 a month
- Enough Python to read the code below without panic
- A VPS, around ₹500 a month on Vultr or DigitalOcean
Step 1 — Set up the project
mkdir trading-bot && cd trading-bot
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install kiteconnect pandas python-dotenv schedule
Layout
trading-bot/
├── .env
├── config.py
├── auth.py
├── strategy.py
├── bot.py
├── logger.py
└── logs/
Splitting it into files this early looks like overkill for a hundred lines. It stops being overkill the first time you want to change the strategy without touching the order code.
Step 2 — Config and authentication
.env
KITE_API_KEY=your_key
KITE_API_SECRET=your_secret
KITE_ACCESS_TOKEN=
SYMBOL=RELIANCE
INSTRUMENT_TOKEN=738561
QUANTITY=10
PAPER_TRADE=true
Note PAPER_TRADE=true. That flag is the most important line in the project and it stays true for weeks.
auth.py
from kiteconnect import KiteConnect
from dotenv import load_dotenv, set_key
import os
load_dotenv()
def get_kite():
kite = KiteConnect(api_key=os.getenv("KITE_API_KEY"))
token = os.getenv("KITE_ACCESS_TOKEN")
if not token:
print("Visit:", kite.login_url())
request_token = input("Paste request_token: ")
data = kite.generate_session(request_token, api_secret=os.getenv("KITE_API_SECRET"))
token = data["access_token"]
set_key(".env", "KITE_ACCESS_TOKEN", token)
kite.set_access_token(token)
return kite
Add .env to .gitignore now, before you forget. API secrets in a public repository is a bad afternoon.
Step 3 — The strategy
strategy.py
import pandas as pd
def ma_crossover_signal(df):
df['ma_fast'] = df['close'].rolling(9).mean()
df['ma_slow'] = df['close'].rolling(21).mean()
if len(df) < 22:
return None
last = df.iloc[-1]
prev = df.iloc[-2]
if prev['ma_fast'] <= prev['ma_slow'] and last['ma_fast'] > last['ma_slow']:
return "BUY"
if prev['ma_fast'] >= prev['ma_slow'] and last['ma_fast'] < last['ma_slow']:
return "SELL"
return None
Notice it compares the previous bar with the current one. Checking only whether fast is above slow would fire a signal on every single bar of a trend instead of once at the crossing.
Step 4 — The main loop
bot.py
import os, time
import pandas as pd
from datetime import datetime, timedelta
from auth import get_kite
from strategy import ma_crossover_signal
from logger import log
PAPER = os.getenv("PAPER_TRADE", "true") == "true"
SYMBOL = os.getenv("SYMBOL")
TOKEN = int(os.getenv("INSTRUMENT_TOKEN"))
QTY = int(os.getenv("QUANTITY"))
def fetch_candles(kite):
end = datetime.now()
start = end - timedelta(days=5)
return kite.historical_data(TOKEN, start, end, "5minute")
def place_order(kite, side):
if PAPER:
log(f"[PAPER] {side} {QTY} {SYMBOL}")
return
order_id = kite.place_order(
variety="regular",
exchange="NSE",
tradingsymbol=SYMBOL,
transaction_type=side,
quantity=QTY,
product="MIS",
order_type="MARKET"
)
log(f"[LIVE] {side} order: {order_id}")
def main():
kite = get_kite()
log("Bot started — paper=" + str(PAPER))
while True:
try:
now = datetime.now()
if not (9 <= now.hour < 15 or (now.hour == 15 and now.minute < 15)):
time.sleep(60); continue
candles = fetch_candles(kite)
df = pd.DataFrame(candles)
signal = ma_crossover_signal(df)
if signal:
log(f"Signal: {signal}")
place_order(kite, signal)
time.sleep(300)
except Exception as e:
log(f"ERROR: {e}")
time.sleep(60)
if __name__ == "__main__":
main()
Two details worth pointing out. The try-except around the whole loop means a network blip logs an error and retries instead of killing the process — which is the difference between a bot and a script. And the market hours check keeps it from placing orders at eight in the evening when a bug in your time logic would otherwise be very expensive.
Step 5 — Logging
logger.py
import os
from datetime import datetime
os.makedirs("logs", exist_ok=True)
def log(msg):
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
line = f"[{ts}] {msg}"
print(line)
with open(f"logs/{datetime.now().strftime('%Y-%m-%d')}.log", "a") as f:
f.write(line + "\n")
Twelve lines, and it will be the file you open every time something surprises you. Log more than feels necessary.
Step 6 — Paper trade, properly
python bot.py
Run it in paper mode for at least two weeks and actually read the logs each evening. You are checking three things: does it fire when you expect, does it fire when you do not expect, and does it survive a day where the API misbehaves.
Only when all three look right should you set PAPER_TRADE=false — and then start with a quantity small enough that a bad day is a lesson rather than a problem.
Step 7 — Deploy to a VPS
- Create an Ubuntu 22.04 server on Vultr or DigitalOcean — ₹400 to ₹600 a month.
- SSH in and clone your repository.
- Repeat the virtualenv setup there.
- Run it under systemd so it restarts on failure and survives your SSH session closing.
systemd service
# /etc/systemd/system/tradingbot.service
[Unit]
Description=Trading Bot
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/trading-bot
ExecStart=/home/ubuntu/trading-bot/venv/bin/python bot.py
Restart=always
[Install]
WantedBy=multi-user.target
sudo systemctl enable tradingbot
sudo systemctl start tradingbot
sudo journalctl -u tradingbot -f
Restart=always is doing real work here. If the process dies at 10:30, systemd brings it back in seconds instead of leaving you flat with an open position.
Step 8 — Know when something breaks
A bot running silently on a server you never look at is a liability. Push alerts somewhere you actually read.
import requests
def telegram_alert(msg):
requests.post(
f"https://api.telegram.org/bot{TOKEN}/sendMessage",
data={"chat_id": CHAT_ID, "text": msg}
)
Alert on every order and every exception. Not on every loop iteration — you will mute the channel within a day, and then it is worse than having no alerts at all.
What to build next
- Stop-loss and target, which this version does not have
- Position sizing based on capital rather than a fixed quantity
- Support for multiple symbols
- A daily loss limit and a kill switch
- A backtesting module so you can test changes before deploying them
The stop-loss is not optional in any real version. It is left out here only so the main loop stays readable.
Before you use this with money
This is a teaching example. A production system needs thorough backtesting, proper risk management, order state reconciliation and a tested kill switch. If you want that built properly, we build complete systems — and we hand over the source code, so it stays yours.
FAQs
Does the bot need to run 24/7?
It only trades during market hours, but keeping the process running on a VPS avoids the real failure modes — a laptop that sleeps, a Wi-Fi drop, or a power cut at 10:15.
How long should I paper trade before going live?
At least two weeks, and ideally across different market conditions. You are checking that it fires when expected, does not fire when it should not, and survives a day when the API is unreliable.
Why does the code split into so many files for such a small bot?
So you can change the strategy without touching the order code. The first time you want to test a different signal, that separation saves you from breaking something that already worked.
Is a stop-loss included in this example?
No, deliberately — it is left out to keep the main loop readable. Any version that touches real money needs one, along with a daily loss limit and a kill switch.
Need a custom solution?
Instacode builds production-grade software — algo trading, ecommerce, web apps. Let's talk.
Get in Touch
💬 Comments (0)
Be the first to comment 🚀