DEV Community

shakti tiwari
shakti tiwari

Posted on • Originally published at optiontradingwithai.in

AI Options Trading on the AEX

AI Options Trading on the AEX

Answer-first: Build an AI-assisted options-trading bot for the AEX 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 AEX market (Euronext Amsterdam, regulator AFM).

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

Why AEX options are a strong AI target

  • Concentrated liquidity at major strikes -> cleaner labels than broad ETFs.
  • AEX Volatility Index -> a native regime signal.
  • AFM clarity -> 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": 780.0, "bid": 780.0*0.004, "ask": 780.0*0.0044,
        "iv": 0.18, "delta": 0.49, "gamma": 0.002, "theta": -0.9, "vega": 3.2,
        "oi": 50000, "volume": 3000, "spot": 776.0, "dte": 11}])
    f = build_features(demo, pcr=0.9)
    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 — random leaks the future and inflates AUC.

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

AEX Volatility Index regime filter

  • Vol low: favor longer-DTE structures.
  • Vol mid: baseline.
  • Vol high: halve size, widen band.

Worked Example (AEX, strike 780.0, DTE 11)

Suppose the model outputs prob_up = 0.63, Greeks delta=0.49, theta=-0.9, vega=3.2. Capital 10,000 EUR, risk 1%:

  1. risk_per_trade = 0.01.
  2. `size = (10_000 * 0.01) / max(0.9, 1e-9) = 111.11 EUR budget.
  3. Stop at theta * 2.5 = -2.25.
  4. Open only if dte > 1 and vega <= 8 — both satisfied. Result: paper_entry, size 111.11 EUR, stop at -2.25 theta.

Market Data Sources (AFM)

  • Euronext Amsterdam: official options chain, IV surface, OI.
  • AEX Volatility Index: regime signal.
  • AFM publications: conduct rules, product governance.
  • Broker APIs (Binck, DEGIRO, ING): forward Euronext Amsterdam prices.

Local Market Structure

The AEX is part of the single Euronext book shared with Paris and Brussels, so dedupe cross-venue snapshots or the same contract double-counts.

Position Sizing Calculator (runnable)

`python
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-9)
return round(lots, 2)

if name == "main":
print("calm :", position_size(10000, 0.01, 0.9, 3.0))
print("stress:", position_size(10000, 0.01, 0.9, 24.0))
`

The stress case shows the calculator automatically cuts exposure when vega blows past the cap — exactly the behaviour the rules engine enforces.

Strategy Variations

  • Vertical spread: caps max loss, favourite in high-vega regimes.
  • Calendar spread: profits from term-structure slope.
  • Iron condor: collects theta, watch gamma at short strikes.
  • Naked long call/put: highest convex payoff, only with prob_up 0.70-0.80 and dte > 5.

Walk-Forward Evaluation

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

Feature Importance

On options data the ranking is usually: (1) theta_per_delta, (2) iv_skew, (3) moneyness, (4) vol-index, (5) pcr. If your model ranks oi or volume first, suspect leakage — those are post-hoc liquidity, not predictive. Drop them and re-check.

Glossary

  • Delta: directional exposure per 1 unit of underlying.
  • Gamma: rate of change of delta; high gamma = convex risk.
  • Theta: daily time decay; cost of holding.
  • Vega: sensitivity to implied volatility; dominant risk in stress.
  • IV skew: strike IV minus ATM IV; cheapness signal.
  • PCR: put-call ratio; sentiment extreme indicator.
  • DTE: days to expiry; hard stop before assignment.
  • Moneyness: strike / spot - 1; negative = ITM, positive = OTM.

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.

Monitoring and Alerting (production hygiene)

A model that is not monitored decays silently. Wire three alerts:

  1. AUC drift: retrain daily; if rolling 5-day AUC drops more than 3% from the 30-day mean, halt paper entries and flag for review.
  2. Fill divergence: compare expected mid (from your predicted probability band) to actual fill; a persistent gap means the broker quote is wider than your assumption — tighten spread_pct.
  3. Vol-regime flip: if the local volatility index moves more than 1.5 standard deviations in a session, force the risk layer into the high-vol branch regardless of model output.

These three alerts catch the failures that a backtest, by definition, cannot — because the backtest already assumed the regime you are now living in.

Regime-Switching Model (optional upgrade)

Instead of a single classifier, train two: one on low-vol windows, one on high-vol windows, and route each live row to the matching model by the current volatility-index z-score. This typically adds 1-3 AUC points over a monolithic model because the IV-skew feature behaves oppositely in the two regimes. The routing code is trivial:

python
def route(model_low, model_high, row, vol_z):
m = model_high if vol_z > 0 else model_low
return m.predict_proba(row)[:, 1]

Keep the rules engine identical — the routing only changes which probability the engine receives, never the hard limits.

Why This Beats a Black-Box Net

A gradient-boosting model on ten transparent features is explainable: you can show a regulator or a client exactly which input moved the decision. A deep net cannot. For retail options — where a single bad fill ends the week — explainability is not a nicety, it is the difference between surviving a regime change and blowing up inside it. Build the boring model, enforce the boring risk layer, and let compounding do the rest.

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.

The Full Production Pipeline (Data Engine -> Predictor -> Filter)

A published article often shows only the model and backtest. The production system that actually runs has four stages between raw market data and a trade:

`plaintext

  1. DATA ENGINE fetch chain + IV + PCR + vol-index every N seconds
  2. FEATURE ENGINE build_features() -> clean, dedup, label
  3. PREDICTOR gradient-boosting model -> prob_up per strike
  4. FILTER Greeks + regime + prob-band rules -> allow/block
  5. EXECUTOR paper or live entry sized by position_size() `

1. Data Engine

Connects to the broker/exchange feed (NSE, Eurex, OSE, Euronext, LSE, TMX, ASX, HKEX, SGX, KRX, etc.) and snapshots the full options chain on a timer. It must:

  • Dedupe cross-venue snapshots (Euronext shares one book).
  • Cap snapshot latency under the decision window.
  • Survive a feed gap without feeding stale mid quotes to the model.

2. Feature Engine

Runs build_features() on the raw snapshot: mid, spread_pct, moneyness, iv_skew, pcr, theta_per_delta. This is where bad data dies — a strike with no OI or a synthetic CFD quote is dropped before the model sees it.

3. Predictor

The trained HistGradientBoostingClassifier outputs prob_up per strike. It is stateless at inference time — load once, predict many.

4. Filter (the part most beginners skip)

The predictor is NOT the trade. The filter is a hard rules layer:

python
def filter(prob_up, greeks, vol_z, max_capital):
if not (0.58 <= prob_up <= 0.80):
return {}
if greeks["dte"] <= 1:
return {}
if abs(greeks["vega"]) > 8.0:
return {}
if vol_z > 2.0: # vol spike -> shrink
max_capital *= 0.5
size = (max_capital * 0.01) / max(greeks["theta"], 1e-9)
return {"action": "paper_entry", "size": round(size, 2)}

The filter is what makes the system survive a regime the model never saw in training.

5. Executor

Turns the allowed signal into a sized order. Paper first (log every fill), then live only after the broker review. Never skip stage 4.

This five-stage split is why a 1500-word model section is not the whole product — the data engine and the filter carry as much weight as the predictor.

Market Microstructure & Liquidity (why it matters for the model)

A signal is only as good as the liquidity it trades into. Three microstructure facts the model must respect:

  1. Bid-ask spread eats thin edges. An ATM option with a 0.3% spread needs the signal to clear more than 0.3% just to break even. The spread_pct feature we engineered earlier is not decoration — it is the first filter. If spread_pct > 0.5%, the predictor's probability is academic; the executor will slip.
  2. Open Interest build-up defines support/resistance. When OI piles at a strike, that strike acts as a magnet or wall at expiry. A model that ignores OI concentration misprices the pinning effect. This is why pcr and per-strike OI slope are features, not afterthoughts.
  3. Volume confirms, OI positions. Rising volume with rising OI = new money committing (trend confirmation). Rising volume with falling OI = squaring (exhaustion). The model treats volume as a confirmation flag, never as a standalone predictor, because volume without OI context is noise.

Practical checklist before trusting any entry: spread tight, OI slope sensible vs the signal direction, and volume not in exhaustion pattern.

Volatility Regime Detection (real code)

Markets are not stationary. A model trained in calm IV behaves badly in a vol spike. Detect regime from the vol index and switch logic:

`python

