
The strategy trades 10 energy futures, using seven style-based long-short sub-portfolios, standardizing signals, weighting positions by signal value, combining portfolios equally, and rebalancing monthly for diversified returns.
ASSET CLASS: futures | REGION: United States | FREQUENCY:
Monthly | MARKET: commodities | KEYWORD: Energy
I. STRATEGY IN A NUTSHELL
Trade 10 U.S.-listed energy futures using seven standardized style signals—roll-yield, hedging pressure, speculative pressure, past performance, value, liquidity, and skewness. Go long on positive signals and short on negative ones, combining sub-portfolios equally and rebalancing monthly.
II. ECONOMIC RATIONALE
Combining diverse, proven style predictors enhances return forecasting and captures risk premia in energy futures. The composite signal approach yields robust, consistent performance even after accounting for costs and alternative specifications.
III. SOURCE PAPER
Capturing Energy Risk Premia [Click to Open PDF]
Adrian Fernandez-Perez, Ana-Maria Fuertes and Joëlle Miffre. University College Dublin (UCD) – Department of Banking & Finance. Bayes Business School, City, University of London. Audencia Business School.
<Abstract>
This paper studies the energy futures risk premia that can be extracted through long-short
portfolios that exploit heterogeneities across contracts as regards various characteristics or
signals and integrations thereof. Investors can earn a sizeable premium of about 8% and 12%
per annum by exploiting the energy futures contract risk associated with the hedgers’ net
positions and roll-yield characteristics, respectively, in line with predictions from the hedging
pressure hypothesis and theory of storage. Simultaneously exploiting various signals towards
style-integration with alternative weighting schemes further enhances the premium. In
particular, the style-integrated portfolio that equally weights all signals stands out as the most
effective. The findings are robust to transaction costs, data mining and sub-period analyses.


