Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 56 additions & 6 deletions quantflow/options/docs/butterfly.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,64 @@
# Butterfly

A butterfly consists of three strikes: a lower wing, a body, and an upper wing with equal
log-spacing. It is constructed by buying the wings and selling twice the body (long butterfly)
A butterfly consists of three strikes: a lower wing, a body, and an upper wing.
It is constructed by buying the wings and selling twice the body (long butterfly)
or the reverse (short butterfly).

## Structure

All three legs share the same maturity and the same option type. Only the strikes differ.

- quantity option at K_low (lower wing)
- -2 * quantity option at K_mid (body)
- quantity option at K_high (upper wing)

The three strikes are symmetric in log space: log(K_mid/K_low) = log(K_high/K_mid).

A positive quantity is a long butterfly. A negative quantity is a short butterfly.

## Wing symmetry

The strikes are usually equally spaced in price, so that the body sits midway between
the wings:

K_mid - K_low = K_high - K_mid

This is the conventional construction, but it is not enforced. Any three increasing strikes
are accepted, which allows unequal wing widths (a structure sometimes called a broken wing
or skip strike butterfly).

The difference between the two wing widths drives the behaviour of the unbalanced case:

residual = (K_mid - K_low) - (K_high - K_mid)

This is zero when the wings are equally spaced, and two properties of the butterfly hold
only in that case.

The first is that the payoff closes. Outside the wings a balanced butterfly expires worthless,
giving the familiar tent shaped payoff peaking at K_mid. With unequal wings a constant value
equal to the residual remains beyond the outer strikes: a loss when the upper wing is wider,
a gain when the lower wing is wider.

The second is that the call and put constructions coincide (see below).

## Call vs Put construction

By put-call parity, a butterfly built entirely with calls is equivalent in price to one built
entirely with puts. The choice is purely a liquidity consideration:
entirely with puts, provided the wings are equally spaced. The price difference between the two
constructions is exactly the residual defined above, so it vanishes only in the balanced case.

For a balanced butterfly the choice is therefore purely a liquidity consideration:

- Body above ATM (moneyness > 0): use calls, which are more liquid OTM on the upside
- Body below ATM (moneyness < 0): use puts, which are more liquid OTM on the downside
- Body at ATM (moneyness = 0): either works

With unequal wings the two constructions no longer price the same, so the option type becomes
a pricing decision rather than a liquidity one and should be chosen explicitly.

## Greeks

- Delta: near zero for log-symmetric strikes around ATM
The Greeks below describe the balanced case.

- Delta: near zero when the wings are equally spaced and the body sits close to the forward
- Gamma: small and negative when long, small and positive when short. The gamma of the wings
and body largely cancel out, leaving low net exposure.
- Vega: small and negative when long, small and positive when short. The vega of the three
Expand All @@ -36,6 +69,23 @@ The low vega and gamma distinguish the butterfly from outright vol strategies su
and strangles. The butterfly is primarily sensitive to the curvature of the vol smile across
strikes, not to the overall level of volatility.

### Gamma near expiry

The cancellation above holds only while the wings sit close to the body relative to the size of
a typical move, that is, while the wing width is small compared with sigma * sqrt(ttm).

When the body sits at the forward and expiry approaches, the wings move far away in those units
and their gamma decays to zero, while the body gamma rises. What remains is the body alone.

A long butterfly then carries the gamma of a short straddle struck at the body, and a short
butterfly the gamma of a long straddle. The near cancellation of the longer dated case is gone.

The counterpart is theta: the same position earns time decay for as long as the underlying stays
pinned to the body.

Away from the body the sign reverses. Close to expiry the position is long gamma near each wing,
where the wing option is itself at the money.

## Use case

A long butterfly profits when the underlying stays close to the body strike at expiry.
Expand Down
83 changes: 83 additions & 0 deletions quantflow/options/docs/ladder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Ladder

A ladder combines three options of the same type and the same maturity: one bought at the
strike closest to the money and two sold further out of the money. It is sometimes called a
Christmas tree.

The position is net short one option, so a long ladder is short volatility and short gamma.

## Structure

All three legs share the same maturity and the same option type. Only the strikes differ.

A positive quantity is a long ladder. A negative quantity is a short ladder.

