“该策略交易10种能源期货,使用七个基于风格的多空子投资组合,标准化信号,按信号值加权头寸,等权组合投资组合,并每月重新平衡以获得多元化回报。”

I. 策略概要

该策略交易10种美国交易所上市的能源期货,包括原油、天然气和电力。前端合约持有至到期前一个月,然后展期至次近月合约。根据七种风格创建七个多空子投资组合:展期收益、对冲压力、投机压力、过往表现、价值、流动性和偏度。每种风格的信号(详见表1)在子投资组合内通过减去均值并除以标准差进行标准化。具有正信号的能源期货做多,具有负信号的做空,头寸按信号值加权。最终投资组合等权组合七个子投资组合,并每月重新平衡,确保风格多元化。

II. 策略合理性

首先,最终策略中使用的所有风格都被广泛认为是期货的成功预测因子。当投资者依赖于聚合各种投资风格信息的复合信号时,他能够更可靠地预测随后的价格变化,从而更有能力捕捉能源期货市场中存在的风险溢价。

最后,本研究的发现对各种稳健性检验都是稳健的;在考虑交易成本、集成投资组合的替代规范、数据窥探检验和经济子周期分析后,它们仍然成立。

III. 来源论文

Capturing Energy Risk Premia [点击查看论文]

<摘要>

本文研究了能源期货风险溢价,可以通过多空投资组合来提取这些溢价,这些投资组合利用合约在各种特征或信号以及其整合方面的异质性。投资者可以通过利用与对冲者净头寸和展期收益特征相关的能源期货合约风险,分别每年赚取约8%和12%的可观溢价,这与对冲压力假说和储存理论的预测一致。同时利用各种信号进行风格整合,并采用替代加权方案,进一步提高了溢价。特别是,等权所有信号的风格整合投资组合表现最为出色。研究结果对交易成本、数据挖掘和子周期分析具有稳健性。

IV. 回测表现

年化回报12.38%
波动率13.75%
β值0.053
夏普比率0.9
索提诺比率-0.164
最大回撤-22.32%
胜率16%

V. 完整的 Python 代码

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"))

发表评论

了解 Quant Buffet 的更多信息

立即订阅以继续阅读并访问完整档案。

继续阅读