Last active
May 20, 2021 17:23
-
-
Save chrisforbes/85d65b8abe9a0ce06ade9336cdcafe48 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Trades SPY short strangles around 16 deltas, according to most of tastytrade mechanics | |
// - Manages one position at a time, sized to not exceed 30% of net liq | |
// - XXX: QC margin model is not really correct for short strangles, need to swap in a custom model. | |
// - XXX: BPR is determined by putting on one unit first. This is not 100% reliable. | |
// - Looks for a trade to put on whenever it has nothing | |
// - Opens trades at 30-60 dte | |
// - XXX: legs in and out with single option orders. Platform supports multi-leg orders but not using it yet. | |
// - Closes to 50% max profit | |
// - If assigned (almost never happens due to management mechanics) liquidates entire remaining stock+option position | |
// - XXX: calculates delta on our side since there are issues with the builtin pricing model impl. Only IV from the builtin | |
// model is used. | |
// - XXX: does not yet roll untested side intracycle when strikes are breached. | |
namespace QuantConnect.Algorithm.CSharp | |
{ | |
public class ManagedShortStrangles : QCAlgorithm | |
{ | |
public override void Initialize() | |
{ | |
SetStartDate(2010, 1, 1); | |
SetEndDate(2020, 12, 31); | |
SetCash(100000); | |
var spy = AddEquity(sym, Resolution.Minute); | |
spy.SetDataNormalizationMode(DataNormalizationMode.Raw); | |
var opt = AddOption(sym, Resolution.Minute, fillDataForward: true); | |
opt.SetFilter(-200, 200, TimeSpan.FromDays(30), TimeSpan.FromDays(60)); | |
opt.PriceModel = QuantConnect.Securities.Option.OptionPriceModels.BlackScholes (); | |
opt.VolatilityModel = new QuantConnect.Securities.StandardDeviationOfReturnsVolatilityModel(2); | |
spy.VolatilityModel = new QuantConnect.Securities.StandardDeviationOfReturnsVolatilityModel(2); | |
SetWarmUp(100, Resolution.Daily); | |
SetBenchmark(sym); | |
} | |
string sym = "SPY"; | |
DateTime? rollPoint = null; | |
decimal? profitTarget = null; | |
public override void OnData(Slice data) | |
{ | |
if (IsWarmingUp) | |
return; | |
if (!Portfolio.Invested && data.Time.Minute == 0) | |
{ | |
if (data.OptionChains.Count() == 0) | |
return; | |
// find the cycle closest to 45 days and sell the 16 delta strangle in it | |
var opts = data.OptionChains.First().Value.Contracts | |
.Select(o => o.Value) | |
.Where(c => Math.Abs(Delta(c)) < 0.18 && Math.Abs(Delta(c)) >= 0.14) | |
.OrderBy(c => c.Expiry) | |
.ThenByDescending(c => Delta(c)); | |
var put = opts.FirstOrDefault(c => c.Right == OptionRight.Put); | |
var call = opts.FirstOrDefault(c => c.Right == OptionRight.Call); | |
// No suitable options | |
if (put == null || call == null) | |
{ | |
Debug("Failed to find an option to trade this tick, waiting " + data.Time); | |
return; | |
} | |
// XXX: assume we can always get filled at the mid price. | |
// this is true only with aggressive liquidity providers. | |
var sellPrice = 0.5m * (put.BidPrice + put.AskPrice + call.BidPrice + call.AskPrice); | |
var netDelta = -Delta(call) -Delta(put); | |
Debug(string.Format( | |
"Selling the {0}/{1} strangle for {2} and net {3} delta {4} dte", | |
put.Strike, call.Strike, | |
sellPrice, netDelta, (put.Expiry - data.Time).TotalDays | |
)); | |
var bpBefore = Portfolio.GetMarginRemaining(Portfolio.TotalPortfolioValue); | |
// XXX: should really enter this as a single order. | |
Sell(put.Symbol, 1); | |
Sell(call.Symbol, 1); | |
var bpAfter = Portfolio.GetMarginRemaining(Portfolio.TotalPortfolioValue); | |
Debug(string.Format("Buying power reduction: {0} remaining: {1}", | |
bpBefore - bpAfter, bpAfter)); | |
// Trade additional units to get to 30% capital in use | |
var targetBp = 0.7m * Portfolio.TotalPortfolioValue; | |
if (bpBefore != bpAfter) { | |
var additionalUnits = Math.Floor((bpAfter - targetBp) / (bpBefore - bpAfter)); | |
if (additionalUnits > 0) | |
{ | |
Debug(string.Format("+{0} units", | |
additionalUnits)); | |
Sell(put.Symbol, additionalUnits); | |
Sell(call.Symbol, additionalUnits); | |
} | |
} | |
rollPoint = put.Expiry - TimeSpan.FromDays(21); | |
profitTarget = sellPrice / 2; | |
Debug(string.Format("Profit target {0}", profitTarget)); | |
} | |
if (Portfolio[sym].Invested) | |
{ | |
// if we have any of the underlying get rid of it. | |
Debug("Got assigned, liquidating"); | |
Liquidate(); | |
rollPoint = null; | |
profitTarget = null; | |
} | |
if (rollPoint != null && data.Time > rollPoint) | |
{ | |
Debug("Liquidating at 21 dte"); | |
// XXX: do a proper roll. | |
Liquidate(); | |
rollPoint = null; | |
profitTarget = null; | |
} | |
if (data.Time.Minute == 30 && Portfolio.Invested) | |
{ | |
var shortEquity = Portfolio.Select(p => p.Value).Where(p => p.Type == SecurityType.Option && p.Quantity != 0).Sum(p => p.GetQuantityValue(0.1m)) * 0.1m; | |
// Debug(string.Format("{0} .. {1}", profitTarget, shortEquity)); | |
if (shortEquity < profitTarget) | |
{ | |
Debug("Managing 50% winner"); | |
Liquidate(); | |
rollPoint = null; | |
profitTarget = null; | |
} | |
} | |
} | |
static double D1(double S, double K, double T, double sigma, double r, double q) | |
{ | |
return (Math.Log(S/K) + (r - q + (sigma * sigma) / 2) * T) / (sigma * Math.Sqrt(T)); | |
} | |
static double Delta(bool isCall, double S, double K, double T, double sigma, double r, double q) | |
{ | |
var d1 = D1(S, K, T, sigma, r, q); | |
if (isCall) | |
return Math.Exp(-r * T) * NCDF(d1); | |
else | |
return -Math.Exp(-r * T) * NCDF(-d1); | |
} | |
static double Delta(OptionContract o) | |
{ | |
var t = (o.Expiry - o.Time).TotalSeconds / (86400 * 365); | |
return Delta(o.Right == OptionRight.Call, | |
(double)o.UnderlyingLastPrice, | |
(double)o.Strike, | |
t, (double)o.ImpliedVolatility, | |
0.0, 0.0); // XXX: r,q not really zero. | |
} | |
static double NCDF(double z) | |
{ | |
double p = 0.3275911; | |
double a1 = 0.254829592; | |
double a2 = -0.284496736; | |
double a3 = 1.421413741; | |
double a4 = -1.453152027; | |
double a5 = 1.061405429; | |
int sign; | |
if (z < 0.0) | |
sign = -1; | |
else | |
sign = 1; | |
double x = Math.Abs(z) / Math.Sqrt(2.0); | |
double t = 1.0 / (1.0 + p * x); | |
double erf = 1.0 - (((((a5 * t + a4) * t) + a3) | |
* t + a2) * t + a1) * t * Math.Exp(-x * x); | |
return 0.5 * (1.0 + sign * erf); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment