Some Iron Condor Heuristics

In the world of Iron Condors (an option position composed of short, higher strike (bear) call spread and a short, lower strike (bull) put spread), there are two heuristics that are bandied about without much comment. I’m generally unwilling to accept some Internet author’s word for something (unless it completely gels with everything I know), so I set out to convince myself that these heuristics held. Here are some demonstrations (I hesitate to use the word proof) that convinced me.

Introduction and Some Preliminaries

In [54]:
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import scipy.stats as ss

def labelIt(ax, title):
    ax.set_title(title)
    ax.set_xlabel("Spot")
    ax.set_ylabel("Position Value")


pd.set_option('display.mpl_style', 'default') # Make the graphs a bit prettier
%pylab inline --no-import-all
figsize(15, 5)
Populating the interactive namespace from numpy and matplotlib

An Iron Condor is composed of four options: long put, short put, short call, long call. The Iron Condor is equivalent to two other, non-primitive, positions.

The first is a position that is short one (credit) put spread (a bull put spread) and short one (credit) call spread (a bear call spread).

We can also describe it as being short one strangle (the inner, short, put and call – also called the body) and long one strangle (the outer, long, put and call – also called the wings) with the long strangle being wider than the short strangle.

In the following scenario, we’re going to assume we have an Iron Condor at 90/100 and 150/160 and we receive $1 for each spread we sell. The underlying stock price is $125. Our spread widths are $10. In one Iron Condor we sell two spreads, so we receive $2.

In [3]:
stockP = np.linspace(75,175,200)
zs     = stockP * 0.0
width = 10.0

The Standalone Spreads

Here we have the strike, premium, and value of the put side of the Iron Condor.

In [33]:
shortPutStrike = 100.0
longPutStrike = shortPutStrike - width

longPutPrem = 3.00
shortPutPrem = 4.00

longPutV  = -longPutPrem + np.maximum(longPutStrike - stockP, zs)
shortPutV = shortPutPrem - np.maximum(shortPutStrike - stockP, zs)

bullPutSpreadV = longPutV + shortPutV
print "put spread (max loss, max gain): (%s, %s)"% (np.min(bullPutSpreadV), np.max(bullPutSpreadV))
put spread (max loss, max gain): (-9.0, 1.0)

And likewise, here we have the strike, premium, and value of the call side of the Iron Condor.

In [38]:
shortCallStrike = 150.0
longCallStrike = shortCallStrike + 10.0

longCallPrem = 2.50
shortCallPrem = 3.50

shortCallV = shortCallPrem - np.maximum(stockP - shortCallStrike, zs)
longCallV = -longCallPrem + np.maximum(stockP - longCallStrike, zs)

bearCallSpreadV = longCallV + shortCallV
print "call spread (max loss, max gain): (%s, %s)"% (np.min(bearCallSpreadV), np.max(bearCallSpreadV))
call spread (max loss, max gain): (-9.0, 1.0)

In [60]:
ax1 = plt.subplot(121)
ax2 = plt.subplot(122)
ax1.plot(stockP, bullPutSpreadV)
ax2.plot(stockP, bearCallSpreadV)
labelIt(ax1, "Put Spread")
labelIt(ax2, "Call Spread")

The Iron Condor

Now, we can literally add those two positions and get the equivalent Iron Condor.

In [41]:
#icV_longhand = (shortCallPrem - np.maximum(stockP - 150.0, zs) - 
#                longCallPrem + np.maximum(stockP - 160.0, zs) +
#                -longPutPrem + np.maximum(90.0 - stockP, zs) + 
#                shortPutPrem - np.maximum(100.0 - stockP, zs))

icV = bullPutSpreadV + bearCallSpreadV

# max loss, max gain
# max loss = premium received - spread width (2 - 10 for current example) 
print "iron condor (max loss, max gain): (%s, %s)"% (np.min(icV), np.max(icV))
iron condor (max loss, max gain): (-8.0, 2.0)

In [62]:
plt.plot(stockP, icV, "b")
plt.plot(stockP, zs, 'k')
plt.ylim(np.min(icV)-1, np.max(icV)+1)

# since we received $2, we have $2 of insurance
# thus, $2 more extreme than our short strikes is our break even point
plt.plot(shortPutStrike - 2.0,  0.0, 'rx')   # total premium received = 2.0
plt.plot(shortCallStrike + 2.0, 0.0, 'rx')
labelIt(plt.gca(), "Iron Condor")

An Inferred Normal

Let’s assume that our positions, which are $25 from the current stock price, are \(10\delta\) positions. This means that the (risk-neutral) probability of a 25 point move is 10%. There are two side-notes here.

  1. We don’t expect \(10\delta\) to be symmetric about the stock price. Puts generally run at a premium to calls because folks want more insurance against bad events than they want opportunity for riches.
  2. The equivalence of \(10\%= 10\delta\) is a (commonly used) approximation. See the discussion of Heuristic 2.

