I always find charts intuitive. I'll look at calls, but it is straigthforward to extend it to puts as well.
As mentioned in a comment:
- Sticky strike is really just black Scholes delta computed with Finite Difference.
- Sticky delta refers to adjusting IV when you bump up and down (because IV is stuck to delta /moneyness).
I use Julia to plot and compute the relevant values. The following code is used for plotting a dummy vol surface that has 3 different regions:
- a downward sloping IV (around P2) -> Sticky Delta will be lower
- a flat IV (around P5) -> Sticky Delta will be identical
- an upward sloping IV (around P8) -> Sticky Delta will be higher
The graph on the left hand side (LHS) shows in the dashed green line a flat vol surface where the up and down shifts around spot have the same vol as an input. The orange dots on the RHS correspond to the orange dots on the LHS. The only difference are the two IVs at the very beginning and end, which makes the skew steeper.
using Plots, Distributions, DataFrames, PrettyTables
vola = [0.2474, 0.2471, 0.2470,0.2470,0.2470,0.2470, 0.2470, 0.2471, 0.2473]
vola_flat = [0.2471, 0.2471, 0.2471,0.2470,0.2470, 0.2470, 0.2471, 0.2471, 0.2471]
vola2 = [0.2484, 0.2471, 0.2470,0.2470,0.2470,0.2470,0.2470, 0.2471, 0.2483]
strike = 98.5:0.5:102.5
vol = Dict(zip(strike,vola))
vol_flat = Dict(zip(strike,vola_flat))
vol2 = Dict(zip(strike,vola2))
p1 = plot(strike, vola, ylims=(minimum(vola2)-0.0005,maximum(vola2)+0.0005),xlabel="Strike",
ylabel="IV", label = false, linewidth = 3)
plot!(strike, vola_flat, ylims=(minimum(vola2)-0.0005,maximum(vola2)+0.0005),xlabel="Strike",
ylabel="IV", label = false, linewidth = 2, linestyle = :dashdot, linecolor = :green)
[annotate!([strike[i]], [vola[i]+0.0001], "P$i") for i in 2:3:8]
[plot!([strike[i], strike[i]], [vola[i], 0], linestyle = :dashdot) for i in 2:3:8]
plot!(strike, vola, seriestype=:scatter, label = false, color = :orange)
p2 = plot(strike, vola2, ylims=(minimum(vola2)-0.0005,maximum(vola2)+0.0005),xlabel="Strike",
ylabel="IV", label = false, linewidth = 3)
plot!(strike, vola2, seriestype=:scatter, label = false,color = :green)
plot!(strike, vola, seriestype=:scatter, label = false, color = :orange)
[annotate!([strike[i]], [vola[i]+0.0001], "P$i") for i in 2:3:8]
[plot!([strike[i], strike[i]], [vola[i], 0], linestyle = :dashdot) for i in 2:2:8]
plot(p1,p2, left_margin = 2Plots.mm, legend = false)
plot!(size=(900,500))
Let's start by computing Black Scholes values and Delta first. The function looks like this (the cp_flag is allowing for calls (1) and puts (-1)):
function BSM(S,K,t,rf,d,σ, cp_flag)
d1 = ( log(S/K) + (rf - d + 1/2*σ^2)*t ) / (σ*sqrt(t))
d2 = d1 - σ*sqrt(t)
opt = cp_flag*exp(-d*t)S*N(cp_flag*d1) - exp(-rf*t)*cp_flag*K*N(cp_flag*d2)
return opt, exp(-d*t)*N(cp_flag*d1)
end
We evaluate this for different spot values, that correspond to the 3 different points,
df = DataFrame("Spot" => spot, "Call Constant Vol" => [BSM(spot,k,t,log(1+r*t)/t,log(1+d*t)/t,vol_flat[spot], 1)[1] for spot in spot],
"IV Constant" => vola_flat,
"Call BS Delta" => [round(BSM(spot,k,t,log(1+r*t)/t,log(1+d*t)/t,vol_flat[spot], 1)[2]*100, digits = 3) for spot in spot],
"Call Variable IV I" => [BSM(spot,k,t,log(1+r*t)/t,log(1+d*t)/t,vol[spot], 1)[1] for spot in spot],
"IV I" => vola,
"Call Variable IV II" => [BSM(spot,k,t,log(1+r*t)/t,log(1+d*t)/t,vol2[spot], 1)[1] for spot in spot],
"IV II" => vola2)
hl_1 = Highlighter((data,i,j) -> data[i,1] == 99.0, crayon"bg:dark_gray white bold")
hl_2 = Highlighter((data,i,j) -> data[i,1] == 100.5, crayon"bg:dark_gray white bold")
hl_3 = Highlighter((data,i,j) -> data[i,1] == 102.0, crayon"bg:dark_gray white bold")
PrettyTables.pretty_table(df, border_crayon = Crayons.crayon"blue",
header_crayon = Crayons.crayon"bold green",
formatters = ft_printf("%.2f" ),
highlighters = (hl_value(55.307),hl_value(57.643), hl_value(59.915), hl_1,hl_2, hl_3))
which yields the following table:
The Black lines indicate where we want to compute Delta. The Table already shows the corresponding Black Scholes Delta in orange color. It also computed all corresponding BSM values for the various spot values, either with the a flat vol (the shifted spots will use identical IVs) or actual vols according to the change in moneyness / delta the corresponds to the shift.
What is left now is to show what sticke strike and sticky delta refers to. As already mentioned, sticky strike just corresponds to BSM Delta. Finite Difference means that you bump up and down to get delta (see the link above for more details). In our case this looks like this:
df_delta = DataFrame( "Spot" => ["$(spot[i+1])" for i in 1:2:5],
"Sticky Strike" => [round((df[!, "Call Constant Vol"][i+2] - df[!, "Call Constant Vol"][i])/0.01, digits = 2) for i in 1:3:8],
"Sticky Delta I" => [round((df[!, "Call Variable IV I"][i+2] - df[!, "Call Variable IV I"][i])/0.01, digits = 2) for i in 1:3:8],
"Sticky Delta II" => [round((df[!, "Call Variable IV II"][i+2] - df[!, "Call Variable IV II"][i])/0.01, digits = 2) for i in 1:3:8])
PrettyTables.pretty_table(df_delta, border_crayon = Crayons.crayon"blue",
header_crayon = Crayons.crayon"bold green",
formatters = ft_printf("%.2f" ),
highlighters = (hl_value(55.31),hl_value(57.64), hl_value(59.91)))
As suggested at the begining:
- Sticky Delta will be lower if the IV surface slopes downward
- Sticky Delta will be identical if the IV surface is flat (around spot)
- Sticky Delta will be higher if the IV surface slopes downward
The steeper, the more pronounced this effect. You can quickly check the flat surface case if you have Bloomberg. Load OVML (for FX) and compare Delta with sticky delta (both are displayed). Override IV manually and you will see both become identical.
Since the tables display the BSM values as well, it is also easy to find an explanation. Shifting spot down makes a call option worth more. Increasing IV as well. If the surface slopes downward, you have higher BSM values if you shift down, and lower if you shift up. Since the difference is what matters, you get smaller delta values. The opposite holds for upward sloping surfaces.