DEV Community

shakti tiwari
shakti tiwari

Posted on • Originally published at optiontradingwithai.in

AI Options Trading on the ASX 200 — Architecture, Greeks, and a pandas Backtest

AI options trading ASX 200

Answer-first: Build an AI-assisted options-trading bot for the ASX 200 by combining a feature pipeline (options chain, implied volatility, PCR), a gradient-boosting classifier for directional probability, and a backtest that enforces Greeks-based risk limits. The model emits a probability; a rules engine decides whether to act. Here is a runnable Python scaffold.

Written for retail quants targeting the Sydney market (ASX, regulator ASIC), with Australia-specific anchors (CommSec, SelfWealth, AUD overlay).

Educational only. Not investment advice. Options can lose their full value. Consult ASIC and a licensed advisor.

Why ASX 200 options are a strong AI target

  • Concentrated liquidity in index leaders → cleaner labels than broad ETFs.
  • AUD/USD overlay → extra feature dimension.
  • Australian regulatory clarity (ASIC) → transparent cost disclosure.

Architecture (three layers)

1. Data/Feature Pipeline → chain, IV, PCR
2. Model (gradient boosting) → P(direction | features)
3. Rules + Greeks Engine → sizing, stop, DTE limit
Enter fullscreen mode Exit fullscreen mode

Layer 1 — Features (Python)

# Mac / Linux / Termux
python3 features.py
# Windows CMD
py features.py
Enter fullscreen mode Exit fullscreen mode
import pandas as pd, numpy as np

def build_features(chain: pd.DataFrame, pcr: float) -> pd.DataFrame:
    df = chain.copy()
    df["mid"] = (df["bid"] + df["ask"]) / 2.0
    df["spread_pct"] = (df["ask"] - df["bid"]) / df["mid"].clip(lower=1e-9)
    df["moneyness"] = df["strike"] / df["spot"] - 1.0
    atm_iv = df.loc[(df["moneyness"].abs()).idxmin(), "iv"]
    df["iv_skew"] = df["iv"] - atm_iv
    df["pcr"] = pcr
    df["theta_per_delta"] = df["theta"] / df["delta"].clip(lower=1e-9)
    return df

if __name__ == "__main__":
    demo = pd.DataFrame([{"strike": 7800, "bid": 22, "ask": 23, "iv": 0.14,
        "delta": 0.50, "gamma": 0.0016, "theta": -3, "vega": 12,
        "oi": 42000, "volume": 2100, "spot": 7780, "dte": 14}])
    f = build_features(demo, pcr=0.90)
    print(f[["mid","spread_pct","moneyness","iv_skew","theta_per_delta"]].to_string())
Enter fullscreen mode Exit fullscreen mode

Layer 2 — Model (HistGradientBoosting)

# Mac / Linux / Termux
python3 train.py
# Windows CMD
py train.py
Enter fullscreen mode Exit fullscreen mode
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.model_selection import TimeSeriesSplit, roc_auc_score
import pandas as pd

FEATURES = ["spread_pct","moneyness","iv_skew","pcr",
            "theta_per_delta","gamma","vega","dte","oi","volume"]

def train(X: pd.DataFrame, y: pd.Series):
    tscv = TimeSeriesSplit(n_splits=5)
    model = HistGradientBoostingClassifier(max_depth=4, learning_rate=0.05, max_iter=300)
    for tr, te in tscv.split(X):
        model.fit(X.iloc[tr], y.iloc[tr])
        pred = model.predict_proba(X.iloc[te])[:, 1]
        print("fold AUC:", round(roc_auc_score(y.iloc[te], pred), 3))
    model.fit(X, y)
    return model
Enter fullscreen mode Exit fullscreen mode

Time-series split, never random.

Layer 3 — Greeks rules engine

def decide(prob_up, greeks, max_capital, risk_per_trade=0.01):
    if not (0.58 <= prob_up <= 0.80):
        return {}
    if greeks["dte"] <= 1:
        return {}
    if abs(greeks["vega"]) > 8.0:
        return {}
    size = (max_capital * risk_per_trade) / max(greeks["theta"], 1e-9)
    return {"action": "paper_entry", "size": round(size, 2),
            "stop_theta": greeks["theta"] * 2.5}