So, based on random walk (normal distribution outcomes), we need a standard normal curve with a mean of 125 and variance such that P(x>150) = P(x<100) = .1. To graph this, we need to figure out the appropriate standard deviation (or variance) that will match this.

Recalling that a z-score is the distance to a point from the mean of a standard normal distribution, we can ask what z-score gives us 10% of the distribution further to the right. For the standard normal distribution, the "percentile point function" gives the z-score (\(\sigma\) distance) for a given cumulative percent of the standard normal distribution:

In [47]:
ss.norm.ppf(.9)
Out[47]:
1.2815515655446004

The prior cell says that "90% of the mass of the standard normal lives to the left of 1.28155…". Then, since \(z=\frac{x-\mu}{\sigma}\), we have \(\sigma=\frac{25.0}{ss.norm.ppf(.9)}\)

In [8]:
sigma = 25.0 / ss.norm.ppf(.9)

And we can graph that normal distribution:

In [48]:
plt.plot(stockP, ss.norm(loc=125.0,scale=sigma).pdf(stockP));

Breakeven

For yucks, we can overlay the probability and payoff graphs:

In [64]:
ax1 = plt.gca()
ax2 = ax1.twinx()
ax1.set_ylim(-10, 4)
ax2.set_ylim(-.005, .030)

# we have two dollars of insurance, so we breakeven at 
# $2 more extreme than the short strikes 
p1 = shortPutStrike  - 2.0
p2 = shortCallStrike + 2.0

ax1.plot(stockP, icV, 'g')
ax1.plot(p1, 0.0, 'rx')   # total premium received = 2.0
ax1.plot(p2, 0.0, 'rx')
labelIt(ax1, "Iron Condor")

ourNorm = ss.norm(loc=125.0,scale=sigma)
ax2.plot(stockP, ourNorm.pdf(stockP))
ax2.plot(p1, ourNorm.pdf(p1), 'rx')   # total premium received = 2.0
ax2.plot(p2, ourNorm.pdf(p2), 'rx')
ax2.set_ylabel("Probability");
In [53]:
# the options must not be priced fairly!
#    either there is an inefficiency in the market
#    or, in this case,
#    my example numbers, made for ease of reading, don't make for
#    a realistic scenario
# or, better yet, 
#    the expectd value is the risk-free rate of return for this time frame
print "Expected value of the option: %s" % np.sum(icV * ss.norm(loc=125.0,scale=sigma).pdf(stockP))
Expected value of the option: 1.58790908775

Heuristic 1: The Probability of Profit for an Iron Condor Profits is \(MaxLoss / SpreadWidth\)

We’re now in position to look at the first heuristic. You may see this (e.g., ):

\[
ProbabilityOfProfit = PoP = MaxLoss / Width = \frac{(SpreadWidth – Premium)}{SpreadWidth}
\]

\(MaxLoss\) is a positive quantity indicating how much you can lose (e.g., \(MaxLoss=8\) means you can lose $8).

Now, we know that the profitable area of the trade is the region between the red Xs on the probability plot. So, let’s define two probabilities: \(P_g\) and \(P_b\) as the probability of a good (profitable – non-loss) event and a bad event. These sum to 1, so \(P_g + P_b = 1\). For us, a bad result is in the tails of our normal distribution. Specifically, it is the red area below. We need to determine an expression for that area (our \(PoP=P_g=1-RedArea\)).

In [52]:
extremeMask = np.logical_or(stockP < p1, stockP > p2)
plt.plot(stockP, ourNorm.pdf(stockP))
plt.fill_between(stockP, ourNorm.pdf(stockP), where=extremeMask, color='r');

Now, in a purely efficient market with "correctly" priced options, we have \(\mathbb{E}(Position)=0\). So, with \(w=SpreadWidth\) and \(g=Premium\) we have a good result \(g\) and a bad result (\(MaxLoss=SpreadWidth – Premium = w-g\)). And some unknown probabilities that these events happen (\(P_b\) and \(P_g\)).

\[
\mathbb{E}(Position) = 0 = g P_g + -(w – g) P_b
\]

One quick note. Strictly speaking, \(g\) is really not a constant over the profitable region. However, we could solve for a "new" \(g\) value that accounts for the trapezoid shape of the profitable area of the IC. The end result would be the same, we’d just have three different regions to work with (two tails, two triangles, and one center — but the tails and triangles are symmetric).

This means that:

\[
gP_g = (w-g)P_b = (w-g)(1-P_g)
\]

and

\[
\begin{eqnarray}
\frac{g}{w-g} &=& \frac{1-P_g}{P_g} = \frac{1}{P_g} – 1 \\
\frac{g}{w-g} + \frac{w-g}{w-g} &=& \frac{1}{P_g} \\
\frac{w}{w-g} &=& \frac{1}{P_g} \\
P_g &=& \frac{w-g}{w} = 1 – \frac{g}{w}
\end{eqnarray}
\]

