Published on

Surface Porn

Authors
  • avatar
    Name
    Teddy Xinyuan Chen
    Twitter

I finally finished my library and web app for plotting multileg options positions -

Now let's explore the 3D surfaces of greeks with respect to time and spot price or volatility and spot price in this post!

JPM collar's delta. Interactive plots below!
Table of Contents

LEAPS Call - 700 DTE

Just 1 leg, 1 ATM call. With almost 2 years to expiration, delta moves slowly, with respect to the other 2 axis:

Full screen: https://g.teddysc.me/9680fa585d93d6517235fc29f77e79f5

You can drag it around and zoom in / pan.

The most intuitive 2nd order greek, gamma, being 2CS2\frac{\partial^2 C}{\partial S^2}, shows that delta's change to spot change can mostly be ignored:

Full screen: https://g.teddysc.me/fcbf75daccb223c18c8aacd73be93576

This chart helped me develop intuitive understanding for the 3rd order greeks, partial derivative of gamma to time (color) and spot price (speed).

Color is positive everywhere for long options, sign of speed changes at the center peak.

How does vol affect vega?

Shape of vega with vol axis is rarely seen in the wild:

Full screen: https://g.teddysc.me/f2ffa5a8940b38824cdbde392c9382c2

Have you seen anything with such shape irl?

This tells us that the higher the vol, the more sensitive non-near-the-money options are to vol change.

The slope along vol axis is vomma (2Cσ2\frac{\partial^2 C}{\partial \sigma^2}), and the slope along spot price axis is vanna (2CσS\frac{\partial^2 C}{\partial \sigma \partial S}).

It looks even more interesting if we raise the vol to 200%, like a piece of paper folded, a very smooth paper:

Full screen: https://g.teddysc.me/9cfb6c33757eaba7674f15a1cf3fbff5

Let's make it extreme, raise vol to 1000%:

Full screen: https://g.teddysc.me/be995368eda8baf496d718b4f9a1958b

Looks like vomma flipped sign to become negative after vol > 2 across strikes.

But option prices become indifferent to change in vol when it's even more extreme, at 20:

Full screen: https://g.teddysc.me/b45e2b486cb78d5cd17ca938c31faf48

A Simple Vertical Spread

The dynamic of the long and short leg changes near the "center" of the 2 legs, changing from a net long structure to a net short structure:

Delta:

Full screen: https://g.teddysc.me/02aadfa4f5ce72f899e54537861e16c4

Vega (very important to consider this when you try to hedge with vertical spread during market crash / a "vol event"):

When volatility spikes, like on 2/21/2025, your put debit spread will be hurt a lot even if spot is already below the short leg.

Full screen: https://g.teddysc.me/25b01891d1dba6cb8d3ebbe67ab10c4f

There are 2 gamma peaks near the strikes. I say "near" because the one leg is influenced by the other, although it can mostly be ignored:

Gamma:

Full screen: https://g.teddysc.me/e11eb0791b649fbcfa43bb292ee9eea2

If this chart remind you of a service like optionsdepth, you're not alone :)

How does volatility affect gamma?

It makes it less concentrated (still the vertical spread, but swapped time axis with vol):

Full screen: https://g.teddysc.me/f137a5520e571af6808351556da80fe4

This is a beautiful shape. You can tell that at expiration the peaks are not symmetric w.r.t. spot price, which you cannot tell with just your eyes if I use time axis.

Long call fly, gambler's favorite (or not)

I'm a fan of extremely narrow long flies, they're more like lottery ticket than 1/13 dtes. Lower win rate, but extreme payoff if it landed at the right spot (really low probability).

I guess it's better done on indexes than earnings play on $APP, one of the most ridiculous stocks I've seen.

Liquidity is not great on individual names for butterflies, and the quote fluctuate a lot.

I'm planning to build a implied distribution tool based on this idea: https://www.morganstanley.com/content/dam/msdotcom/en/assets/pdfs/Options_Probabilities_Exhibit_Link.pdf

Delta:

Full screen: https://g.teddysc.me/495f29200c312431b6274a40d3f42c19

Shape of vega with vol axis looks like a space ship:

Higher vol dampens gamma, making change of delta w.r.t. spot change smooth.

Full screen: https://g.teddysc.me/788f52b66baa280a185c503009ffd38e

Making it narrow 😈

Full screen: https://g.teddysc.me/e28bec55349338e80c80c4434dbf3ea8

As you can see, delta is basically 0 except near the center of the fly, which is consistent with my experience that a narrow fly is almost a bad idea because the structure's price barely react to spot change, and suffer from theta bleed, especially near the long strikes:

Full screen: https://g.teddysc.me/b60e54869d0c2960aff659a1350b4ef1

Volatility will keep the price of the narrow fly afloat

This can be observed pre-earning or pre-index-inclusion. At those times, delta is bloated by vol across the board, and a narrow fly is a cheap (in dollar terms) way to gain some directional exposure (other structures, like vertical spread, or long call or put, will be extremely expensive). But don't hold it across the events (or you're be subject to binary outcome).

