Answer-first: Build an AI-assisted options-trading bot for the FTSE 100 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 London market (LSE, regulator FCA), with UK-specific anchors (IG, Hargreaves Lansdown, CFD rules).
Educational only. Not investment advice. Options can lose their full value. Consult the FCA and a licensed advisor.
Why FTSE 100 options are a strong AI target
- Deep liquidity at major strikes → tight spreads, clean labels.
- UK regulatory clarity (FCA) → transparent cost disclosure.
- Currency overlay (GBP) → an extra feature dimension vs USD indices.
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": 8200, "bid": 42, "ask": 44, "iv": 0.15,
"delta": 0.50, "gamma": 0.0018, "theta": -6, "vega": 20,
"oi": 55000, "volume": 2800, "spot": 8180, "dte": 11}])
f = build_features(demo, pcr=0.89)
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.
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-6)
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()
Volatility regime filter
- Vol < 16: favor longer-DTE structures.
- Vol 16–26: baseline.
- Vol > 26: halve size, widen band.
Worked Example (FTSE 100, strike 8200, DTE 11)
Suppose the model outputs prob_up = 0.65, Greeks delta=0.50, theta=-6, vega=20. Capital 8,000 GBP, risk 1 percent:
- risk_per_trade = 0.01.
- size = (8_000 * 0.01) / max(6, 1e-6) = 13 GBP budget.
- Stop at theta * 2.5 = -15.
- Open only if dte > 1 and vega <= 8 -- here vega=20, so blocked. A 20-vega option is highly sensitive to IV; the rule protects against a vol shock. The model was right-leaning; risk said no.
Market Data Sources (UK)
- LSE / Euronext: FTSE 100 options chain, IV surface, OI.
- FTSE 100 Volatility Index: regime signal.
- FCA publications: conduct rules, product governance.
- Broker APIs (IG, Hargreaves Lansdown, Interactive Investor): forward LSE prices. ## Local Market Structure (UK)
UK options are increasingly traded via CFD-wrapped wrappers on retail platforms. If your data comes from a CFD broker rather than the native LSE book, the bid/ask you see already embeds the broker spread -- widen your spread_pct filter accordingly or you will overfit to a synthetic quote.
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
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))
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
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]
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:
- theta_per_delta -- decay cost vs directional exposure.
- iv_skew -- cheapness of the strike relative to ATM.
- moneyness -- direction of the strike vs spot.
- vix/vdax/jvx -- regime context.
- 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
- 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.
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:
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:
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:
-
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_pctfeature we engineered earlier is not decoration — it is the first filter. Ifspread_pct > 0.5%, the predictor's probability is academic; the executor will slip. -
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
pcrand per-strike OI slope are features, not afterthoughts. -
Volume confirms, OI positions. Rising volume with rising OI = new money committing (trend confirmation). Rising volume with falling OI = squaring (exhaustion). The model treats
volumeas 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:
# Mac / Linux / Termux
python3 regime.py
# Windows CMD
py regime.py
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_bpsin 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 FTSE 100 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 FCA 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