Mac / Linux / Termux

python3 regime.py

Windows CMD

py regime.py
`

`python
def regime_state(vix, vix_ma20):
z = (vix - vix_ma20) / (vix_ma20 + 1e-9)
if z > 2.0:
return "CRASH", 0.5 # halve size
if z > 1.0:
return "STRESS", 0.75 # shrink size
if z < -1.0:
return "CALM", 1.0 # full size
return "NORMAL", 1.0

def size_with_regime(base_capital, z, max_capital):
_, mult = regime_state(vix=z, vix_ma20=1.0)
return (max_capital * 0.01 * mult) / max(base_capital, 1e-9)
`

The CRASH state cuts size to 50% — this single rule is what keeps a strategy alive across the 2020-style gaps that destroy naive bots. The model's probability is unchanged; only the executor's capital adapts.

Execution & Broker Reality

Backtest assumes fills at mid. Live fills at ask (buy) / bid (sell), plus brokerage and STT. Three realities:

  • Brokerage + taxes: per-lot flat fee plus exchange charges. A round-trip on a cheap option can cost 0.5-1% — model this as fees_bps in backtest, not zero.
  • Slippage: in fast markets the quoted mid moves between signal and fill. Cap position size so slippage stays under the edge.
  • Margin: short options need margin blocks; long options need premium. The position_size() function already sizes from premium risk, so a long option's max loss is known upfront.

Never let a backtest show profit that a live account cannot realize after fees. If the net-after-fees AUC-era return is negative, the signal is not an edge — it is a fee generator for the broker.

A Realistic Weekly Routine

Consistency beats bursts. A workable week for this system:

  • Monday: pull last week's chain CSV, retrain if drift alert fired, review regime state.
  • Tuesday–Thursday: run the paper loop during market hours; log every entry/exit with the model's probability and the filter's decision.
  • Friday: if expiry week, tighten DTE limits; review realized vs predicted.
  • Weekend: read one regulatory update; check if broker margin rules changed.

This is not a get-rich loop. It is a measurement loop. After 8-12 weeks of honest paper logs you will know your true edge — and that number, not a backtest chart, is what you size against.

FAQ

Q1. Do I need a neural network for AEX 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 AFM 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)