The ATM straddle for APP (trading around 375) right before the Feb 2025 earning cost more than ATM straddle for SPX with the same expiration date, which is insane because SPX / APP ~= 6000/375. The dealers are charging extreme IV for option buyers. And to no one's surprise, at 6 pm that day it shot up to 500.

What about theta?

Beautiful asymmetry:

Full screen: https://g.teddysc.me/53471e3c7eb39ef6763713c5958c79f9

Calendars!

My app can't model calendars yet, will update later.

Put Ratio Spread (-2*100P, +110P)

Something like this: https://optionstrat.com/build/put-ratio-spread/SPX/.SPX250417P5740x-2,.SPX250417P5770

The delta chart with time axis can be easily imagined in your own head, so here's a smoother surface with vol axis:

Full screen: https://g.teddysc.me/3c04ecdcc548be3e76aa6122030e0436

If vol is high, the delta's change w.r.t. spot is almost monotonic!

The theta profile is funny, it almost looks like some abstract building, or that new train station in NYC:

Full screen: https://g.teddysc.me/1092a76882dcb076d1641e0f2ce268d3

This structure can be seen as a short straddle, with the short call replaced by a debit put spread.

JPM Collar 4700/5565/6165 (March 2025 Series)

Multiple plots here: https://g.teddysc.me/dc5935d9df9e50e9da50c85416f8cd86?m

100x100 data points to plot 1 surface

The number of data points along x and y axises is configurable, by default it's 100x100.

Running the pricing model 10k times for each leg, multiply them with quantity of each individual leg, sum them up, then smooth them to a surface, is how the plots above are generated.

# 3-nested loop! 🤯
    for i in range(price_grid.shape[0]):
        for j in range(vol_grid.shape[1]):
            net_greek = 0
            for leg in strategy:
                # Calculate Greek for this leg
                leg_greek = wrapper.sensitivities(
                    greek=greek,
                    S=price_grid[i, j],
                    K=leg["strike"],
                    T=T,
                    r=r,
                    sigma=vol_grid[i, j],
                    option=leg["put_call"],
                )
                # Add to net Greek (accounting for quantity)
                net_greek += leg_greek * leg["quantity"]

            # Apply market maker view if mm=True
            greek_values[i, j] = -net_greek if mm else net_greek

    fig = go.Figure(
        data=[
            go.Surface(z=greek_values, x=price_grid, y=vol_grid, colorscale=colorscale)
        ]
    )

Demo of my web app

Choose your own axises, dte, vol, rate, and legs!

https://www.youtube.com/watch?v=GTtuX7JnvGQ

Pydantic Models for my API routes supporting this app

class LegModel(BaseModel):
    strike: float
    quantity: int  # Positive for long, negative for short
    put_call: Literal["put", "call"]

    @validator("put_call")
    def put_call_must_be_valid(cls, v):
        if v not in ["put", "call"]:
            raise ValueError("put_call must be 'put' or 'call'")
        return v


class StrategyInput(BaseModel):
    spot: Union[float, int]
    strategy: List[LegModel]
    r: float = 0.01
    sigma: float = 0.2
    price_range: Optional[Tuple[Optional[float], Optional[float]]] = None
    time_range: Tuple[float, float] = (0.0027, 7 / 365)
    num_prices: int = 100
    num_times: int = 100
    colorscale: str = "viridis"
    mm: bool = False
    greek: Literal[
        "delta",
        "gamma",
        "theta",
        "vega",
        "rho",
        "vomma",
        "vanna",
        "charm",
        "zomma",
        "speed",
        "color",
        "ultima",
        "vega bleed",
        "price",
    ] = "delta"

    @validator("price_range", pre=True, always=True)
    def validate_price_range(cls, v):
        if v is None:
            return None
        try:
            a, b = v
            a_val = float(a) if a is not None else None
            b_val = float(b) if b is not None else None
            if a_val is None and b_val is None:
                return None
            return (a_val, b_val)
        except Exception:
            return None


class VolPriceStrategyInput(BaseModel):
    spot: Union[float, int]
    strategy: List[LegModel]
    T: float
    r: float = 0.01
    price_range: Optional[Tuple[Optional[float], Optional[float]]] = None
    vol_range: Tuple[float, float] = (0.1, 0.5)
    num_prices: int = 100
    num_vols: int = 100
    colorscale: str = "viridis"
    mm: bool = False
    greek: Literal[
        "delta",
        "gamma",
        "theta",
        "vega",
        "rho",
        "vomma",
        "vanna",
        "charm",
        "zomma",
        "speed",
        "color",
        "ultima",
        "vega bleed",
        "price",
    ] = "delta"

    @validator("price_range", pre=True, always=True)
    def validate_price_range(cls, v):
        if v is None:
            return None
        try:
            a, b = v
            a_val = float(a) if a is not None else None
            b_val = float(b) if b is not None else None
            if a_val is None and b_val is None:
                return None
            return (a_val, b_val)
        except Exception:
            return None