构建一个商品期货交易池。每月根据上一年的表现按五分位排名。做多动量表现最强的五分之一,做空表现最弱的五分之一。每月重新平衡。

策略概述

建立一个由商品期货构成的交易池。根据过去12个月的表现将每种商品期货分为五个部分(五分位)。对动量表现最强的五分之一商品期货进行做多,对表现最弱的五分之一进行做空。每月根据最新的表现数据对投资组合进行调整和重新平衡。该策略旨在利用商品价格的周期性和趋势性,通过动量效应在商品期货市场中获取潜在收益。

策略合理性

商品动量回报与期货市场是否处于现货升水(价格随时间下降)或期货升水(价格随时间上升)有关,倾向于购买现货升水的合约和卖出期货升水的合约。这表明交易最极端的现货升水和期货升水合约是有利可图的,这与凯恩斯和希克斯的正常现货升水理论相一致。然而,这些动量收益并不被视为对风险的补偿。此外,商品动量回报与传统资产类别的相关性较低,表明其在多元化投资组合中的潜在价值。

Switzer和Jiang指出,商品期货中的动量利润与行为金融学理论一致,即市场参与者对信息反应不足导致市场效率低下。该策略的成功还依赖于市场期限结构和对冲压力等因素的影响。此外,市场流动性、专注于31种商品以及较低的交易成本进一步支持了这一策略的成功。

论文来源

Momentum in Commodity Futures Markets [点击浏览原文]

<摘要>

本文测试了商品期货价格中的短期延续和长期反转效应。虽然逆势策略不起作用,但文章确定了13种有利可图的动量策略,每年平均回报率为9.38%。对动量策略推荐交易的商品期货进行的更深入分析表明,我们买入现货升水的合约并卖出期货升水的高波动性合约。研究还发现,动量回报与传统资产类别的回报之间的相关性较低,这使基于商品的相对强度组合成为多元化投资组合的极佳选择。

回测表现

年化收益率14.6%
波动率25.6%
Beta-0.104
夏普比率0.047
索提诺比率0.053
最大回撤76.7%
胜率57%

完整python代码

from AlgorithmImports import *

class CommodityMomentumStrategy(QCAlgorithm):
    '''
    This class defines a commodity momentum trading strategy.
    It initializes trading settings, selects commodities based on momentum,
    and manages positions accordingly.
    '''

    def Initialize(self):
        '''Initializes the trading algorithm settings, including start date, initial cash,
        commodity tickers to trade, analysis period, and momentum indicators for each commodity.'''
        
        self.SetStartDate(2000, 1, 1)  # Set the start date for the algorithm
        self.SetCash(100000)  # Set the initial cash for the algorithm

        # Define the commodity tickers that will be traded
        self.commodity_tickers = [
            "CME_S1", "CME_W1", "CME_SM1", "CME_BO1", "CME_C1", "CME_O1",
            "CME_LC1", "CME_FC1", "CME_LN1", "CME_GC1", "CME_SI1", "CME_PL1",
            "CME_CL1", "CME_HG1", "CME_LB1", "CME_NG1", "CME_PA1", "CME_RR1",
            "CME_DA1", "ICE_RS1", "ICE_GO1", "CME_RB2", "CME_KW2", "ICE_WT1",
            "ICE_CC1", "ICE_CT1", "ICE_KC1", "ICE_O1", "ICE_OJ1", "ICE_SB1",
        ]

        self.analysis_period = 252  # Approx. 12 months of trading days
        self.SetWarmUp(self.analysis_period, Resolution.Daily)  # Set warm-up period for indicators
        self.momentum_indicator = {}  # Dictionary to store momentum indicators for each commodity

        # Initialize momentum indicators for each commodity ticker
        for ticker in self.commodity_tickers:
            commodity = self.AddData(QuantpediaFutures, ticker, Resolution.Daily)  # Add commodity data
            commodity.SetFeeModel(StandardFeeModel())  # Set fee model for the commodity
            commodity.SetLeverage(5)  # Set leverage for the commodity
            
            # Add a Rate of Change (ROC) indicator for the commodity based on the analysis period
            self.momentum_indicator[commodity.Symbol] = self.ROC(ticker, self.analysis_period, Resolution.Daily)

        self.previous_month = -1  # Variable to track the last processed month

    def OnData(self, data: Slice):
        '''Processes new data and manages portfolio positions based on momentum strategy.'''
        
        if self.IsWarmingUp:  # Check if the algorithm is still in the warm-up period
            return

        if self.Time.month == self.previous_month:  # Check if this month has already been processed
            return
        self.previous_month = self.Time.month  # Update the last processed month

        # Calculate the performance (momentum) for each commodity and filter out the ones not ready
        performance = {symbol: indicator.Current.Value for symbol, indicator in self.momentum_indicator.items() if indicator.IsReady and symbol in data}

        if len(performance) < 5:  # Ensure there are enough commodities to form a strategy
            return

        # Sort commodities based on their performance in descending order
        sorted_commodities = sorted(performance.items(), key=lambda x: x[1], reverse=True)
        quintile_size = len(sorted_commodities) // 5  # Determine the size of a quintile
        long_positions = [symbol for symbol, _ in sorted_commodities[:quintile_size]]  # Commodities to go long
        short_positions = [symbol for symbol, _ in sorted_commodities[-quintile_size:]]  # Commodities to go short

        # Liquidate positions that are not in the current long or short lists
        for symbol in self.Portfolio:
            if symbol not in long_positions + short_positions:
                self.Liquidate(symbol)

        # Allocate equal weight to each long position
        for symbol in long_positions:
            self.SetHoldings(symbol, 1 / len(long_positions))
        
        # Allocate equal weight to each short position, inversely
        for symbol in short_positions:
            self.SetHoldings(symbol, -1 / len(short_positions))

class QuantpediaFutures(PythonData):
    '''
    This class is responsible for importing commodity futures data
    from a specified source.
    '''

    def GetSource(self, config, date, isLiveMode):
        '''Defines the data source for each commodity futures data.'''
        
        source = f"data.quantpedia.com/backtesting_data/futures/{config.Symbol.Value}.csv"  # Data source URL
        return SubscriptionDataSource(source, SubscriptionTransportMedium.RemoteFile)

    def Reader(self, config, line, date, isLiveMode):
        '''Parses each line of the data source and returns a populated data object.'''
        if not line.startswith('20'):  # Ignore lines that do not start with a year
            return None

        data = self.__class__()  # Create a new instance of the data class
        try:
            
            # Split the line by semicolon and assign values
            data.Time, back_adjusted, spliced = line.split(';')
            
            # Parse the date and adjust it by one day
            data.Time = datetime.strptime(data.Time, '%Y-%m-%d') + timedelta(days=1)
            data.Value = float(back_adjusted)  # Set the data value
            data.Symbol = config.Symbol  # Set the data symbol
        except ValueError:
            return None

        return data

class StandardFeeModel(FeeModel):
    '''
    This class defines a standard fee model for transactions.
    '''

    def GetOrderFee(self, parameters):
        '''Calculates the transaction fee based on the order details.'''
        
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005  # Calculate the fee
        return OrderFee(CashAmount(fee, "USD"))  # Return the fee as an OrderFee object

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading