Skip to content

Instantly share code, notes, and snippets.

@chrisforbes
Last active May 20, 2021 17:23
Show Gist options
  • Save chrisforbes/85d65b8abe9a0ce06ade9336cdcafe48 to your computer and use it in GitHub Desktop.
Save chrisforbes/85d65b8abe9a0ce06ade9336cdcafe48 to your computer and use it in GitHub Desktop.
// 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