Automated trading bot for Nifty options based on 5 EMA crossing VWAP on 5-minute candles.
Entry Signal:
- BUY: When 5 EMA crosses above VWAP → Buy CE (Call) option
- SELL: When 5 EMA crosses below VWAP → Buy PE (Put) option
Exit Rules (Fixed Target/SL, 1:2 R:R):
- Target: +50 points from entry
- Stop Loss: -25 points from entry
- No breakeven/trailing: trade closes at target, SL, or force close - nothing in between
- Force Close: 3:15 PM if still open
Philosophy: One trade per day, fixed 1:2 risk:reward. Inspired by Tom Hougaard's "Best Loser Wins" - cut losses early, but wins are capped rather than left to run.
Fyers API (Candles) → Signal Detection → Kite API (Orders) → Telegram (Alerts)
| Component | Purpose |
|---|---|
vwap_ema_signal.py |
Main bot - fetches candles, calculates indicators, detects signals |
trade_manager.py |
Enforces trade rules, monitors positions, tracks P&L |
option_selector.py |
Selects strike based on delta (~0.7 ITM) or premium mode |
kite_api.py |
Zerodha order execution |
Findings from running the bot live (paper) and backtesting the trade log. Full detail in LEARNINGS.md.
The system is asymmetric by design, not by accident. Across 32 logged trades the win rate is ~25% (8W/24L), but the average win (+83.5 pts) is ~3.5× the average loss (−23.6 pts), giving a positive expectancy of ~+3.2 pts/trade. Frequent small stop-losses punctuated by rare large runners is the intended texture — a cluster of SL hits is normal, not a malfunction.
Crossover strength has no predictive edge — so I didn't add a filter for it. It was tempting to filter out "thin" EMA/VWAP crossovers after a losing streak, but the data refuted it: correlation between crossover separation and outcome was −0.03, and the three widest crossovers in the sample all lost. Threshold sweeps swung wildly on the small sample (classic overfitting), so the filter was rejected.
Time of day looked like an edge, but live results didn't hold up. Entries before 10:00 AM were the money pit in the initial backtest (−157 pts over 13 trades), so TRADE_START_TIME was pushed to 10:00 on Jun 16. The post-implementation tally told a different story: the filter blocked 3 would-be wins against only 1 avoided loss, so it was reverted back to TRADE_START_TIME=09:30 on Jun 27. The backtest isn't refuted — the before-10:00 window may still underperform over a larger sample — but the small live sample didn't support keeping the filter. See LEARNINGS.md §7b for the full tally.
Points-positive but rupees-negative pointed to position sizing as the bigger lever. The sample was +101 pts yet −₹23k, because size was largest during a drawdown. The signal wasn't the main problem — risk sizing was.
Caveat: 32 trades / 8 wins is a small sample, so these splits may be partly regime-driven. The takeaway is the process — test the hypothesis against the full log, reject what overfits, and change only what the data supports.
git clone git@github.com:umeshkedimi/ema_vwap.git
cd ema_vwap
cp config.example.env config.env
# Edit config.env with your API credentialspython3 -m venv venv
source venv/bin/activate
pip install requests pandas python-dotenv| API | Purpose | Get from |
|---|---|---|
| Fyers | Historical candle data | Fyers API |
| Kite Connect | Order execution | Kite Connect |
| Telegram | Trade alerts | @BotFather |
source venv/bin/activate
python vwap_ema_signal.pySee SERVER_SETUP.md for DigitalOcean VPS setup and cron configuration.
Edit config.env:
# Trading mode
TRADING_ENABLED=true # Enable/disable order execution
PAPER_TRADING=true # true = simulate orders, false = live trading
# Trade timing
TRADE_START_TIME=09:30 # No trades before this (skip first 15 min)
TRADE_END_TIME=14:30 # No trades after this
# Trade parameters
MAX_TRADES_PER_DAY=1 # Max 1 trade per day
TARGET_POINTS=50 # Fixed target at +50 points
STOPLOSS_POINTS=25 # Fixed SL at -25 points
LOT_SIZE=195 # Quantity (1 lot = 65, this is 3 lots)
# Strike selection
STRIKE_MODE=delta # delta or premium
MIN_PREMIUM=220 # Minimum option premium
ITM_OFFSET_FOR_DELTA=150 # ITM points for delta mode- Timing: No trades before 9:30 AM or after 2:30 PM
- One at a time: Must close current trade before taking next signal
- Daily limit: Max 1 trade per day
- Fixed exit: Target +50, SL -25 (1:2 R:R), no breakeven/trailing
- Expiry: Weekly options (Tuesday expiry)
Weekly: NIFTY{YY}{M}{DD}{strike}{CE/PE} → NIFTY2651923600CE (May 19, 2026)
Monthly: NIFTY{YY}{MON}{strike}{CE/PE} → NIFTY26MAY23600CE
Month codes (weekly): 1-9 for Jan-Sep, O/N/D for Oct/Nov/Dec
ema_vwap/
├── vwap_ema_signal.py # Main bot
├── trade_manager.py # Trade rules & execution
├── option_selector.py # Strike selection
├── kite_api.py # Zerodha API client
├── review_signals.py # Backtest/review tool
├── config.env # Your credentials (not in git)
├── config.example.env # Template config
├── trade_journal.csv # Trade history with actual fills
└── SERVER_SETUP.md # Deployment guide
Fyers and Kite tokens expire daily. First-time setup — copy the template and fill in
your server IP and SSH key (the real update_tokens.sh is gitignored so credentials
stay private):
cp update_tokens.example.sh update_tokens.sh
# edit SERVER and SSH_KEY at the top, then:
./update_tokens.shOr manually update FYERS_ACCESS_TOKEN and KITE_ACCESS_TOKEN in config.env.
The bot sends alerts for:
- Bot startup and token validation
- Every candle scan (EMA/VWAP values)
- Signal detection
- Trade entry with option details
- Trade exit (target/SL hit)
- Daily summary
- Back to 1 trade per day, PAPER_TRADING=true
- Fixed target +50 / SL -25 (1:2 R:R), no breakeven/trailing
- LOT_SIZE=195 (3 lots × 65)
- MAX_TRADES_PER_DAY raised to 2 (LOT_SIZE=130, 2 lots × 65)
- Trade 2 only if Trade 1 closed with SL hit or breakeven (profit → day ends)
- Pre-10 AM block: max 1 trade before 10:00 AM
- Replaced fixed +80 target with trailing SL starting at +75
- Philosophy: Cut losses early, let winners run
- Fixed target +80, SL -25, breakeven at +50
- 1 trade per day
- Delta mode strike selection
This bot is for educational purposes. Trading involves risk. Past performance does not guarantee future results. Use at your own risk.