Overall Statistics |
Total Orders
4281
Average Win
0.81%
Average Loss
-0.71%
Compounding Annual Return
3.514%
Drawdown
47.600%
Expectancy
0.073
Start Equity
100000
End Equity
236620.88
Net Profit
136.621%
Sharpe Ratio
0.083
Sortino Ratio
0.102
Probabilistic Sharpe Ratio
0.000%
Loss Rate
50%
Win Rate
50%
Profit-Loss Ratio
1.14
Alpha
0.012
Beta
-0.018
Annual Standard Deviation
0.133
Annual Variance
0.018
Information Ratio
-0.157
Tracking Error
0.209
Treynor Ratio
-0.609
Total Fees
$2022.15
Estimated Strategy Capacity
$0
Lowest Capacity Asset
CME_PA1.QuantpediaFutures 2S
Portfolio Turnover
3.26%
|
# https://quantpedia.com/strategies/return-asymmetry-effect-in-commodity-futures/ # # The investment universe consists of 22 commodity futures, namely: # soybean oil, corn, cocoa, cotton, feeder cattle, gold, copper, heating oil, coffee, live cattle, lean hogs, # natural gas, oats, orange juice, palladium, platinum, soybean, sugar, silver, soybean meal, wheat, and crude oil. # Firstly, at the beginning of each month, construct the asymmetry measure (IE) for each commodity based on the latest 260 daily returns using the following formula # (the formula originally consists of theoretical density and integrals, however the solution is simple when empirical distribution is utilized): # IE = (number of trading days when the daily return is greater than the average plus two standard deviations) – # (number of trading days when the daily return is smaller than the average minus two standard deviations). # Then rank the commodities according to their IE. # Buy the bottom seven commodities with the lowest IE in the previous month and sell the top seven commodities with the highest IE in the previous month. # Weigh the portfolio equally and rebalance monthly. # # QC Implementation: # - Universe consists of Quantpedia comodity futures. # - Buying bottom 7 commodities and selling top 7 commodities according to IE. from AlgorithmImports import * class ReturnAsymmetryEffectInCommodityFutures(QCAlgorithm): def Initialize(self): self.SetStartDate(2000, 1, 1) self.SetCash(100000) self.tickers = [ "CME_S1", # Soybean Futures, Continuous Contract "CME_W1", # Wheat Futures, Continuous Contract "CME_SM1", # Soybean Meal Futures, Continuous Contract "CME_BO1", # Soybean Oil Futures, Continuous Contract "CME_C1", # Corn Futures, Continuous Contract "CME_O1", # Oats Futures, Continuous Contract "CME_LC1", # Live Cattle Futures, Continuous Contract "CME_FC1", # Feeder Cattle Futures, Continuous Contract "CME_LN1", # Lean Hog Futures, Continuous Contract "CME_GC1", # Gold Futures, Continuous Contract "CME_SI1", # Silver Futures, Continuous Contract "CME_PL1", # Platinum Futures, Continuous Contract "CME_CL1", # Crude Oil Futures, Continuous Contract "CME_HG1", # Copper Futures, Continuous Contract "CME_LB1", # Random Length Lumber Futures, Continuous Contract # "CME_NG1", # Natural Gas (Henry Hub) Physical Futures, Continuous Contract "CME_PA1", # Palladium Futures, Continuous Contract "CME_RR1", # Rough Rice Futures, Continuous Contract "CME_RB2", # Gasoline Futures, Continuous Contract "CME_KW2", # Wheat Kansas, Continuous Contract "ICE_CC1", # Cocoa Futures, Continuous Contract "ICE_CT1", # Cotton No. 2 Futures, Continuous Contract "ICE_KC1", # Coffee C Futures, Continuous Contract "ICE_O1", # Heating Oil Futures, Continuous Contract "ICE_OJ1", # Orange Juice Futures, Continuous Contract "ICE_SB1" # Sugar No. 11 Futures, Continuous Contract "ICE_RS1", # Canola Futures, Continuous Contract "ICE_GO1", # Gas Oil Futures, Continuous Contract "ICE_WT1", # WTI Crude Futures, Continuous Contract ] self.data = {} # storing objects of SymbolData class keyed by comodity symbols self.period = 261 # need 261 daily prices, to calculate 260 daily returns self.buy_count = 7 # buy n comodities on each rebalance self.sell_count = 7 # sell n comodities on each rebalance self.symbol = self.AddEquity("SPY", Resolution.Daily).Symbol # subscribe to futures contracts for ticker in self.tickers: security = self.AddData(QuantpediaFutures, ticker, Resolution.Daily) security.SetFeeModel(CustomFeeModel()) security.SetLeverage(5) self.data[security.Symbol] = SymbolData(self.period) self.settings.daily_precise_end_time = False self.settings.minimum_order_margin_portfolio_percentage = 0. self.rebalance_flag = False self.Schedule.On(self.DateRules.MonthStart(self.symbol), self.TimeRules.BeforeMarketClose(self.symbol, 0), self.Rebalance) def OnData(self, data): # update daily closes for symbol in self.data: if self.securities[symbol].get_last_data() and self.time.date() > QuantpediaFutures.get_last_update_date()[symbol]: self.liquidate() return if symbol in data and data[symbol]: close = data[symbol].Value self.data[symbol].update_closes(close) # rebalance monthly if not self.rebalance_flag: return self.rebalance_flag = False IE = {} for symbol, symbol_obj in self.data.items(): # check if comodity has ready prices if not symbol_obj.is_ready(): continue # calculate IE IE_value = symbol_obj.calculate_IE() # store IE value under comodity symbol IE[symbol] = IE_value # make sure, there are enough comodities for rebalance if len(IE) < (self.buy_count + self.sell_count): return # sort commodities based on IE values sorted_by_IE = [x[0] for x in sorted(IE.items(), key=lambda item: item[1])] # select long and short parts long = sorted_by_IE[:self.buy_count] short = sorted_by_IE[-self.sell_count:] # trade execution invested = [x.Key for x in self.Portfolio if x.Value.Invested] for symbol in invested: if symbol not in long + short: self.Liquidate(symbol) for symbol in long: self.SetHoldings(symbol, 1 / self.buy_count) for symbol in short: self.SetHoldings(symbol, -1 / self.sell_count) def Rebalance(self): self.rebalance_flag = True class SymbolData(): def __init__(self, period): self.closes = RollingWindow[float](period) def update_closes(self, close): self.closes.Add(close) def is_ready(self): return self.closes.IsReady def calculate_IE(self): closes = np.array([x for x in self.closes]) daily_returns = (closes[:-1] - closes[1:]) / closes[1:] average_daily_returns = np.average(daily_returns) two_daily_returns_std = 2 * np.std(daily_returns) avg_plus_two_std = average_daily_returns + two_daily_returns_std avg_minus_two_std = average_daily_returns - two_daily_returns_std over_avg_plus_two_std = 0 # counting number of daily returns, which were over avg_plus_two_std under_avg_minus_two_std = 0 # counting number of daily returns, which were under avg_minus_two_std for daily_return in daily_returns: if daily_return > avg_plus_two_std: over_avg_plus_two_std += 1 elif daily_return < avg_minus_two_std: under_avg_minus_two_std += 1 IE_value = over_avg_plus_two_std - under_avg_minus_two_std return IE_value # Quantpedia data. # NOTE: IMPORTANT: Data order must be ascending (datewise) class QuantpediaFutures(PythonData): _last_update_date:Dict[Symbol, datetime.date] = {} @staticmethod def get_last_update_date() -> Dict[Symbol, datetime.date]: return QuantpediaFutures._last_update_date def GetSource(self, config, date, isLiveMode): return SubscriptionDataSource("data.quantpedia.com/backtesting_data/futures/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv) def Reader(self, config, line, date, isLiveMode): data = QuantpediaFutures() data.Symbol = config.Symbol if not line[0].isdigit(): return None split = line.split(';') data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1) data['back_adjusted'] = float(split[1]) data['spliced'] = float(split[2]) data.Value = float(split[1]) if config.Symbol not in QuantpediaFutures._last_update_date: QuantpediaFutures._last_update_date[config.Symbol] = datetime(1,1,1).date() if data.Time.date() > QuantpediaFutures._last_update_date[config.Symbol]: QuantpediaFutures._last_update_date[config.Symbol] = data.Time.date() return data # Custom fee model. class CustomFeeModel(FeeModel): def GetOrderFee(self, parameters): fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 return OrderFee(CashAmount(fee, "USD"))