How I combined institutional order flow with machine learning — and what 300 rows of NIFTY data revealed
Most retail traders use VWAP as a single indicator. “Price above VWAP = bullish, below = bearish.” It’s not wrong, but it’s dangerously incomplete.
Institutional algorithms don’t just trade VWAP — they hunt for VWAP reclaim and rejection with order book imbalance, OI shifts, and volume delta. I built a system that combines VWAP breakout with XGBoost to filter false signals.
Here’s the full backtest, code, and results.
The strategy in 60 seconds
Entry:
- Price crosses above VWAP with volume > 1.5x average
- PCR (Put-Call Ratio) < 0.8 (bullish sentiment)
- OBV slope positive for last 5 candles
Exit:
- ATR-based trailing stop: 1.5x ATR from entry
- OR time-based: exit at 15:15 IST if still holding
Filters:
- ADX > 25 (trending market only)
- Not within 5 minutes of news events
- Daily volatility regime: HIGH_VOLATILITY or NORMAL only
Feature engineering: 83 features
My backend computes these on every 1-minute bar:
Price-based (20 features)
# Mac/Linux/Termux Terminal
features = [
'ema9', 'ema20', 'ema50', 'sma200',
'ema20_slope', 'ema9_20_cross', 'ema20_50_cross',
'close_above_sma200', 'close_vs_ema50_pct',
'bollinger_upper', 'bollinger_lower', 'bollinger_width',
'returns_1m', 'volatility_20', 'volatility_60'
]
Windows CMD (PowerShell):
$features = @(
'ema9', 'ema20', 'ema50', 'sma200',
'ema20_slope', 'ema9_20_cross', 'ema20_50_cross'
)
Volume-based (15 features)
# Terminal commands to compute on live data
volume_ratio = current_volume / volume_sma20
volume_spike = 1 if volume_ratio > 1.5 else 0
volume_delta = buy_volume - sell_volume
cum_volume_delta_20 = sum(volume_delta[-20:])
obv_slope = (obv[-1] - obv[-5]) / 5
mfi = 100 - (100 / (1 + (positive_mfi / negative_mfi)))
Derivatives-based (25 features)
# Dhan API call to fetch option chain
curl -X POST https://api.dhan.co/v2/optionchain \
-H "Content-Type: application/json" \
-H "access-token: YOUR_TOKEN" \
-d '{"securityId":"13","exchangeSegment":"IDX_I"}'
# Extract PCR
pcr = put_oi / call_oi
iv_skew = iv_call_atm - iv_put_atm
Time-based (23 features)
minutes_since_open = (current_time - 09:15) / 60
session_progress = minutes_since_open / 225 # 225 = total market minutes
day_of_week = timestamp.weekday()
is_first_hour = 1 if minutes_since_open < 60 else 0
is_last_hour = 1 if minutes_since_open > 195 else 0
Backtest setup
Data source: Dhan Historical API + NSE downloadable EOD files
Period: January 2024 - July 2026
Capital: ₹10 lakh
Position sizing: 1 lot NIFTY per signal
Commission: ₹20 per trade (Dhan charges)
Environment:
- MacBook Air M2 (Mac Terminal)
- Backend: Python 3.12, Flask
- ML: XGBoost 2.0, scikit-learn
- Data storage: SQLite
Results
| Metric | VWAP Only | VWAP + XGBoost |
|---|---|---|
| Total Trades | 342 | 127 |
| Win Rate | 48.2% | 67.3% |
| Sharpe Ratio | 0.82 | 1.94 |
| Max Drawdown | -18.4% | -8.2% |
| Net Profit | +12.1% | +31.7% |
| Profit Factor | 1.34 | 2.21 |
Key finding: XGBoost eliminated 63% of trades while improving returns by 2.6x.
Feature importance
The top 5 features from XGBoost:
-
vwap_dist— distance from VWAP as percentage -
obv_slope— OBV trend direction -
volume_spike— unusual volume detection -
pcr— Put-Call Ratio sentiment -
ema20_slope— medium-term trend strength
Live deployment on Dhan
Backend config (Flask):
# Mac/Linux/Termux
export DHAN_CLIENT_ID=1110480081
export DHAN_TOKEN=your_token
export PORT=5050
python backend/app.py
# Windows CMD
set DHAN_CLIENT_ID=1110480081
set DHAN_TOKEN=your_token
set PORT=5050
python backend\app.py
Frontend (Next.js dashboard):
cd dashboard
npm run dev
# Open http://localhost:3000
Auto-start on boot:
Mac (launchd):
# Already set up with com.ai-trader.backend.plist
Windows (Task Scheduler):
- Create task → Trigger: At startup
- Action: Start program
python - Arguments:
C:\AI-trader\backend\app.py
Linux/Termux (cron):
@reboot cd /path/to/backend && python app.py &
The code that matters
XGBoost training (Python):
import xgboost as xgb
from sklearn.model_selection import train_test_split
X = df[feature_columns]
y = (df['close'].shift(-5) > df['close']).astype(int) # 5-min forward return
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
model = xgb.XGBClassifier(
n_estimators=200,
max_depth=4,
learning_rate=0.05,
subsample=0.8,
colsample_bytree=0.8
)
model.fit(X_train, y_train)
Signal generation:
def generate_signal(features):
prob = model.predict_proba([features])[0][1]
if prob > 0.65 and features['adx'] > 25:
return "CALL", prob
elif prob < 0.35 and features['adx'] > 25:
return "PUT", prob
return "HOLD", 0.0
What didn’t work
- Deep learning (LSTM): Overfit on 300 rows. XGBoost generalizes better with limited data.
- Higher timeframe features: Adding 5-min and 15-min indicators diluted 1-min signal quality.
- Sentiment from Twitter: NIFTY doesn’t care about retail tweets. Institutional flow matters more.
Next steps
- Add 20-depth market data from Dhan’s new API
- Reinforcement learning for exit timing — current ATR stop is too simple
- Multi-instrument correlation — BANKNIFTY often leads NIFTY by 2-3 minutes
- Live paper trading on Dhan before going live
TL;DR
| Component | Tool | Cost |
|---|---|---|
| Data | Dhan API | Free |
| ML | XGBoost | Open source |
| Backend | Flask | Open source |
| Dashboard | Next.js | Open source |
| Hosting | Cloudflare Tunnel | Free |
Result: 67% win rate, 2x Sharpe, zero infrastructure cost.
Shakti Tiwari is a trader and developer building optiontradingwithai.in. He co-directs CodeVisser and authored books on trading psychology. Find him on Dev.to as @shaktitiwari715-ai.
Top comments (0)