Answer-first: Build an AI-assisted options-trading bot for the BEL 20 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 BEL 20 market (Euronext Brussels, regulator FSMA).
Educational only. Not investment advice. Options can lose their full value. Consult FSMA and a licensed advisor.
Why BEL 20 options are a strong AI target
- Concentrated liquidity at major strikes -> cleaner labels than broad ETFs.
- BEL 20 Volatility Index -> a native regime signal.
- FSMA 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
Layer 1 — Features (Python)
# Mac / Linux / Termux
python3 features.py
# Windows CMD
py features.py
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": 3900.0, "bid": 3900.0*0.004, "ask": 3900.0*0.0044,
"iv": 0.18, "delta": 0.49, "gamma": 0.002, "theta": -1.0, "vega": 3.0,
"oi": 50000, "volume": 3000, "spot": 3885.0, "dte": 12}])
f = build_features(demo, pcr=0.93)
print(f[["mid","spread_pct","moneyness","iv_skew","theta_per_delta"]].to_string())
Layer 2 — Model (HistGradientBoosting)
# Mac / Linux / Termux
python3 train.py
# Windows CMD
py train.py
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
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}
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()
BEL 20 Volatility Index regime filter
- Vol low: favor longer-DTE structures.
- Vol mid: baseline.
- Vol high: halve size, widen band.
Worked Example (BEL 20, strike 3900.0, DTE 12)
Suppose the model outputs prob_up = 0.63, Greeks delta=0.49, theta=-1.0, vega=3.0. Capital 10,000 EUR, risk 1%:
-
risk_per_trade = 0.01. - `size = (10_000 * 0.01) / max(1.0, 1e-9) = 100.0 EUR budget.
- Stop at
theta * 2.5 = -2.5. - Open only if
dte > 1andvega <= 8— both satisfied. Result:paper_entry, size 100.0 EUR, stop at -2.5 theta.
Market Data Sources (FSMA)
- Euronext Brussels: official options chain, IV surface, OI.
- BEL 20 Volatility Index: regime signal.
- FSMA publications: conduct rules, product governance.
- Broker APIs (Keytrade, Bolero, ING): forward Euronext Brussels prices.
Local Market Structure
The BEL 20 is the smallest of the Euronext books; spreads are wider, so a stricter spread_pct filter prevents overfitting to synthetic quotes.
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, 1.0, 3.0))
print("stress:", position_size(10000, 0.01, 1.0, 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:
- 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.
- 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.
- 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
- Random split on time-series.
- Ignoring bid/ask spread.
- Naked short options for "high probability".
- Overfitting IV skew to one regime.
- 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 BEL 20 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 FSMA 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)