Cash-Secured Put — Code Walkthrough
Understand how CRR prices the sold put, what assignment probability means mathematically, and how annualized return on the collateral is calculated.
Cash-Secured Put — Code Walkthrough
The cash-secured put (CSP) is a popular income strategy. You sell an OTM put and hold enough cash to buy the stock if assigned. It's equivalent to a covered call in terms of risk profile. Let's walk through what makes its Python implementation interesting — especially the assignment probability calculation.
Step 1 — CashSecuredPutStrategy.evaluate()
def evaluate(self, spot_price, historical_vol=0.25):
sigma = self.implied_volatility or historical_vol
T = self.days_to_expiry / 365.0
strike = round(spot_price * (1 + self.strike_offset), 2) # e.g. 5% OTM
# Price the put we're selling using CRR
premium = crr_price(spot_price, strike, T, r, sigma, 'put')
total_premium = premium * self.contracts * 100 # per-contract dollar amount
capital = strike * self.contracts * 100 # cash collateral required
breakeven = strike - premium # effective purchase price if assigned
# Annualized return on collateral
ann_return = (total_premium / capital) * (365 / self.days_to_expiry) * 100
assign_prob = assignment_probability_crr(spot_price, strike, T, r, sigma)The Breakeven Concept
"If I get assigned, I'm forced to buy the stock at the strike price. But I collected the premium first, so my effective cost basis is strike − premium." If you planned to buy the stock anyway, this is a strategy to acquire it at a discount — or collect income if it doesn't fall.
Step 2 — assignment_probability_crr: Risk-Neutral Terminal Distribution
def assignment_probability_crr(S, K, T, r, sigma, steps=100):
"""P(assigned) = P(S_T < K) under the risk-neutral measure."""
dt = T / steps
u = math.exp(sigma * math.sqrt(dt))
d = 1.0 / u
p = (math.exp(r * dt) - d) / (u - d)
q = 1 - p
# Build the probability of each terminal node using log-space arithmetic
log_p, log_q = math.log(p), math.log(q)
log_binom = [0.0] * (steps + 1)
log_binom[0] = steps * log_q # all-down node: q^n
for j in range(1, steps + 1):
# Recurrence: log C(n,j) × p^j × q^(n-j)
log_binom[j] = log_binom[j-1] + math.log(steps - j + 1) - math.log(j) + log_p - log_q
# Sum probabilities for all terminal nodes where S_T < K
prob_below = 0.0
for j in range(steps + 1):
spot_t = S * (u**j) * (d**(steps - j))
if spot_t < K:
prob_below += math.exp(log_binom[j])
return min(max(prob_below, 0.0), 1.0)Why Log-Space?
Binomial coefficients C(100, 50) are astronomically large (~10^29). Computing them directly overflows even float64. By working in log-space (log of the probability instead of the probability), we avoid overflow entirely, then exponentiate only at the summation step.
The function uses the CRR's risk-neutral probability p to construct the full terminal distribution, then sums the probability mass for all nodes where the stock ends below the strike. This is the model-implied P(assignment) — the probability the put expires in the money under the risk-neutral measure.
Step 3 — Interpreting the Output
| Output Field | Formula | What to Watch For |
|---|---|---|
| premium_collected | CRR put price × contracts × 100 | Higher IV = fatter premium, but also higher risk |
| breakeven | strike − premium (per share) | Your actual cost basis if assigned |
| annualized_return | (premium/capital) × (365/days) × 100 | Compare to risk-free rate — is the risk worth it? |
| assignment_probability | P(S_T < K) × 100 | Too high = risky; sweet spot is usually 20–35% |
- You sell an OTM put and hold cash = strike × 100 as collateral
- Max profit is the premium collected; max loss is strike − premium
- Assignment probability uses the risk-neutral terminal distribution from the CRR tree
- Annualized return = (premium / capital) × (365 / days) × 100