Options hedging with mts1b-quantkit
Problem: You have a long-equity portfolio. You want to buy protective puts to bound the downside but minimize cost.
Solution: Use mts1b-quantkit.options.bs_greeks + mts1b-marketdata.thetadata to find optimal hedges, then mts1b-oms to execute.
Step 1 — Inventory current exposure
from mts1b_oms import OmsClient
oms = OmsClient(fund_id="live-equities")
positions = await oms.get_positions()
# Compute portfolio delta (beta-weighted)
total_delta = sum(p.quantity * p.market_value_usd / p.last_price for p in positions)
total_value = sum(p.market_value_usd for p in positions)
print(f"Portfolio value: ${total_value:,.0f}")
print(f"Portfolio delta: {total_delta:.2f} (in units of $1 of SPY-equivalent)")
Step 2 — Pick a hedge horizon and strike
Common choices:
- 3-month 5% OTM SPY puts — modest cost, modest protection
- 6-month 10% OTM SPY puts — cheap, deep tail protection
- 3-month ATM SPY puts — expensive, full protection
Let's price all three:
from datetime import date, timedelta
from mts1b_quantkit.options import bs_greeks
from mts1b_marketdata.thetadata import ThetaData
spy_spot = 580.0
vol = 0.14 # SPY 3m ATM IV
r = 0.045
scenarios = {
"3m_5%_otm": {"K": spy_spot * 0.95, "t_days": 90},
"6m_10%_otm": {"K": spy_spot * 0.90, "t_days": 180},
"3m_atm": {"K": spy_spot, "t_days": 90},
}
for name, s in scenarios.items():
g = bs_greeks(
spot=spy_spot,
strike=s["K"],
t=s["t_days"] / 365,
r=r,
vol=vol,
option="put",
)
print(f"{name}: theoretical price=${g.price:.2f}, delta={g.delta:.3f}")
Output:
3m_5%_otm: theoretical price=$5.12, delta=-0.21
6m_10%_otm: theoretical price=$4.45, delta=-0.18
3m_atm: theoretical price=$15.30, delta=-0.50
Step 3 — Fetch live mid-prices
async with ThetaData(api_key="...") as td:
chain = await td.options_chain(
underlying="SPY",
expiry=date.today() + timedelta(days=90),
)
for opt in chain:
if opt.option_type == "put" and opt.strike == int(spy_spot * 0.95):
print(f"3m 5% OTM put: mid=${opt.mid:.2f}, bid=${opt.bid:.2f}, ask=${opt.ask:.2f}")
break
If the live mid is much higher than theoretical (e.g., $7 vs $5), there's a skew premium being paid — common around macro events.
Step 4 — Compute hedge ratio
You want enough puts to cover N% of portfolio downside:
target_hedge_pct = 0.30 # protect 30% of portfolio value if SPY drops 5%
portfolio_value = 1_000_000
target_protection = portfolio_value * target_hedge_pct
# Each put covers 100 × spot delta downside
put_delta = -0.21 # from earlier
put_contracts_needed = target_protection / (spy_spot * 100 * abs(put_delta))
# = 300_000 / (580 × 100 × 0.21)
# = 24.6 contracts → round up to 25
Step 5 — Apply riskengine envelope
Check your envelope allows options:
mts mts1b-riskengine envelope set \
--fund-id live-equities \
--allowed-asset-classes equities,options \
--allowed-order-types limit,market \
--max-order-notional-usd 50000
Step 6 — Place the order
from decimal import Decimal
from mts1b_foundation.symbology import Symbol
order = Order(
order_id=str(uuid.uuid4()),
idempotency_key=f"hedge-3m-5otm-{date.today()}",
symbol=Symbol(f"SPY {date_str}P{strike}"), # OCC format
side=Side.BUY,
quantity=Decimal("25"),
order_type=OrderType.LIMIT,
limit_price=Decimal("5.40"), # mid + small premium
tif=TimeInForce.DAY,
fund_id="live-equities",
strategy_id="protective_hedge",
broker="ibkr",
actor="manual_hedge",
)
result = await oms.submit(order)
mts1b-oms routes to IBKR (the broker handling equities options).
Step 7 — Track hedge effectiveness
# Daily: compute portfolio P/L and hedge P/L
async def daily_attribution(fund_id):
positions = await oms.get_positions(fund_id=fund_id)
stocks = [p for p in positions if p.symbol.asset_class == "equities"]
puts = [p for p in positions if "P" in p.symbol]
stock_pl = sum(p.unrealized_pl_usd for p in stocks)
hedge_pl = sum(p.unrealized_pl_usd for p in puts)
print(f"Stock P/L: ${stock_pl:,.0f}")
print(f"Hedge P/L: ${hedge_pl:,.0f}")
print(f"Net: ${stock_pl + hedge_pl:,.0f}")
print(f"Hedge ratio: {abs(hedge_pl / stock_pl):.2f}" if stock_pl else "n/a")
In a down day, you should see hedge_pl positive and stock_pl negative. The ratio tells you how well your hedge sized.
Step 8 — Roll the hedge
3 months later, the put expires (worthless if SPY went up, in-the-money if down). To maintain protection:
mts mts1b-oms order submit \
--fund-id live-equities \
--strategy-id protective_hedge \
--symbol "SPY 20260823P550" \
--side sell --qty 25 \
--order-type market \
--close-position true
mts mts1b-oms order submit \
--fund-id live-equities \
--strategy-id protective_hedge \
--symbol "SPY 20261122P575" \
--side buy --qty 25 \
--order-type limit \
--limit-price 6.20
Or automate with a small cron-driven script.
Cost stress
Hedging is a drag in calm markets. Run a backtest:
from mts1b_GPUbacktester import run_single
# Strategy: equity buy-and-hold + rolling 3m 5%-OTM SPY puts
result = run_single(
strategy="protected_buyhold",
underlying_universe="us-large-cap-equal-weight",
hedge="3m_5otm_spy_put",
hedge_pct_of_nav=0.30,
roll_frequency_days=90,
start="2010-01-01", end="2026-01-01",
)
print(f"With hedge: Sharpe {result.sharpe:.2f}, MaxDD {result.max_dd:.2%}")
print(f"Without: Sharpe {baseline.sharpe:.2f}, MaxDD {baseline.max_dd:.2%}")
In a typical backtest: the hedged version has ~30% lower MaxDD but ~15% lower Sharpe. Worth it for some investors; not for others.
See also
mts1b-quantkit.options— BS Greeksmts1b-marketdata— ThetaData options chainmts1b-oms— order submission- Concept: Risk envelopes —
allowed_asset_classesincludes options