Strategy: Vertical Spreads¶
Bull call spread and bear call spread templates with payoff charts.
Part 09 of 20 in the ServLoci algo/options trading notebook series — full index in notebooks/README.md.
Setup¶
# Get your dedicated static IPv6 + SOCKS5 credentials free:
# https://comm.servloci.in/register (or /auth/google?free=1 for an instant trial)
# Your api_key / api_secret pair shows up in the portal after signup:
# https://comm.servloci.in/user
!pip install -q "requests[socks]"
!curl -sL https://comm.servloci.in/sdk/servloci.py -o servloci.py
import os
from servloci import ServLoci
SERVLOCI_API_KEY = os.environ.get("SERVLOCI_API_KEY", "dhan:1000000001") # broker:client_id
SERVLOCI_API_SECRET = os.environ.get("SERVLOCI_API_SECRET", "") # from the portal — leave blank to run this notebook in demo mode
sl = None
if SERVLOCI_API_SECRET:
sl = ServLoci(api_key=SERVLOCI_API_KEY, api_secret=SERVLOCI_API_SECRET)
print("ServLoci configured:", sl.host, sl.port)
else:
print("SERVLOCI_API_SECRET not set — running in demo mode (no live proxy calls).")
Port of bull_call_spread / bear_call_spread from shared/data/strategyTemplates.js — capped-risk directional strategies.
from scipy.stats import norm
import math
def bs_price(opt_type, spot, strike, t_years, vol, rate=0.065):
if t_years <= 0 or vol <= 0:
return max(spot - strike, 0) if opt_type == "CE" else max(strike - spot, 0)
d1 = (math.log(spot / strike) + (rate + vol * vol / 2) * t_years) / (vol * math.sqrt(t_years))
d2 = d1 - vol * math.sqrt(t_years)
if opt_type == "CE":
return spot * norm.cdf(d1) - strike * math.exp(-rate * t_years) * norm.cdf(d2)
return strike * math.exp(-rate * t_years) * norm.cdf(-d2) - spot * norm.cdf(-d1)
def leg_payoff_at_expiry(leg, underlying_price):
iv = max(underlying_price - leg["strike"], 0) if leg["type"] == "CE" else max(leg["strike"] - underlying_price, 0)
sign = 1 if leg["side"] == "buy" else -1
return sign * (iv - leg["premium"]) * leg["qty"]
def strategy_payoff_at_expiry(legs, underlying_price):
return sum(leg_payoff_at_expiry(leg, underlying_price) for leg in legs)
def net_premium(legs):
return sum((-1 if leg["side"] == "buy" else 1) * leg["premium"] * leg["qty"] for leg in legs)
def payoff_curve(legs, spot, rng=0.15, steps=120):
lo, hi = spot * (1 - rng), spot * (1 + rng)
pts = []
for i in range(steps + 1):
p = lo + (hi - lo) * i / steps
pts.append({"price": p, "pnl": strategy_payoff_at_expiry(legs, p)})
return pts
def breakevens(points):
crossings = []
for a, b in zip(points, points[1:]):
if (a["pnl"] < 0 <= b["pnl"]) or (a["pnl"] > 0 >= b["pnl"]):
t = 0 if a["pnl"] == b["pnl"] else -a["pnl"] / (b["pnl"] - a["pnl"])
crossings.append(a["price"] + t * (b["price"] - a["price"]))
return crossings
def max_profit_loss(points):
pnls = [p["pnl"] for p in points]
return {"maxProfit": max(pnls), "maxLoss": min(pnls)}
def nearest_strike(spot, step):
return round(spot / step) * step
def find_premium(chain, strike, opt_type):
if not chain:
return 0
row = next((r for r in chain if r["strike"] == strike), None)
if not row:
return 0
leg = row.get("ce" if opt_type == "CE" else "pe")
return leg["ltp"] if leg and isinstance(leg.get("ltp"), (int, float)) else 0
def fill_demo_premiums(legs, spot, t_years=7 / 365, vol=0.13):
for leg in legs:
if not leg["premium"]:
leg["premium"] = round(bs_price(leg["type"], spot, leg["strike"], t_years, vol), 2)
return legs
def bull_call_spread(spot, step, chain=None, qty=1):
atm = nearest_strike(spot, step)
higher = atm + 2 * step
return [
{"side": "buy", "type": "CE", "strike": atm, "premium": find_premium(chain, atm, "CE"), "qty": qty},
{"side": "sell", "type": "CE", "strike": higher, "premium": find_premium(chain, higher, "CE"), "qty": qty},
]
def bear_call_spread(spot, step, chain=None, qty=1):
atm = nearest_strike(spot, step)
higher = atm + 2 * step
return [
{"side": "sell", "type": "CE", "strike": atm, "premium": find_premium(chain, atm, "CE"), "qty": qty},
{"side": "buy", "type": "CE", "strike": higher, "premium": find_premium(chain, higher, "CE"), "qty": qty},
]
SPOT, STEP = 24000, 50
import matplotlib.pyplot as plt
def plot_payoff(points, title):
xs = [p["price"] for p in points]
ys = [p["pnl"] for p in points]
plt.axhline(0, color="grey", linewidth=0.8)
plt.plot(xs, ys)
plt.title(title)
plt.xlabel("Underlying spot")
plt.ylabel("P&L (INR)")
plt.show()
for name, builder in [("Bull call spread", bull_call_spread), ("Bear call spread", bear_call_spread)]:
legs = fill_demo_premiums(builder(SPOT, STEP), SPOT)
points = payoff_curve(legs, SPOT)
print(name, "max P/L:", max_profit_loss(points))
plot_payoff(points, name)
« Previous: Strategy: Straddle & Strangle
Next: Strategy: Iron Condor »
Try the concepts above interactively: Options Strategy Builder · Docs · Get your static IP