
The strategy allocates 88% to Treasury Bond ETFs and 12% to S&P 500 call options, rebalancing quarterly to maintain options at 3% of portfolio value, holding options for one year.
ASSET CLASS: bonds, ETFs, options | REGION: Emerging Markets, Global, United States | FREQUENCY:
Quarterly | MARKET: bonds, equities | KEYWORD: Barbell
I. STRATEGY IN A NUTSHELL
Allocate 88% to 7–10 year Treasury ETFs and 12% to in-the-money S&P 500 calls, rebalanced quarterly. Options are held for one year, maintaining a 3% portfolio allocation at each rebalance.
II. ECONOMIC RATIONALE
Treasuries reduce portfolio volatility and downside risk, while options provide equity upside. This Barbell approach mitigates losses during crises, outperforms in risk-adjusted terms, and recovers faster than traditional equity/bond mixes.
III. SOURCE PAPER
Using Barbells to Lift Risk-Adjusted Return [Click to Open PDF]
William Trainor, Dan Cupkovic, Indudeep Chhachhi, Chris Brown,
<Abstract>
This study demonstrates how a barbell strategy invested primarily in fixed income assets coupled with in-the-money long-term call options on various equity asset classes can achieve a significant percentage of upside appreciation and significantly reduce downside risk. An examination of exchange-traded funds (ETFs) covering S&P 500, NASDAQ 100, mid-cap, small-cap, developed international, emerging, and real estate equities shows a barbell strategy of 88-percent bonds and 12-percent long-term call options captures 70–124 percent of the geometric annual return of the underlying ETFs for December 2002–November 2019.


IV. BACKTEST PERFORMANCE
| Annualised Return | 8.62% |
| Volatility | 6.85% |
| Beta | -0.073 |
| Sharpe Ratio | 1.26 |
| Sortino Ratio | -0.314 |
| Maximum Drawdown | -12.68% |
| Win Rate | 19% |
V. FULL PYTHON CODE
from AlgorithmImports import *
class VolatilityRiskPremiumEffect(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(1000000)
data = self.AddEquity("IEF", Resolution.Minute)
data.SetLeverage(5)
self.symbol = data.Symbol
option = self.AddOption("IEF", Resolution.Minute)
# first parameter is min strike, second is max strike, third is min expiry and fourth is max expiry
option.SetFilter(-20, 20, timedelta(days=360), timedelta(days=500))
self.delta_target = 0.7
# storing an entire chain of option contracts for a single underlying security
self.chains = None
self.recent_month = -1
self.traded_flag = False
def OnData(self,slice):
# We have to check options contracts each minute,
# because we don't exactly know, when they are available
for i in slice.OptionChains:
self.chains = i.Value
# Check once a quarter.
if self.Time.month == self.recent_month:
return
if self.Time.month in [3,6,9,12] and not self.traded_flag:
self.recent_month = self.Time.month
else:
self.traded_flag = False
return
# Continue only if we have any option contracts
if self.chains != None:
# divide option chains into call and put options
calls = list(filter(lambda x: x.Right == OptionRight.Call, self.chains))
# if lists are empty return
if not calls: return
underlying_price = self.Securities[self.symbol].Price
expiries = [i.Expiry for i in calls]
# determine expiration date nearly one month
# expiry = min(expiries, key=lambda x: abs((x.date()-self.Time.date()).days-30))
expiry = min(expiries, key=lambda x: abs((x.date()-self.Time.date()).days-360))
strikes = [i.Strike for i in calls]
# determine at-the-money strike
strike = min(strikes, key=lambda x: abs(x-underlying_price))
# determine ITM strike by delta
itm_call = min(, key=lambda x: abs(x.Greeks.Delta-self.delta_target))
if itm_call:
options_q = int((self.Portfolio.TotalPortfolioValue*0.03) / (underlying_price * 100))
# set leverage
self.Securities[itm_call.Symbol].MarginModel = BuyingPowerModel(5)
# buy ITM call
self.Buy(itm_call.Symbol, options_q)
# buy index.
self.SetHoldings(self.symbol, 0.88)
self.traded_flag = True
else: # In this case we don't have any option contracts ready, so we will liquidate IEF, if it is the only asset in portfolio
invested = [x.Key.Value for x in self.Portfolio]
if len(invested) == 1:
self.Liquidate()
VI. Backtest Performance