Enter fullscreen mode Exit fullscreen mode

Backtest (pandas vectorized)

def backtest(signals: pd.DataFrame, fees_bps=2.0) -> float:
    s = signals.copy()
    s["position"] = ((s["prob_up"] >= 0.60) & (s["dte"] > 1)).astype(int)
    s["pnl"] = s["position"] * (s["delta"] * s["spot_ret"] * 100
                                - s["theta"] + s["prob_up"] - 0.5)
    s["pnl"] -= (s["position"] * fees_bps / 10000.0)
    return s["pnl"].sum()
Enter fullscreen mode Exit fullscreen mode

Volatility regime filter

  • Vol < 15: favor longer-DTE structures.
  • Vol 15–25: baseline.
  • Vol > 25: halve size, widen band.

Worked Example (ASX 200, strike 7800, DTE 14)

Suppose the model outputs prob_up = 0.64, Greeks delta=0.50, theta=-3, vega=12. Capital 15,000 AUD, risk 1 percent:

  1. risk_per_trade = 0.01.
  2. size = (15_000 * 0.01) / max(3, 1e-6) = 50 AUD budget.
  3. Stop at theta * 2.5 = -7.5.
  4. Open only if dte > 1 and vega <= 8 -- here vega=12, so blocked. The rule shields you from an IV move that would dominate the directional edge. Model leaned long; risk said no.

Market Data Sources (Australia)

  • ASX: official options chain, IV surface, OI.
  • S&P/ASX 200 Volatility Index (AVIX): regime signal.
  • ASIC publications: conduct rules, product governance.
  • Broker APIs (CommSec, SelfWealth, Interactive Brokers): forward ASX prices. ## Local Market Structure (Australia)

ASX options settle physically and the index is dominated by a few banks and miners, so single-name news in those constituents drives correlated IV moves. Your IV-skew feature should down-weight the top-3 weight names to avoid a concentration bias in the training labels.

Position Sizing Calculator (runnable)

A fixed 1 percent rule is a start, but sizing should adapt to the Greek budget. Here is a calculator that reduces size when vega is elevated:

# Mac / Linux / Termux
python3 sizecalc.py
# Windows CMD
py sizecalc.py
Enter fullscreen mode Exit fullscreen mode
def position_size(capital, risk_pct, theta, vega, vega_cap=8.0):
    base = capital * risk_pct
    if abs(vega) > vega_cap:
        base *= vega_cap / abs(vega)
    lots = base / max(abs(theta), 1e-6)
    return round(lots, 2)

if __name__ == "__main__":
    print("calm  :", position_size(10000, 0.01, 1.0, 3.0))
    print("stress:", position_size(10000, 0.01, 1.0, 24.0))
Enter fullscreen mode Exit fullscreen mode

The stress case shows the calculator automatically cuts exposure to a third when vega triples past the cap -- exactly the behaviour the rules engine enforces, now made explicit and tunable.

Strategy Variations

The same pipeline supports several structures without rewriting the model:

  • Vertical spread: long + short same-expiry different-strike -- caps max loss, favourite in high-vega regimes.
  • Calendar spread: same-strike different-expiry -- profits from term-structure slope (our VDAX/term-structure feature).
  • Iron condor: two verticals -- collects theta, but watch gamma at the short strikes.
  • Naked long call/put: highest convex payoff, but theta bleeds daily; only with prob_up in the 0.70-0.80 band and dte > 5.

Each variation just changes the feature label and the Greeks fed to the rules engine; the model and backtest stay identical.

Walk-Forward Evaluation (not just train/test)

A single TimeSeriesSplit is honest, but a production system needs walk-forward: retrain on a rolling window, test on the next, slide forward. This catches the "model decayed" failure that static splits hide.

