Illustrative Panels for Short Strangles and Delta Hedging
Summary
This plotting script creates four explanatory visuals for an ETH short-strangle options example. The panels show expiry profit and loss for a short put and call, how the combined option delta may change as spot moves, how a threshold-triggered hedge could limit portfolio delta, and how strikes might be selected around target call and put deltas. It extracts selected strikes and delta readings from a run log, then uses defaults when strike values are missing.
The figures are teaching aids rather than evidence from a market test. The delta curves use a deliberately simplified approximation, the hedge path is generated from random simulated drift, and the strike-picker panel uses a toy implied-volatility smile. The described run settings also disable entries and hedging, so the figures explain mechanics without demonstrating live orders or realized strategy performance.
Key ideas
- A short strangle combines a short put and a short call, with expiry payoff shaped by the strikes and premium.
- The combined delta of the option legs can shift as the underlying price moves.
- A delta threshold can be used as a rule for triggering portfolio rehedging.
- Target option deltas provide one possible basis for choosing call and put strikes.
- The plotted delta, hedge path, and implied-volatility smile are illustrative approximations.
Tags
Full text
# render_panels.py
```py
"""
Render the Derive delta-neutral options tutorial panels.
The runner reads its settings from constants in
``crates/adapters/derive/examples/node_delta_neutral.rs``. Before the capture, set
``DERIVE_ENVIRONMENT`` to ``DeriveEnvironment::Mainnet`` and ``HEDGE_ENABLED`` to
``false``, and revert them afterwards. Then, after building NautilusTrader from
source, run these commands from the repository root:
make sync
timeout 45 cargo run --example derive-delta-neutral --package nautilus-derive --features examples \
> /tmp/derive_dn.log 2>&1
export DN_LOG=/tmp/derive_dn.log
uv run --project python --no-sync \
python docs/tutorials/assets/delta_neutral_options_derive/render_panels.py
The default example has ``enter_strangle: false`` so a clean account
places no option entry orders, and ``HEDGE_ENABLED = false`` makes the run submit
no hedge orders. The renderer parses the log for the selected
call / put strikes, then draws four illustrative panels explaining the
strategy mechanics: a short-strangle payoff curve, a delta-drift
simulation, the rehedge threshold visualization, and the strike picker.
PNGs use the ``nautilus_dark`` tearsheet theme.
"""
from __future__ import annotations
import os
import re
from pathlib import Path
import numpy as np
import plotly.graph_objects as go
from nautilus_trader.analysis.tearsheet import _write_figure
from nautilus_trader.analysis.themes import get_theme
OUT = Path(__file__).resolve().parent
LOG_PATH = Path(os.environ.get("DN_LOG", "/tmp/derive_dn.log")) # noqa: S108
THEME = get_theme("nautilus_dark")
TEMPLATE = THEME["template"]
COLORS = THEME["colors"]
PRIMARY = COLORS["primary"]
POSITIVE = COLORS["positive"]
NEGATIVE = COLORS["negative"]
NEUTRAL = COLORS["neutral"]
GRID = COLORS["grid"]
ANSI = re.compile(r"\x1b\[[0-9;]*m")
SELECTED_CALL = re.compile(r"Selected call: ([^\s]+) \(strike=(?P<k>[\-0-9.]+)\)")
SELECTED_PUT = re.compile(r"Selected put: ([^\s]+) \(strike=(?P<k>[\-0-9.]+)\)")
GREEKS_LINE = re.compile(r"Greeks update: (?P<inst>\S+) delta=(?P<delta>[\-0-9.]+)")
PORTFOLIO_DELTA = re.compile(r"portfolio_delta=(?P<d>[\-0-9.]+)")
def parse_log(path: Path) -> dict:
out = {
"call_strike": None,
"put_strike": None,
"call_inst": None,
"put_inst": None,
"underlying": None,
"deltas": [],
}
if not path.exists():
return out
for raw in path.read_text(encoding="utf-8").splitlines():
line = ANSI.sub("", raw)
m = SELECTED_CALL.search(line)
if m:
out["call_strike"] = float(m.group("k"))
out["call_inst"] = m.group(1)
m = SELECTED_PUT.search(line)
if m:
out["put_strike"] = float(m.group("k"))
out["put_inst"] = m.group(1)
m = GREEKS_LINE.search(line)
if m:
out["deltas"].append(float(m.group("delta")))
m = PORTFOLIO_DELTA.search(line)
if m:
out["deltas"].append(float(m.group("d")))
return out
def apply_layout(fig: go.Figure, title: str, height: int = 480) -> None:
fig.update_layout(
template=TEMPLATE,
title={"text": title, "x": 0.02, "xanchor": "left"},
paper_bgcolor=COLORS["background"],
plot_bgcolor=COLORS["background"],
font={"family": "Inter, system-ui, sans-serif", "size": 13},
margin={"l": 60, "r": 30, "t": 70, "b": 50},
height=height,
width=1200,
legend={"orientation": "h", "yanchor": "bottom", "y": 1.02, "xanchor": "right", "x": 1.0},
)
fig.update_xaxes(gridcolor=GRID, zeroline=False)
fig.update_yaxes(gridcolor=GRID, zeroline=False)
def panel_a_strangle_payoff(
call_k: float,
put_k: float,
underlying: float,
premium: float = 1500.0,
) -> go.Figure:
fig = go.Figure()
span = max(call_k, underlying * 1.1) - min(put_k, underlying * 0.9)
s = np.linspace(
min(put_k, underlying * 0.9) - span * 0.1,
max(call_k, underlying * 1.1) + span * 0.1,
300,
)
payoff = premium - np.maximum(s - call_k, 0) - np.maximum(put_k - s, 0)
fig.add_trace(
go.Scatter(
x=s,
y=payoff,
mode="lines",
name="Strangle pnl at expiry",
line={"color": PRIMARY, "width": 2.0},
fill="tozeroy",
fillcolor="rgba(0, 207, 190, 0.15)",
),
)
fig.add_hline(y=0, line={"color": NEUTRAL, "dash": "dash", "width": 1})
fig.add_vline(
x=put_k,
line={"color": POSITIVE, "dash": "dash", "width": 1.2},
annotation_text=f"PUT strike {put_k:.0f}",
annotation_position="top left",
annotation={"font": {"size": 11, "color": POSITIVE}},
)
fig.add_vline(
x=call_k,
line={"color": NEGATIVE, "dash": "dash", "width": 1.2},
annotation_text=f"CALL strike {call_k:.0f}",
annotation_position="top right",
annotation={"font": {"size": 11, "color": NEGATIVE}},
)
fig.add_vline(
x=underlying,
line={"color": NEUTRAL, "dash": "dot", "width": 1.0},
annotation_text=f"current {underlying:.0f}",
annotation_position="bottom right",
annotation={"font": {"size": 11, "color": NEUTRAL}},
)
apply_layout(
fig,
f"Derive ETH short-strangle payoff: short {put_k:.0f} put + short {call_k:.0f} call",
height=460,
)
fig.update_xaxes(title_text="ETH at expiry (USDC)")
fig.update_yaxes(title_text="pnl (USDC, premium net of intrinsic)")
return fig
def panel_b_delta_drift(call_k: float, put_k: float, underlying: float) -> go.Figure:
fig = go.Figure()
moves = np.linspace(-0.05, 0.05, 200)
spot = underlying * (1.0 + moves)
def call_delta_bs(s, k) -> object:
# Toy approximation: monotonic delta from -0 to 1 around the strike.
z = (s - k) / (underlying * 0.05)
return 0.5 * (1.0 + np.tanh(z))
def put_delta_bs(s, k) -> object:
z = (k - s) / (underlying * 0.05)
return -0.5 * (1.0 + np.tanh(z))
short_call_delta = -call_delta_bs(spot, call_k)
short_put_delta = -put_delta_bs(spot, put_k)
portfolio_delta = short_call_delta + short_put_delta
fig.add_trace(
go.Scatter(
x=spot,
y=short_call_delta,
mode="lines",
name="short CALL leg delta",
line={"color": NEGATIVE, "width": 1.4, "dash": "dot"},
),
)
fig.add_trace(
go.Scatter(
x=spot,
y=short_put_delta,
mode="lines",
name="short PUT leg delta",
line={"color": POSITIVE, "width": 1.4, "dash": "dot"},
),
)
fig.add_trace(
go.Scatter(
x=spot,
y=portfolio_delta,
mode="lines",
name="portfolio delta (pre-hedge)",
line={"color": PRIMARY, "width": 2.0},
),
)
fig.add_hline(y=0.5, line={"color": POSITIVE, "dash": "dash", "width": 1})
fig.add_hline(y=-0.5, line={"color": NEGATIVE, "dash": "dash", "width": 1})
fig.add_vline(
x=underlying,
line={"color": NEUTRAL, "dash": "dot", "width": 1.0},
annotation_text=f"entry {underlying:.0f}",
annotation_position="top right",
annotation={"font": {"size": 11, "color": NEUTRAL}},
)
apply_layout(
fig,
"Portfolio delta drift as the underlying moves around entry (toy approximation)",
height=460,
)
fig.update_xaxes(title_text="ETH spot (USDC)")
fig.update_yaxes(title_text="delta")
return fig
def panel_c_hedge_threshold(threshold: float = 0.5, interval_secs: int = 30) -> go.Figure:
fig = go.Figure()
rng = np.random.default_rng(7)
t = np.linspace(0, 5 * interval_secs, 600)
base = 0.04 * np.cumsum(rng.standard_normal(t.size))
base = base - base[0]
pre_hedge = base.copy()
hedge_marks_x: list[float] = []
hedge_marks_y: list[float] = []
hedge_offset = 0.0
out = []
for i, b in enumerate(pre_hedge):
delta = b + hedge_offset
if abs(delta) > threshold:
hedge_marks_x.append(t[i])
hedge_marks_y.append(delta)
hedge_offset -= delta
delta = 0.0
out.append(delta)
out = np.asarray(out)
fig.add_trace(
go.Scatter(
x=t,
y=pre_hedge,
mode="lines",
name="hypothetical drift (no hedge)",
line={"color": NEUTRAL, "width": 1.0, "dash": "dot"},
),
)
fig.add_trace(
go.Scatter(
x=t,
y=out,
mode="lines",
name="portfolio delta (with hedge)",
line={"color": PRIMARY, "width": 1.6},
),
)
if hedge_marks_x:
fig.add_trace(
go.Scatter(
x=hedge_marks_x,
y=hedge_marks_y,
mode="markers",
name="hedge fired",
marker={"symbol": "x", "size": 12, "color": "#eeeeee"},
),
)
fig.add_hline(y=threshold, line={"color": POSITIVE, "dash": "dash", "width": 1.2})
fig.add_hline(y=-threshold, line={"color": NEGATIVE, "dash": "dash", "width": 1.2})
fig.add_hline(y=0, line={"color": GRID, "width": 1})
apply_layout(
fig,
f"Derive hedge trigger: rehedge_delta_threshold = {threshold} (simulated)",
height=460,
)
fig.update_xaxes(title_text="seconds since entry")
fig.update_yaxes(title_text="portfolio delta")
return fig
def panel_d_strike_picker(
call_k: float,
put_k: float,
underlying: float,
target: float = 0.20,
) -> go.Figure:
fig = go.Figure()
span = max(call_k, underlying * 1.1) - min(put_k, underlying * 0.9)
strikes = np.linspace(
min(put_k, underlying * 0.9) - span * 0.05,
max(call_k, underlying * 1.1) + span * 0.05,
30,
)
iv_smile = 0.30 + 0.0006 * np.abs(strikes - underlying)
fig.add_trace(
go.Scatter(
x=strikes,
y=iv_smile,
mode="lines+markers",
line={"color": PRIMARY, "width": 1.4},
marker={"size": 6},
name="mark IV (toy smile)",
),
)
fig.add_vline(
x=call_k,
line={"color": NEGATIVE, "dash": "dash", "width": 1.4},
annotation_text=f"selected CALL {call_k:.0f}",
annotation_position="top left",
annotation={"font": {"size": 11, "color": NEGATIVE}},
)
fig.add_vline(
x=put_k,
line={"color": POSITIVE, "dash": "dash", "width": 1.4},
annotation_text=f"selected PUT {put_k:.0f}",
annotation_position="top right",
annotation={"font": {"size": 11, "color": POSITIVE}},
)
fig.add_vline(
x=underlying,
line={"color": NEUTRAL, "dash": "dot", "width": 1.0},
annotation_text=f"underlying {underlying:.0f}",
annotation_position="bottom right",
annotation={"font": {"size": 11, "color": NEUTRAL}},
)
apply_layout(
fig,
f"Derive strike selection: call near +{target:.0%} delta and put near -{target:.0%}",
height=420,
)
fig.update_xaxes(title_text="strike (USDC)")
fig.update_yaxes(title_text="mark_iv")
return fig
def main() -> None:
info = parse_log(LOG_PATH)
print(info)
call_k = info["call_strike"] or 3800.0
put_k = info["put_strike"] or 3200.0
underlying = info["underlying"] or ((call_k + put_k) / 2.0)
panels = {
"panel_a_strangle_payoff.png": panel_a_strangle_payoff(call_k, put_k, underlying),
"panel_b_delta_drift.png": panel_b_delta_drift(call_k, put_k, underlying),
"panel_c_hedge_threshold.png": panel_c_hedge_threshold(),
"panel_d_strike_picker.png": panel_d_strike_picker(call_k, put_k, underlying),
}
for name, fig in panels.items():
path = OUT / name
_write_figure(fig, str(path))
print(f"wrote {path} ({path.stat().st_size / 1024:.1f} KB)")
if __name__ == "__main__":
main()
```Shown in full with attribution under the source's licence. Licence: LGPL-3.0
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.