IV. BACKTEST PERFORMANCE
| Annualised Return | 12.38% |
| Volatility | 13.75% |
| Beta | 0.053 |
| Sharpe Ratio | 0.9 |
| Sortino Ratio | -0.164 |
| Maximum Drawdown | -22.32% |
| Win Rate | 16% |
V. FULL PYTHON CODE
from AlgorithmImports import *
import data_tools
from typing import Dict
#endregion
class HedgersEffectCommodities(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2005, 1, 1)
self.SetCash(100000)
tickers:Dict[str, str] = {
'CME_CL1' : Futures.Energies.CrudeOilWTI, # Crude Oil Futures, Continuous Contract
'CME_NG1' : Futures.Energies.NaturalGas, # Natural Gas (Henry Hub) Physical Futures, Continuous Contract
'ICE_O1' : Futures.Energies.HeatingOil, # Heating Oil Futures, Continuous Contract
'CME_CU1' : Futures.Energies.Ethanol, # Chicago Ethanol (Platts) Futures
}
# Weekly hedging pressure data.
self.data:Dict[Symbol, data_tools.SymbolData] = {}
self.futures_data:Dict[str, data_tools.FuturesData] = {}
min_expiration_days:int = 0
max_expiration_days:int = 360
self.min_futures:int = 2
self.leverage:int = 5
self.total_portfolios:int = 7
self.one_year_period:int = 252
self.period:int = int(252 * 5.5)
self.cot_period:int = 52
self.volume_period:int = 42
for qp_ticker, qc_ticker in tickers.items():
# Add quantpedia back-adjusted data.
security = self.AddData(data_tools.QuantpediaFutures, qp_ticker, Resolution.Daily)
security.SetFeeModel(data_tools.CustomFeeModel())
security.SetLeverage(self.leverage)
qp_symbol:Symbol = security.Symbol
cot_ticker:str = 'Q' + qp_ticker.split('_')[1][:-1]
cot_symbol:Symbol = self.AddData(data_tools.CommitmentsOfTraders, cot_ticker, Resolution.Daily).Symbol
# QC futures
future:Future = self.AddFuture(qc_ticker, Resolution.Daily, dataNormalizationMode=DataNormalizationMode.Raw)
future.SetFilter(timedelta(days=min_expiration_days), timedelta(days=max_expiration_days))
future_ticker:str = future.Symbol.Value
self.futures_data[future_ticker] = data_tools.FuturesData(self.one_year_period, self.volume_period)
self.data[qp_symbol] = data_tools.SymbolData(cot_symbol, future_ticker,
self.period, self.cot_period)
self.recent_month:int = -1
self.settings.daily_precise_end_time = False
self.settings.minimum_order_margin_portfolio_percentage = 0.
def FindAndUpdateContracts(self, futures_chain, ticker:str) -> None:
near_contract:FuturesContract = None
dist_contract:FuturesContract = None
if ticker in futures_chain:
contracts:list[:FuturesContract] = [contract for contract in futures_chain[ticker] if contract.Expiry.date() > self.Time.date()]
if len(contracts) >= 2:
contracts:list[:FuturesContract] = sorted(contracts, key=lambda x: x.Expiry, reverse=False)
near_contract = contracts[0]
dist_contract = contracts[1]
self.futures_data[ticker].update_contracts(near_contract, dist_contract)
def OnData(self, data):
curr_date:datetime.date = self.Time.date()
for qp_symbol, symbol_obj in self.data.items():
# store daily price
if qp_symbol in data and data[qp_symbol]:
price:float = data[qp_symbol].Value
symbol_obj.update_qp_prices(price)
cot_symbol:Symbol = symbol_obj.cot_symbol
if cot_symbol in data and data[cot_symbol]:
speculator_long_count:float = data[cot_symbol].GetProperty('LARGE_SPECULATOR_LONG')
speculator_short_count:float = data[cot_symbol].GetProperty('LARGE_SPECULATOR_SHORT')
hedgers_long_count:float = data[cot_symbol].GetProperty('COMMERCIAL_HEDGER_LONG')
hedgers_short_count:float = data[cot_symbol].GetProperty('COMMERCIAL_HEDGER_SHORT')
if speculator_long_count != 0 and speculator_short_count != 0 and hedgers_long_count != 0 and hedgers_short_count != 0:
hedging_pressure_value:float = (hedgers_short_count - hedgers_long_count) / (hedgers_long_count + hedgers_short_count)
speculative_pressure_value:float = (speculator_long_count - speculator_short_count) / (speculator_long_count + speculator_short_count)
symbol_obj.update_pressures(hedging_pressure_value, speculative_pressure_value)
# daily update qc future data
if data.FutureChains.Count > 0:
for ticker, future_obj in self.futures_data.items():
# check if near contract is expired or is not initialized
if not future_obj.is_initialized() or \
(future_obj.is_initialized() and future_obj.near_contract.Expiry.date() == curr_date):
self.FindAndUpdateContracts(data.FutureChains, ticker)
# update QC futures rolling return
if future_obj.is_initialized():
near_c:FuturesContract = future_obj.near_contract
dist_c:FuturesContract = future_obj.distant_contract
if near_c.Symbol in data and data[near_c.Symbol] and dist_c.Symbol in data and data[dist_c.Symbol]:
near_price:float = data[near_c.Symbol].Value * self.Securities[ticker].SymbolProperties.PriceMagnifier
dist_price:float = data[dist_c.Symbol].Value * self.Securities[ticker].SymbolProperties.PriceMagnifier
volume:float = near_c.Volume
if near_price != 0 and dist_price != 0 and volume != 0:
future_obj.update_prices_and_volumes(near_price, dist_price, volume)
if self.recent_month == curr_date.month:
return
self.recent_month = curr_date.month
self.Liquidate()
last_qp_price_by_symbol:Dict[Symbol, float] = {}
portfolios:list[Dict[Symbol, float]] = [
{} for i in range(self.total_portfolios)
]
for qp_symbol, symbol_obj in self.data.items():
future_ticker:str = symbol_obj.future_ticker
if any([self.securities[symbol].get_last_data() and self.time.date() > data_tools.LastDateHandler.get_last_update_date()[symbol] for symbol in [qp_symbol, symbol_obj.cot_symbol]]):
self.liquidate()
return
if symbol_obj.qp_prices_ready() and symbol_obj.pressures_ready() and self.futures_data[future_ticker].prices_and_volumes_ready():
latest_near_contract_price:float = self.futures_data[future_ticker].get_latest_near_contract_price()
near_contract_volumes:list[float] = self.futures_data[future_ticker].get_near_contract_volumes()
portfolios[0][qp_symbol] = self.futures_data[future_ticker].get_roll_yield()
portfolios[1][qp_symbol] = symbol_obj.get_hedging_pressure()
portfolios[2][qp_symbol] = symbol_obj.get_speculative_pressure()
portfolios[3][qp_symbol] = symbol_obj.get_momentum(self.one_year_period)
portfolios[4][qp_symbol] = symbol_obj.get_value(self.one_year_period, latest_near_contract_price)
portfolios[5][qp_symbol] = symbol_obj.get_liquidity(near_contract_volumes)
portfolios[6][qp_symbol] = symbol_obj.get_skewness(self.one_year_period)
last_qp_price_by_symbol[qp_symbol] = symbol_obj.get_last_price()
if len(portfolios[0]) < self.min_futures:
return
# trade execution
for portfolio in portfolios:
# signal mean and std
portfolio_mean = np.mean([x for x in portfolio.values()])
portfolio_std = np.std([x for x in portfolio.values()])
# signal standardization
portfolio = { x[0] : (x[1] - portfolio_mean) / portfolio_std for x in portfolio.items() }
long_leg = [x for x in portfolio.items() if x[1] > 0]
short_leg = [x for x in portfolio.items() if x[1] < 0]
total_signal_long = sum(abs(x[1]) for x in long_leg)
total_signal_short = sum(abs(x[1]) for x in short_leg)
sub_portfolio_weight = self.Portfolio.TotalPortfolioValue / self.total_portfolios
for symbol, signal in long_leg:
symbol_quantity:float = np.floor((sub_portfolio_weight*(signal/total_signal_long)) / last_qp_price_by_symbol[symbol])
self.MarketOrder(symbol, symbol_quantity)
for symbol, signal in short_leg:
symbol_quantity:float = np.floor((sub_portfolio_weight*(signal/total_signal_short)) / last_qp_price_by_symbol[symbol])
self.MarketOrder(symbol, symbol_quantity)
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))