### Call ladder

Requires K_low < K_mid <= K_high.

- quantity call at K_low (bought)
- -quantity call at K_mid (sold)
- -quantity call at K_high (sold)

### Put ladder

Requires K_low <= K_mid < K_high.

- quantity put at K_high (bought)
- -quantity put at K_mid (sold)
- -quantity put at K_low (sold)

## Relation to the ratio spread

When the two sold strikes coincide (K_mid = K_high for a call ladder, K_low = K_mid for a put
ladder) the two short legs fall on the same strike and the structure becomes a ratio spread,
quoted as a 1 by 2: one option bought against two sold.

In that case the strategy holds two legs rather than three, the sold leg carrying twice the
quantity, so the position is represented the same way a hand built 1 by 2 would be.

The ladder is therefore the generalisation of the 1 by 2 in which the two short legs are spread
across different strikes, which softens the loss profile beyond the outer strike at the cost of
a lower premium received.

## Payoff

A long call ladder at expiry, ignoring the premium:

- below K_low: zero
- between K_low and K_mid: rises one for one with the underlying
- between K_mid and K_high: flat at its maximum of K_mid - K_low
- above K_high: falls one for one with the underlying, without limit

The upper breakeven sits at K_mid + K_high - K_low. Above that level the position loses without
limit, so a ladder is not a defined risk structure.

The put ladder mirrors this. Its maximum is K_high - K_mid, reached between K_low and K_mid, and
its lower breakeven sits at K_low + K_mid - K_high. When that level is at or below zero the
position stays profitable all the way down.

## Greeks

- Delta: slightly positive for a long call ladder while the underlying sits below K_low, turning
negative as the underlying rallies through the sold strikes, and approaching -1 once all three
options are deep in the money. The put ladder mirrors this.
- Gamma: positive near the bought strike, where its gamma peaks, and negative around and beyond
the two sold strikes. The position is net short gamma overall.
- Vega: negative when long, since two options are sold against one bought. The exposure grows as
the underlying approaches the sold strikes.
- Theta: positive when long. The ladder collects time decay, which is the counterpart of its
short gamma.

## Use case

A long call ladder expresses a moderately bullish view with a ceiling: it pays most when the
underlying settles between K_mid and K_high, and it is often opened for a net credit because two
options are sold against one bought. The premium collected is compensation for uncapped risk
above the upper breakeven.

A long put ladder is the mirror image, expressing a moderately bearish view with a floor.

Selling two wing options makes the ladder attractive when the wings of the smile are rich
relative to the strike being bought, so it is sensitive to the slope and the level of the smile,
not only to the level of volatility.
4 changes: 3 additions & 1 deletion quantflow/options/docs/strangle.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ A positive quantity is a long strangle (long vol). A negative quantity is a shor

## Greeks

- Delta: near zero for log-symmetric strikes
- Delta: near zero when the two legs carry equal and opposite delta, which is what quoting the
strikes by delta (for example a 25 delta put against a 25 delta call) gives directly. Strikes
placed symmetrically around the forward only approximate this.
- Gamma: positive when long, negative when short
- Vega: positive when long, negative when short; lower magnitude than a straddle for the same notional

Expand Down
2 changes: 2 additions & 0 deletions quantflow/options/strategies/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from .base import Strategy, StrategyError, StrategyLeg, StrategyPrice
from .butterfly import Butterfly
from .calendar_spread import CalendarSpread
from .ladder import Ladder
from .spread import Spread
from .straddle import Straddle
from .strangle import Strangle

