Option Strategies
Long Jelly Roll
Introduction
A long Jelly Roll, or simply Roll, is a combination of a long call calendar spread and a short put calendar spread. It consists of buying a put and selling a call of the same expiry, as well as buying a call and selling a put with a further expiry, where all of the contracts have the same strike price. This strategy serves as an arbitrage on Option mispricing due to the temporary disparity between the call spread and the put spread synthetic portfolios. It is a delta-, gamma-, vega-, and theta-neutral strategy, but sensitive to rho (interest rate) and phi (dividend yield).
Implementation
Follow these steps to implement the long jelly roll strategy:
- In the
initialize
method, set the start date, set the end date, and create an Option universe. - In the
on_data
method, select the expiry and strikes of the contracts in the strategy legs. - In the
on_data
method, select the contracts and place the order.
def initialize(self) -> None: self.set_start_date(2017, 4, 1) self.set_end_date(2017, 4, 22) self.set_cash(100000) self.universe_settings.asynchronous = True option = self.add_option("GOOG", Resolution.MINUTE) self._symbol = option.symbol option.set_filter(lambda x: x.include_weeklys().jelly_roll(5.0, 30, 60))
def on_data(self, slice: Slice) -> None: if self.portfolio.invested: return # Get the OptionChain chain = slice.option_chains.get(self._symbol, None) if not chain: return # Select an expiry date and ITM & OTM strike prices strike = sorted([x.strike for x in chain], key=lambda x: abs(x - chain.underlying.price))[0] contracts = [x for x in chain if x.strike == strike] far_expiry = max([x.expiry for x in contracts]) near_expiry = min([x.expiry for x in contracts])
Approach A: Call the OptionStrategies.jelly_roll
method with the details of each leg and then pass the result to the buy
method.
jelly_roll = OptionStrategies.jelly_roll(self._symbol, strike, near_expiry, far_expiry) self.buy(jelly_roll, 1)
Approach B: Create a list of Leg
objects and then call the combo_market_order, combo_limit_order, or combo_leg_limit_order method.
# Select the call and put contracts near_call = next(filter(lambda x: x.right == OptionRight.CALL and x.expiry == near_expiry, contracts)) far_call = next(filter(lambda x: x.right == OptionRight.CALL and x.expiry == far_expiry, contracts)) near_put = next(filter(lambda x: x.right == OptionRight.PUT and x.expiry == near_expiry, contracts)) call_put = next(filter(lambda x: x.right == OptionRight.PUT and x.expiry == far_expiry, contracts)) legs = [ Leg.create(near_call.symbol, -1), Leg.create(far_call.symbol, 1), Leg.create(near_put.symbol, 1), Leg.create(call_put.symbol, -1), ] self.combo_market_order(legs, 1)
Strategy Payoff
This is a delta-, gamma-, vega-, and theta-neutral strategy. The payoff is
CT1T1=(ST1−K)+PT1T1=(K−ST1)+PayoffT1=(PT1T1−PT1T0−CT1T1+CT1T0+CT2T1−CT2T0−PT2T1+PT2T0)×m−fee whereCT1T1=Market value of Call with expiry at T1 at time T1CT2T1=Market value of Call with expiry at T2 at time T1PT1T1=Market value of Put with expiry at T1 at time T1PT2T1=Market value of Put with expiry at T2 at time T1ST1=Underlying asset price at time T1K=Strike pricePayoffT=Payout total at time TCT1T0=Market value of Call with expiry at T1 when the trade openedCT2T0=Market value of Call with expiry at T2 when the trade openedPT1T0=Market value of Put with expiry at T2 when the trade openedPT2T0=Market value of Put with expiry at T2 when the trade openedm=Contract multiplierT1=Time T1 as the near expirationT2=Time T2 as the far expirationThe following chart shows the payoff at expiration:

The payoff is dependent on the market prices of the Options, but in theory, if assuming call-put parity exists, the expected payoff would be
PayoffT1=Payoffcall calendar spread−Payoffput calendar spread=K×(T2−T1)×r−D wherer=Continuous compounding interest rateD=Dividend payment during the life of the optionIf the Option is American Option, there is a risk of early assignment on the contracts you sell.
Example
The following table shows the price details of the assets in the algorithm:
Asset | Price at position open ($) | Price at the first expiry ($) | Strike ($) |
---|---|---|---|
Near-expiry Call | 18.50 | 23.75 | 832.50 |
Near-expiry Put | 22.50 | 12.85 | 832.50 |
Far-expiry Call | 23.90 | 24.45 | 832.50 |
Far-expiry Put | 19.00 | 13.70 | 832.50 |
Underlying Equity | - | 843.2500 | - |
Therefore, the payoff is
PayoffT1=(PT1T1−PT10−CT1T1+CT10+CT2T1−CT20−PT2T1+PT20)×m−fee=(12.85−22.50−23.75+18.50+24.45−23.90−13.70+19.00)×100−1.00×4=−909.00So, the strategy loses $909.
The following algorithm implements a long jelly roll Option strategy:
class JellyRollOptionStrategy(QCAlgorithm): def initialize(self) -> None: self.set_start_date(2017, 4, 1) self.set_end_date(2017, 4, 23) self.set_cash(100000) option = self.add_option("GOOG", Resolution.MINUTE) self._symbol = option.symbol # set our strike/expiry filter for this option chain option.set_filter(lambda x: x.include_weeklys().jelly_roll(5.0, 30, 60)) def on_data(self, slice: Slice) -> None: if self.portfolio.invested: return # Get the OptionChain chain = slice.option_chains.get(self._symbol, None) if not chain: return # Select an expiry date and ITM & OTM strike prices strike = sorted([x.strike for x in chain], key=lambda x: abs(x - chain.underlying.price))[0] contracts = [x for x in chain if x.strike == strike] far_expiry = max([x.expiry for x in contracts]) near_expiry = min([x.expiry for x in contracts]) jelly_roll = OptionStrategies.jelly_roll(self._symbol, strike, near_expiry, far_expiry) self.buy(jelly_roll, 1)