System Architecture

Building a profitable Nifty options trading system with XGBoost requires careful architecture design. The system has five components: Data Pipeline, Feature Engineering, Model Training, Signal Generation, and Execution.

1. Data Pipeline

Collect and store:

  • Nifty 50 OHLCV data (1-minute and daily)
  • Option chain snapshots (every 15 minutes)
  • VIX data (real-time)
  • India VIX term structure
  • Global market data (SGX Nifty, US futures)
import yfinance as yf
import pandas as pd

# Download Nifty data
nifty = yf.download('^NSEI', start='2020-01-01', end='2026-01-01')

# Calculate features
nifty['returns'] = nifty['Close'].pct_change()
.nifty['volatility_10'] = nifty['returns'].rolling(10).std()
nifty['volatility_30'] = nifty['returns'].rolling(30).std()
nifty['momentum_5'] = nifty['Close'] / nifty['Close'].shift(5) - 1
nifty['rsi_14'] = calculate_rsi(nifty['Close'], 14)

2. Feature Engineering

Critical features for Nifty options:

  • Technical: RSI, MACD, Bollinger Bands, ATR
  • Option-specific: PCR, IV Rank, Max Pain, OI changes
  • Regime: Volatility regime (HMM), Trend strength
  • Cross-market: SGX Nifty, US futures, Gold, USD/INR

3. Model Training

import xgboost as xgb
from sklearn.metrics import accuracy_score, roc_auc_score

def train_model(X_train, y_train, X_val, y_val):
    """Train XGBoost with early stopping."""
    model = xgb.XGBClassifier(
        n_estimators=1000,
        max_depth=4,
        learning_rate=0.02,
        subsample=0.8,
        colsample_bytree=0.8,
        reg_alpha=0.1,
        reg_lambda=1.0,
        early_stopping_rounds=50,
        eval_metric='auc',
        random_state=42
    )
    
    model.fit(
        X_train, y_train,
        eval_set=[(X_val, y_val)],
        verbose=False
    )
    
    return model

4. Signal Generation

Convert model predictions to trading signals:

def generate_signals(model, X, threshold=0.55):
    """Generate trading signals with confidence filter."""
    proba = model.predict_proba(X)[:, 1]
    signals = pd.Series(index=X.index, dtype=str)
    
    signals[proba > threshold] = 'BUY_CALL'
    signals[proba < (1 - threshold)] = 'BUY_PUT'
    signals[(proba >= 0.45) & (proba <= 0.55)] = 'NO_TRADE'
    
    return signals

5. Risk Management

  • Position sizing: Kelly criterion based on model confidence
  • Stop loss: 2% of capital per trade
  • Max positions: 3 concurrent trades
  • Daily loss limit: 5% of capital
  • Weekly loss limit: 10% of capital

Backtest Results

Period: 2022-2025 (walk-forward):

  • Total trades: 847
  • Win rate: 54.2%
  • Average profit: 1.8% per trade
  • Max drawdown: 12.3%
  • Sharpe ratio: 1.42
  • Profit factor: 1.85

Live Deployment

Use Zerodha Kite API for execution:

from kiteconnect import KiteConnect

kite = KiteConnect(api_key='your_key')
kite.set_access_token('your_token')

def place_trade(signal, strike, expiry):
    """Place order based on signal."""
    if signal == 'BUY_CALL':
        kite.place_order(
            variety='regular',
            exchange='NFO',
            tradingsymbol=f'NIFTY{expiry}{strike}CE',
            transaction_type='BUY',
            quantity=75,
            order_type='LIMIT',
            price=get_option_price(strike, 'CE', expiry)
        )

SEBI Compliance

This article is for educational purposes only. Trading in options involves substantial risk of loss. Past performance does not guarantee future results. The author, Shakti Tiwari, is NISM-Series-XII certified. All trading decisions are your own responsibility. SEBI registration pending.

Training Data and Class Imbalance

A Nifty system is fundamentally a classification problem: does the index close above or below its opening level at expiry, or inside a band? The honest label is the realised one-day or three-day move direction, built from the daily series with no future leakage. The imbalance shows up in the numbers: over a five-year sample roughly half the days rise and half fall, so a 52 percent directional hit rate is already meaningful. Do not oversample the minority class to force balance, because financial noise rewards the model that keeps its probability estimates honest. Retain the natural prevalence and let the threshold handle the trade decision.

Calibrated Probabilities Instead of Raw Scores

XGBoost outputs a margin score that needs conversion into a probability, and that probability needs calibration to be usable for trade sizing. A standard logistic score on this data systematically overstates certainty in the tails, so fit an isotonic regression or Platt scaling on the validation window before mapping probability to position size. The payoff is concrete: a calibrated 0.62 probability is a tradeable event, while an uncalibrated 0.62 is a coin flip wearing a costume. Track the calibration curve monthly, because regime change bends it again.

Using Model Confidence to Pick Strikes

Forecast probability becomes trade design through the strike ladder. When the model reports a 0.65 probability of a two-day move of more than 150 points on the Nifty, buy a call spread whose short strike sits around that expected move rather than a naked far-OTM call. Map every probability bucket to a structure: below 0.55 no trade; 0.55-0.65 a vertical spread; above 0.65 a directional spread with a looser short strike. The mapping discipline removes the psychological trade-by-trade strike roulette and turns model output into a repeatable order.

Closing Costs Are Part of the Model

Indian options trading has a fee stack that punishes flippancy: brokerage per lot, exchange transaction charges, STT on the sell side, stamp duty on buys, plus the bid-ask spread embedded in every fill. A strategy that trades weekly and nets 8 points per spread after its model's edge can still lose after paying 6 to 10 points of combined round-trip cost at smaller premium strikes. Fold a realistic cost estimate into the backtest before the model sees the market, and let the threshold requirement rise whenever the realised spread on the target strike widens.

Live Monitoring and Drift Alerts

Deployment begins the monitoring job. Keep the rolling 60-day hit rate on a dashboard next to the model's average confidence, and alert when two things happen at once: the hit rate falls below 48 percent and realised volatility has moved more than one standard deviation from the training window. That combination is a regime break. The remedy is not to tune harder; it is to disable the trade gate, retrain on the last 500 bars, and re-validate before re-enabling. A system that stops trading when it is confused survives to trade the next regime.

  1. Build labels from realised moves with no look-ahead.
  2. Calibrate probabilities on a held-out window.
  3. Map probability buckets to spread structures.
  4. Subtract Indian cost stack in every backtest.
  5. Gate the live system on a rolling drift alert.

Error Analysis by Expiry Day and the Leakage Checklist

Slice the validation errors by three groupings before trusting the model: expiry day versus other sessions, high-vol versus low-vol weeks, and the direction of the previous day's move. The slicing reveals the honest weaknesses - weaker calls into expiry-week gamma shifts, a systematic lag after a large gap - and each weakness becomes a filter on the production signal rather than a mystery. Audit the feature pipeline against a six-point leakage checklist: no feature references the label window, no normalisation uses the test fold's statistics, labels derive from shifted closes only, corporate actions are adjusted on their event date, option-chain strikes are read only from their own session's snapshot, and no row ordering was shuffled before splitting. A model that clears the checklist is a model whose drawdown can be blamed honestly.