This says that \(P_g\) (the probability of a good event), given our assumptions of market-neutrality (is this better said as "no arbitrage"?), is maximum loss divided by spread width which is \(\frac{SpreadWidth – Premium}{SpreadWidth}\).

Heuristic 2: \(\Delta = P(ITM)\)

See pg 14 of http://cfe.cboe.com/education/TradingVolatility.pdf

\(\Delta\) is the rate of change of the value of an option given a unit change in the underlying asset. In other words, if the stock moves by \(s\), …

In the "usual" discussion of the Black-Scholes formula (see http://en.wikipedia.org/wiki/Black%E2%80%93Scholes_model), two variables are defined \(d_1\) and \(d_2\). \(d_2 = d_1 – \sigma \sqrt{T}\) so, \(d_2 < d_1\). We’ll use the usual notation \(\Phi\) as the cumulative normal distribution. Take as a given that \(\Phi(d_1) = \Delta_{Call}\). By put-call parity, we also have \(\Delta_{Call} – \Delta_{Put} = 1\), so \(\Delta_{Put} = \Delta_{Call} – 1 = \Phi(d_1) – 1\). Lastly, if a call and put are struck at the same value, one of them will finish ITM. Thus, \(P_{CallITM} + P_{PutITM} = 1\). So, we have:

\[
\begin{eqnarray}
\Phi(d_1) &=& \Delta_{Call} \\
\Phi(d_1) – 1 &=& \Delta_{Put} \\
\Phi(d_2) &=& P_{CallITM} \\
1 – \Phi(d_2) &=& P_{PutITM}
\end{eqnarray}
\]

A few comments. Since the normal cumulative distribution is monotonically increasing, for \(d_2 < d_1\) we have \(\Phi(d_2) < \Phi(d_1)\). Thus, \(P_{CallITM} = \Phi(d_2) < \Phi(d_1) = \Delta_{Call}\).

Next, for \(0<x<1\), we have \(x-1 < 0\). Then, if we factor out a \(-1\), \(x-1 = -1 (-x + 1)\). But, \(abs(-1(1-x)) = 1-x\). So we have a useful result that \(abs(x-1) = 1-x\).

So, \(abs(\Delta_{Put}) = abs(\Delta_{Call} – 1) = 1 – \Delta_{Call} < 1-P_{Call} = P_{Put}\)

In short, \(P_{Call} < \Delta_{Call}\) and \(abs(\Delta_{Put}) < P_{Put}\)

The difference, \(\Delta_{Call} – P_{Call} = \Phi(d_1) – \Phi(d_2) = \Phi(d_1) – \Phi(d_1 – \sigma\sqrt{T})\) will be small when \(\sigma\sqrt{T}\) is small. So, for short time periods and lower volatility, the heuristic is more accurate.

Visually

Recall that we were working with \(10\Delta\) positions. We’ll also set \(\sigma\sqrt{T} = 5\). So,

In [19]:
print ourNorm.ppf(.1), ourNorm.ppf(.9)
100.0 150.0

In [67]:
plt.ylim(-0.1, 1.0)
plt.plot(stockP, ourNorm.cdf(stockP))

for deltaPoint, label, position in zip([100.0, 150.0],
                             ["$\Delta_{Call} = \Phi(d_1)$", "$abs(\Delta_{Put}) = 1-\Phi(d_1)$"],
                             [(25, 15), (-5, 5)]):
    pt = deltaPoint, ourNorm.cdf(deltaPoint)
    plt.plot(pt[0], pt[1], 'ro')
    plt.annotate(label, xy=pt, xytext = position, 
                 textcoords='offset points', ha="right", fontsize = 18)

sigmaRootT = 5
for itmPoint, label, position in zip([100.0-sigmaRootT, 150.0+sigmaRootT],
                                     ["$\Phi(d_2)=P_{CallITM}$", "$1-\Phi(d_2)=P_{PutITM}$"],
                                     [(-25, -25), (0,-20)]):
    pt = itmPoint, ourNorm.cdf(itmPoint)
    plt.plot(pt[0], pt[1], 'ro')
    plt.annotate(label, xy=pt, xytext = position, 
                 textcoords='offset points', fontsize = 18)
plt.xlabel("Spot")
plt.ylabel("Cumulative Probability");

Additional Resources

You can grab a copy of this notebook.

Even better, you can view it using nbviewer.

License

Unless otherwise noted, the contents of this notebook are under the following license. The code in the notebook should be considered part of the text (i.e., licensed and treated as as follows).

Creative Commons License
DrsFenner.org Blog And Notebooks by Mark and Barbara Fenner is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
Permissions beyond the scope of this license may be available at drsfenner.org/blog/about-and-contacts.