Overall Statistics |
Total Orders 4497 Average Win 0.22% Average Loss -0.27% Compounding Annual Return -7.710% Drawdown 82.300% Expectancy -0.144 Start Equity 1000000 End Equity 301661.38 Net Profit -69.834% Sharpe Ratio -0.14 Sortino Ratio -0.097 Probabilistic Sharpe Ratio 0.000% Loss Rate 53% Win Rate 47% Profit-Loss Ratio 0.82 Alpha -0.038 Beta 0.01 Annual Standard Deviation 0.268 Annual Variance 0.072 Information Ratio -0.419 Tracking Error 0.303 Treynor Ratio -3.688 Total Fees $60822.50 Estimated Strategy Capacity $29000000.00 Lowest Capacity Asset USO THORT68ZZSYT Portfolio Turnover 3.16% |
# region imports from AlgorithmImports import * from delta_hedge_module import DeltaHedgeModule # endregion class ETFOptionsDeltaHedgeModule(DeltaHedgeModule): def __init__( self, algo: QCAlgorithm ) -> None: super(ETFOptionsDeltaHedgeModule, self).__init__(algo) def delta_hedge(self, symbol: Symbol) -> None: delta_hedge_quantity: float = self._get_delta_hedge_quantity(symbol) if delta_hedge_quantity != 0: self._algo.market_order(symbol, delta_hedge_quantity) def _get_delta_hedge_quantity(self, symbol: Symbol) -> float: chains: List[OptionChain] = [ opt_chain for opt_chain in self._algo.current_slice.option_chains.values() if opt_chain.underlying.symbol == symbol ] if not chains: self._algo.log(f'DeltaHedgeModule._get_delta_hedge_quantity: No option chain found for {symbol}. Number of option chains present in the algorithm {self._algo.current_slice.option_chains.count}') return 0. chain: OptionChain = chains[0] contracts_in_portfolio: List[OptionContract] = [ contract.value for contract in chain.contracts if self._algo.securities[contract.value.symbol].type == SecurityType.OPTION and self._algo.portfolio[contract.value.symbol].invested ] delta_hedge_quantity: float = -1 * np.floor(sum([ c.greeks.delta * self._algo.portfolio[c.symbol].quantity * self._algo.securities[c].contract_multiplier for c in contracts_in_portfolio ])) additional_quantity: float = delta_hedge_quantity - self._algo.portfolio[symbol].quantity return additional_quantity
#region imports from AlgorithmImports import * #endregion class SymbolData(): def __init__( self, cot_f_data_symbol: Symbol, cot_c_data_symbol: Symbol, ) -> None: self.cot_f_data_symbol: Symbol = cot_f_data_symbol self.cot_c_data_symbol: Symbol = cot_c_data_symbol self._call: Option = None self._put: Option = None self._call_delta: Union[None, float] = None self._put_delta: Union[None, float] = None def update_options(self, call: OptionContract, put: OptionContract) -> None: self._call = call self._put = put def reset_options(self) -> None: self._call = None self._put = None @property def is_ready(self) -> bool: return self._call or self._put class LastDateHandler(): _last_update_date:Dict[Symbol, datetime.date] = {} @staticmethod def get_last_update_date() -> Dict[Symbol, datetime.date]: return LastDateHandler._last_update_date # Commitments of Traders data. # NOTE: IMPORTANT: Data order must be ascending (datewise). # Data source: https://commitmentsoftraders.org/cot-data/ # Data description: https://commitmentsoftraders.org/wp-content/uploads/Static/CoTData/file_key.html class CommitmentsOfTradersFutures(PythonData): def GetSource(self, config: SubscriptionDataConfig, date: datetime, isLiveMode: bool) -> SubscriptionDataSource: return SubscriptionDataSource("data.quantpedia.com/backtesting_data/futures/cot/{0}.PRN".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv) # File example. # DATE OPEN HIGH LOW CLOSE VOLUME OI # ---- ---- ---- --- ----- ------ -- # DATE LARGE SPECULATOR COMMERCIAL HEDGER SMALL TRADER # LONG SHORT LONG SHORT LONG SHORT def Reader(self, config: SubscriptionDataConfig, line: str, date: datetime, isLiveMode: bool) -> BaseData: data = CommitmentsOfTradersFutures() data.Symbol = config.Symbol if not line[0].isdigit(): return None split = line.split(',') # Prevent lookahead bias. data.Time = datetime.strptime(split[0], "%Y%m%d") + timedelta(days=1) data['LARGE_SPECULATOR_LONG'] = int(split[1]) data['LARGE_SPECULATOR_SHORT'] = int(split[2]) data['COMMERCIAL_HEDGER_LONG'] = int(split[3]) data['COMMERCIAL_HEDGER_SHORT'] = int(split[4]) data['SMALL_TRADER_LONG'] = int(split[5]) data['SMALL_TRADER_SHORT'] = int(split[6]) data['OPEN_INTEREST'] = int(split[1]) + int(split[2]) + int(split[3]) + int(split[4]) + int(split[5]) + int(split[6]) data.Value = int(split[1]) if config.Symbol.Value not in LastDateHandler._last_update_date: LastDateHandler._last_update_date[config.Symbol.Value] = datetime(1,1,1).date() if data.Time.date() > LastDateHandler._last_update_date[config.Symbol.Value]: LastDateHandler._last_update_date[config.Symbol.Value] = data.Time.date() return data # Commitments of Traders data. # NOTE: IMPORTANT: Data order must be ascending (datewise). # Data source: https://commitmentsoftraders.org/cot-data/ # Data description: https://commitmentsoftraders.org/wp-content/uploads/Static/CoTData/file_key.html class CommitmentsOfTradersCombined(PythonData): def GetSource(self, config: SubscriptionDataConfig, date: datetime, isLiveMode: bool) -> SubscriptionDataSource: return SubscriptionDataSource("data.quantpedia.com/backtesting_data/futures/cot/options_futures_combined/{0}.PRN".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv) # File example. # DATE OPEN HIGH LOW CLOSE VOLUME OI # ---- ---- ---- --- ----- ------ -- # DATE LARGE SPECULATOR COMMERCIAL HEDGER SMALL TRADER # LONG SHORT LONG SHORT LONG SHORT def Reader(self, config: SubscriptionDataConfig, line: str, date: datetime, isLiveMode: bool) -> BaseData: data = CommitmentsOfTradersCombined() data.Symbol = config.Symbol if not line[0].isdigit(): return None split = line.split(',') # Prevent lookahead bias. data.Time = datetime.strptime(split[0], "%Y%m%d") + timedelta(days=1) data['LARGE_SPECULATOR_LONG'] = int(split[1]) data['LARGE_SPECULATOR_SHORT'] = int(split[2]) data['COMMERCIAL_HEDGER_LONG'] = int(split[3]) data['COMMERCIAL_HEDGER_SHORT'] = int(split[4]) data['SMALL_TRADER_LONG'] = int(split[5]) data['SMALL_TRADER_SHORT'] = int(split[6]) data['open_interest'] = int(split[1]) + int(split[2]) + int(split[3]) + int(split[4]) + int(split[5]) + int(split[6]) data.Value = int(split[1]) if config.Symbol.Value not in LastDateHandler._last_update_date: LastDateHandler._last_update_date[config.Symbol.Value] = datetime(1,1,1).date() if data.Time.date() > LastDateHandler._last_update_date[config.Symbol.Value]: LastDateHandler._last_update_date[config.Symbol.Value] = data.Time.date() return data
# region imports from AlgorithmImports import * from abc import abstractmethod, ABC # endregion class DeltaHedgeModule(ABC): def __init__( self, algo: QCAlgorithm, ) -> None: self._algo: QCAlgorithm = algo @abstractmethod def delta_hedge(self, symbol: Symbol) -> None: ... @abstractmethod def _get_delta_hedge_quantity(self, symbol: Symbol) -> float: ...
# https://quantpedia.com/strategies/hedging-pressure-predicts-commodity-option-returns/ # # The investment universe consists of 24 commodity options and their underlying futures contracts. A complete list of traded commodity options is described in # Appendix A. Apply the following filters to the options universe. First, include only the standard option contracts with the same maturity months as the # underlying futures contracts. Second, eliminate option prices that violate no-arbitrage boundary conditions. Third, discard options that have Black's (1976) # implied volatilities less than 1%. Last, remove options in the last week before expiration. Using the data from the Commitments of Traders (COT) reports, # infer hedgers' positions in options in week t that are long the underlying commodity i by taking the futures-and-option-combined long positions of commercial # traders and subtracting their futures-only long position. Analogously, infer hedgers' positions in options that are short the underlying commodity. Finally, # compute the option interest as the futures-and-option-combined open interest minus futures-only open interest. For each commodity i in week t, calculate the # hedging pressure in options (HPO) as the net short option position of the hedgers divided by the option open interest (see equation 1). At the end of each # Tuesday, rank the 24 commodities in ascending order based on their HPO and form five equal-weighted quantile portfolios consisting of 5, 5, 4, 5, 5 commodities, # respectively. The trading rule for the strategy is as follows: at the end of Tuesday of week t, buy (sell) the delta-hedged OTM calls and sell (buy) the # delta-hedged OTM puts for commodities in the highest (lowest) quintile (see equation 5). Categorize an option as out-of-the-money (OTM) if its absolute delta # is between 0.125 and 0.375 and recompute the option's delta weekly, every Tuesday (see equations 2,3,4). Hold the position for four weeks and then rebalance. # # Implementation changes: # - The investment universe consists of 1 commodity options and their underlying commodity ETFs. # - Rebalance is done on monthly basis with one month holding period. # - Only 50% of portfolio value is traded. # region imports from AlgorithmImports import * import data_tools from delta_hedge.etf_options_delta_hedge_module import ETFOptionsDeltaHedgeModule # endregion class HedgingPressurePredictsCommodityOptionReturns(QCAlgorithm): def initialize(self) -> None: self.set_start_date(2010, 1, 1) self.set_cash(1_000_000) symbols: List[str] = [ ('WEAT', 'QW', 'UW'), # Teucrium Wheat Fund ('CORN', 'QC', 'UC'), # Teucrium Corn Fund ('SOYB', 'QS', 'US'), # Teucrium Soybean Fund ('CANE', 'QSB', 'USB'), # Teucrium Sugar Fund ('CPER', 'QHG', 'UHG'), # United States Copper Index Fund ('USO', 'QCL', 'UCL'), # United States Oil Fund LP ('UNG', 'QNG', 'UNG'), # United States Natural Gas Fund LP ('UGA', 'QRB', 'URB'), # United States Gasoline Fund LP ('GLD', 'QGC', 'UGC'), # SPDR Gold Shares ('PALL', 'QPA', 'UPA'), # abrdn Physical Palladium Shares ETF ('PPLT', 'QPL', 'UPL'), # abrdn Physical Platinum Shares ETF # ('IAU',), # iShares Gold Trust # ('BNO',), # United States Brent Oil Fund LP ] seeder: FuncSecuritySeeder = FuncSecuritySeeder(self.get_last_known_prices) self.set_security_initializer(lambda security: seeder.seed_security(security)) self.universe_settings.data_normalization_mode = DataNormalizationMode.RAW self.settings.minimum_order_margin_portfolio_percentage = 0 self.settings.daily_precise_end_time = False # delta hedge module self._delta_hedge_module: ETFOptionsDeltaHedgeModule = ETFOptionsDeltaHedgeModule(self) leverage: int = 5 self._traded_count: int = 2 self._traded_percentage: float = 0.5 self._min_expiry: int = 30 self._max_expiry: int = 3 * 30 self._min_delta: float = 0.125 self._max_delta: float = 0.375 self._symbol_data: Dict[Symbol, data_tools.SymbolData] = {} for ticker, cot_f_symbol, cot_c_symbol in symbols: # COT futures data cot_f_symbol: Symbol = self.add_data(data_tools.CommitmentsOfTradersFutures, cot_f_symbol, Resolution.DAILY).symbol # COT option and futures combined data cot_c_symbol: Symbol = self.add_data(data_tools.CommitmentsOfTradersCombined, cot_c_symbol, Resolution.DAILY).symbol symbol: Security = self.add_equity(ticker, Resolution.DAILY, leverage=leverage).symbol # QC futures self._symbol_data[symbol] = data_tools.SymbolData(cot_f_symbol, cot_c_symbol) self._selection_flag: bool = False self._rebalance_flag: bool = False self.schedule.on( self.date_rules.every(DayOfWeek.WEDNESDAY), self.time_rules.at(0, 0), self._selection ) self._recent_month: int = -1 self._HPO: Dict[Symbol, float] = {} market: Symbol = self.add_equity('SPY', Resolution.DAILY).symbol self.schedule.on( self.date_rules.every_day(market), self.time_rules.at(10, 0), self._delta_hedge ) def _delta_hedge(self) -> None: for etf_symbol in self._symbol_data: self._delta_hedge_module.delta_hedge(etf_symbol) def on_data(self, slice: Slice) -> None: if self._rebalance_flag: self._rebalance_flag = False if len(self._HPO) < self._traded_count: self._HPO.clear() return sorted_by_HPO: List[Symbol] = sorted(self._HPO, key=self._HPO.get, reverse=True) long: List[Symbol] = sorted_by_HPO[:self._traded_count] short: List[Symbol] = sorted_by_HPO[-self._traded_count:] self._HPO.clear() if self.portfolio.invested: return for symbol in long + short: underlying_price: float = self.securities[symbol].ask_price if self._symbol_data[symbol]._call: options_quantity: int = self.portfolio.total_portfolio_value * self._traded_percentage // self._traded_count // (self.securities[self._symbol_data[symbol]._call.symbol].symbol_properties.contract_multiplier * underlying_price) if symbol in long: self.buy(self._symbol_data[symbol]._call.symbol, options_quantity) else: self.sell(self._symbol_data[symbol]._call.symbol, options_quantity) if self._symbol_data[symbol]._put: options_quantity: int = self.portfolio.total_portfolio_value * self._traded_percentage // self._traded_count // (self.securities[self._symbol_data[symbol]._call.symbol].symbol_properties.contract_multiplier * underlying_price) if symbol in long: self.sell(self._symbol_data[symbol]._put.symbol, options_quantity) else: self.buy(self._symbol_data[symbol]._put.symbol, options_quantity) self._symbol_data[symbol].reset_options() if not self._selection_flag: return self._selection_flag = False self.liquidate() for symbol, symbol_data in self._symbol_data.items(): if symbol_data.is_ready: self.remove_option_contract(symbol_data._call.symbol) self.remove_option_contract(symbol_data._put.symbol) custom_data_last_update_date: Dict[str, datetime.date] = data_tools.LastDateHandler.get_last_update_date() for symbol, symbol_data in self._symbol_data.items(): if any([self.securities[cot_symbol].get_last_data() and self.time.date() > custom_data_last_update_date[cot_symbol] for cot_symbol in [symbol_data.cot_c_data_symbol.value, symbol_data.cot_f_data_symbol.value]]): self.liquidate() self.log(f'There is no more COT data for {symbol}') return cot_c_data = self.securities[symbol_data.cot_c_data_symbol].get_last_data() cot_f_data = self.securities[symbol_data.cot_f_data_symbol].get_last_data() if cot_c_data and cot_f_data: # calculate hedging pressure long_options_position: float = cot_c_data.Commercial_Hedger_Long - cot_f_data.Commercial_Hedger_Long short_options_position: float = cot_c_data.Commercial_Hedger_Short - cot_f_data.Commercial_Hedger_Short open_interest: float = cot_c_data.Open_Interest - cot_f_data.Open_Interest if long_options_position != 0 and short_options_position != 0: hpo: float = (short_options_position - long_options_position) / open_interest chain: OptionChain = self.option_chain(symbol) # chain: OptionChain = slice.option_chains.get(symbol_data.option_symbol) if not chain: self.log(f'No option chain found for {symbol_data.option_symbol}') else: contracts: List[OptionContract] = list(map(lambda kvp: kvp.value, chain.contracts)) # get contracts within delta range otm_call_contracts: List[OptionContract] = [ c for c in contracts if c.right == OptionRight.CALL and c.underlying_last_price <= c.strike and self._min_delta <= abs(c.greeks.delta) <= self._max_delta ] otm_put_contracts: List[OptionContract] = [ c for c in contracts if c.right == OptionRight.PUT and c.underlying_last_price >= c.strike and self._min_delta <= abs(c.greeks.delta) <= self._max_delta ] if len(otm_call_contracts) != 0 and len(otm_put_contracts) != 0: otm_call: OptionContract = sorted(otm_call_contracts, \ key = lambda x: x.expiry)[0] otm_put: OptionContract = sorted(otm_put_contracts, \ key = lambda x: x.expiry)[0] symbol_data.update_options( self.add_option_contract(otm_call.symbol), self.add_option_contract(otm_put.symbol) ) if symbol_data.is_ready: self._HPO[symbol] = hpo else: self.log(f'Not enought calls (len: {len(otm_call_contracts)}) or puts (len: {len(otm_put_contracts)}) found for {symbol}') self._rebalance_flag = True def _selection(self) -> None: # self._selection_flag = True if self._recent_month != self.time.month: self._selection_flag = True self._recent_month = self.time.month # Custom fee model class CustomFeeModel(FeeModel): def GetOrderFee(self, parameters: OrderFeeParameters) -> OrderFee: fee: float = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 return OrderFee(CashAmount(fee, "USD"))