Neural Networks Part 3: Using Neural Networks to Detect Mispriced Option Spreads

In Part 2, we trained a neural network to price options using the Cox-Ross-Rubinstein (CRR) model. It learned to predict the fair value of an option using inputs like strike, time to expiry, interest rate, and volatility.

Now, we’ll use that trained model to do something powerful:
Scan live option chains and identify mispriced vertical call spreads — a core strategy for directional or volatility-based options trading.

This is where the theory becomes practice.

A link to this notebook and the other notebooks can be found here on GitHub


What We’re Building

We’ll use real option chain data and our neural network to:

  1. Fetch calls for a symbol (e.g. PLTR)
  2. Predict the model price for each option using the trained neural net
  3. Construct all possible vertical call spreads
  4. Compare the market spread vs the model spread
  5. Flag spreads with significant mispricing

Our neural net becomes a custom pricing engine — one that can flag trading opportunities based on model-vs-market discrepancies.


Step 1: Pull the Option Chain

We use Alpaca’s broker API to get the latest option quotes. We filter down to call options and compute the mid price (average of bid and ask) for each.


Step 2: Calculate Time to Expiration

Since option values depend heavily on time, we compute T (in years) for each row. We align everything to U.S. market time (Eastern).


Step 3: Predict Model Prices Using Your Trained NN

We load the trained model and predict each call option’s fair value using our NN.

from datetime import datetime
import pandas as pd
import numpy as np
import pytz

# Pull option chain and filter calls
symbol = ticker_symbol  # set this variable earlier, e.g., "PLTR" or "AAPL"
option_chain_raw = fetch_option_chain(symbol, option_type="call", limit=250)
df_options = option_chain_to_df(option_chain_raw)
df_calls_live = df_calls(df_options)

# Clean and prepare
df_calls_live.dropna(subset=["implied_volatility", "bid_price", "ask_price", "expiration_date"], inplace=True)
df_calls_live["mid_price"] = (df_calls_live["bid_price"] + df_calls_live["ask_price"]) / 2

# Convert expiration_date to timezone-aware EST datetime
eastern = pytz.timezone("US/Eastern")
df_calls_live["expiration_date"] = pd.to_datetime(df_calls_live["expiration_date"], format="%y%m%d").dt.tz_localize(eastern)

# Get current time in EST
today = pd.Timestamp.now(tz="US/Eastern")

# Calculate T (time to expiration in years)
df_calls_live["T"] = (df_calls_live["expiration_date"] - today).dt.days / 365

# Define model price prediction using row-specific T
def predict_nn_price(row, S, r):
    inputs = np.array([[S, row['strike_price'], row['T'], r, row['implied_volatility']]])
    inputs_scaled = scaler.transform(inputs)
    return model.predict(inputs_scaled)[0]

# Fetch latest price and apply predictions
S = fetch_latest_underlying_price(ticker_symbol)
r = 0.05
df_calls_live["model_price"] = df_calls_live.apply(lambda row: predict_nn_price(row, S, r), axis=1)

# Preview
df_calls_live.head()

Step 4: Build All Vertical Call Spreads

We iterate through all combinations of call options with different strikes (but same expiry) and calculate:

  • Market spread value = difference in mid prices
  • Model spread value = difference in model prices
  • Mispricing = market value – model value
spreads = []

# Add DTE if not already in DataFrame
df_calls_live["DTE"] = (df_calls_live["T"] * 365).round().astype(int)

# Sort calls by strike
calls = df_calls_live.sort_values("strike_price").reset_index(drop=True)

# Construct all vertical call spreads
for i in range(len(calls)):
    for j in range(i + 1, len(calls)):
        c1 = calls.iloc[i]
        c2 = calls.iloc[j]
        
        spread_width = c2["strike_price"] - c1["strike_price"]
        market_spread = c1["mid_price"] - c2["mid_price"]
        model_spread = c1["model_price"] - c2["model_price"]
        mispricing = market_spread - model_spread
        
        spreads.append({
            "long_strike": c1["strike_price"],
            "short_strike": c2["strike_price"],
            "spread_width": spread_width,
            "market_spread_value": market_spread,
            "model_spread_value": model_spread,
            "mispricing": mispricing,
            "DTE": c1["DTE"]  # assume same expiry
        })

# Convert to DataFrame
df_spreads = pd.DataFrame(spreads)

# Add calculated metrics
df_opportunities["mispricing_pct"] = df_opportunities["mispricing"] / df_opportunities["model_spread_value"]
df_opportunities["max_profit"] = df_opportunities["spread_width"] - df_opportunities["market_spread_value"]
df_opportunities["max_loss"] = df_opportunities["market_spread_value"]

# Preview
df_opportunities.head()

Step 5: Filter for Actionable Trades

We now flag verticals that look significantly mispriced — say, $0.10+ — and fall in a reasonable DTE range.

threshold = 0.10

df_opportunities = df_spreads[
    (np.abs(df_spreads["mispricing"]) > threshold) &
    (df_spreads["DTE"] >= 10) & (df_spreads["DTE"] <= 60) &
    (df_spreads["spread_width"] >= 1) &
    (df_spreads["market_spread_value"] > 0)
].copy()

df_opportunities.sort_values("mispricing", ascending=False, inplace=True)
df_opportunities.head()

Example Output

LongShortSpread WidthMarket ValueModel ValueMispricingDTE
20.533.012.57.371.99+5.3817
20.023.53.53.306.95−3.6524
  • Positive mispricing → market is overpaying → consider selling the spread
  • Negative mispricing → market is underpaying → consider buying the spread

Key Takeaways

  • Neural nets trained on CRR prices can be used to screen live options data
  • You can compute model vs market spreads instantly and spot edge
  • This approach is scalable across tickers and DTE ranges

A link to this notebook can be found here on GitHub

Sharing

Related Articles

  • All Post
  • Articles
  • Blog Post
  • General Business Automation
  • Portfolio
  • Stock Market & Finance