__all__ = [
"Butterfly",
"CalendarSpread",
"Ladder",
"Spread",
"Strategy",
"StrategyError",
Expand Down
6 changes: 0 additions & 6 deletions quantflow/options/strategies/butterfly.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,6 @@ def _option_type_for_log_strike(mid_log_strike: float) -> OptionType:


class Butterfly(Strategy, frozen=True):
"""Three-strike strategy: long wings, short body.

Long butterfly when quantity > 0, short butterfly when quantity < 0.
Can be constructed with calls or puts, both are equivalent by put-call parity.
"""

description: ClassVar[str] = load_description("butterfly.md")

@classmethod
Expand Down
5 changes: 0 additions & 5 deletions quantflow/options/strategies/calendar_spread.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@


class CalendarSpread(Strategy, frozen=True):
"""Same strike, same option type, two maturities.

Long the far maturity, short the near maturity when quantity > 0.
"""

description: ClassVar[str] = load_description("calendar_spread.md")

@property
Expand Down
101 changes: 101 additions & 0 deletions quantflow/options/strategies/ladder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from __future__ import annotations

from datetime import datetime
from decimal import Decimal
from typing import ClassVar

from typing_extensions import Self

from quantflow.options.inputs import OptionMetadata, OptionType
from quantflow.utils.numbers import Number, to_decimal

from .base import Strategy, StrategyError, StrategyLeg, load_description

Positions = tuple[tuple[Decimal, Decimal], ...]


class Ladder(Strategy, frozen=True):
description: ClassVar[str] = load_description("ladder.md")

@property
def option_type(self) -> OptionType:
"""Option type of the ladder."""
return self.legs[0].meta.option_type

@classmethod
def _from_positions(
cls, option_type: OptionType, maturity: datetime, positions: Positions
) -> Self:
return cls(
legs=tuple(
StrategyLeg(
meta=OptionMetadata(
option_type=option_type,
strike=strike,
maturity=maturity,
),
quantity=quantity,
)
for strike, quantity in positions
)
)

@classmethod
def call(
cls,
low_strike: Number,
mid_strike: Number,
high_strike: Number,
maturity: datetime,
quantity: Number = 1.0,
) -> Self:
"""Long call at low_strike, short calls at mid_strike and high_strike.

When mid_strike equals high_strike the two short legs collapse onto a
single leg of twice the quantity and the ladder becomes a 1 by 2 call
ratio spread.
"""
low = to_decimal(low_strike)
mid = to_decimal(mid_strike)
high = to_decimal(high_strike)
if not (low < mid <= high):
raise StrategyError(
"Strikes must satisfy low_strike < mid_strike <= high_strike."
)
q = to_decimal(quantity)
positions: Positions = (
((low, q), (mid, to_decimal(-2) * q))
if mid == high
else ((low, q), (mid, -q), (high, -q))
)
return cls._from_positions(OptionType.CALL, maturity, positions)

@classmethod
def put(
cls,
low_strike: Number,
mid_strike: Number,
high_strike: Number,
maturity: datetime,
quantity: Number = 1.0,
) -> Self:
"""Long put at high_strike, short puts at mid_strike and low_strike.

When low_strike equals mid_strike the two short legs collapse onto a
single leg of twice the quantity and the ladder becomes a 1 by 2 put
ratio spread.
"""
low = to_decimal(low_strike)
mid = to_decimal(mid_strike)
high = to_decimal(high_strike)
if not (low <= mid < high):
raise StrategyError(
"Strikes must satisfy low_strike <= mid_strike < high_strike."
)
q = to_decimal(quantity)
positions: Positions = (
((high, q), (mid, to_decimal(-2) * q))
if low == mid
else ((high, q), (mid, -q), (low, -q))
)
return cls._from_positions(OptionType.PUT, maturity, positions)
7 changes: 0 additions & 7 deletions quantflow/options/strategies/spread.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,6 @@


class Spread(Strategy, frozen=True):
"""Vertical spread: same option type, two strikes, same maturity.

Long the spread when quantity > 0 (debit), short when quantity < 0 (credit).
Call spread: long low strike, short high strike.
Put spread: long high strike, short low strike.
"""

description: ClassVar[str] = load_description("spread.md")

@classmethod
Expand Down
5 changes: 0 additions & 5 deletions quantflow/options/strategies/straddle.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@


class Straddle(Strategy, frozen=True):
"""Call and put at the same strike.

Long vol when quantity > 0, short vol when quantity < 0.
"""

description: ClassVar[str] = load_description("straddle.md")

@classmethod
Expand Down
5 changes: 0 additions & 5 deletions quantflow/options/strategies/strangle.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@


class Strangle(Strategy, frozen=True):
"""Call and put at different OTM strikes.

Long vol when quantity > 0, short vol when quantity < 0.
"""

description: ClassVar[str] = load_description("strangle.md")

@classmethod
Expand Down
Loading
Loading