# Mac / Linux / Termux
python3 walkforward.py
# Windows CMD
py walkforward.py
Enter fullscreen mode Exit fullscreen mode
from sklearn.model_selection import TimeSeriesSplit
import pandas as pd, numpy as np

def walk_forward(X, y, n_splits=10, train_size=300, test_size=60):
    aucs = []
    for start in range(0, len(X) - train_size - test_size, test_size):
        tr = slice(start, start + train_size)
        te = slice(start + train_size, start + train_size + test_size)
        # train + eval placeholder; plug your model here
        aucs.append(0.0)  # replace with real roc_auc_score
    return np.mean(aucs)

# Real use: fit HistGradientBoostingClassifier on X.iloc[tr], score on X.iloc[te]
Enter fullscreen mode Exit fullscreen mode

The point is the loop shape: never let the test window touch training data, and slide by exactly the test size so windows are contiguous and non-overlapping.

Feature Importance (what actually drives the signal)

After training, inspect which features the model leans on. On options data the ranking is usually:

  1. theta_per_delta -- decay cost vs directional exposure.
  2. iv_skew -- cheapness of the strike relative to ATM.
  3. moneyness -- direction of the strike vs spot.
  4. vix/vdax/jvx -- regime context.
  5. pcr -- sentiment extreme.

If your model ranks oi or volume first, suspect leakage: those are post-hoc liquidity, not predictive of next-window mid move. Drop them from features and re-check.

Deployment Checklist

Before any paper trade:

  • [ ] TimeSeriesSplit AUC printed, not random-split.
  • [ ] Walk-forward mean AUC stable across windows.
  • [ ] Feature importance sane (no leakage features ranked top).
  • [ ] Rules engine hard limits active (dte, vega, prob band).
  • [ ] Backtest includes fees and theta accrual.
  • [ ] Position size calculator wired to the rules layer.
  • [ ] Canonical URL and disclaimers present in published version.

    Glossary (terms the model relies on)

  • Delta: directional exposure of the option per 1 unit of underlying move.

  • Gamma: rate of change of delta; high gamma = convex PnL, fast risk shift.

  • Theta: daily time decay; the cost you pay for holding.

  • Vega: sensitivity to implied-volatility moves; the dominant risk in stress.

  • IV skew: difference between a strike's IV and ATM IV; a cheapness signal.

  • PCR: put-call ratio; a sentiment extreme indicator when far from 1.0.

  • DTE: days to expiry; the hard stop before assignment/gamma risk.

  • Moneyness: strike divided by spot minus one; negative = ITM, positive = OTM.

Understanding these is what separates a backtest that looks good from one that survives live. The rules engine exists precisely because no single Greek is safe alone.

Common mistakes

  1. Random split on time-series.
  2. Ignoring bid/ask spread.
  3. Naked short options for "high probability".
  4. Overfitting IV skew to one regime.
  5. No position sizing.

Weekly routine

  • Mon: rebuild features, retrain if AUC drift > 3%.
  • Tue–Thu: paper-trade, log fills vs prediction.
  • Fri: review false positives, tighten rules.

FAQ

Q1. Do I need a neural network for ASX 200 options?
No. Gradient-boosting on well-built features typically matches or beats nets on tabular options data and is easier to audit.

Q2. Is this legal under ASIC rules?
Building and paper-trading your own model is legal. Live automation triggers broker review. Consult a compliance professional.

Q3. How much capital per trade?
≤1% of capital per trade, scaled by Greeks. Never risk what you can't lose.

Q4. Can I run this from a phone?
Yes. Pure Python/pandas runs on Termux or a Raspberry Pi.

Q5. Biggest edge — model or risk layer?
The risk layer. A mediocre model with strict Greek limits survives; a great model without them does not.

Footer

Shakti Tiwari — Options Trader, XGBoost Expert.
Books: Option Trading with AI (B0H9ZNTBPK) · The AI Opportunity (B0HBBFKDQF)
Site: optiontradingwithai.in · Free help: shaktitiwari715@gmail.com
Dev.to: @shaktitiwari · X: @shaktitiwari

Top comments (0)