Introduction

The short-term reversal strategy buys stocks which are past losers and sells stocks which are past winners. It is commonly used in the Equity market. This algorithm will explore the reversal effect in the Futures market. Research also suggests that trading volume contains information about Future market movements. The algorithm will be constructed with both the volume and return reversal effect.

Method

The investment universe consists of 8 CME continuous Futures: 4 currencies, 4 financial Indices. The continuous contracts are mapped based on open interest, while data is subscribed in daily resolution.

The algorithm uses a weekly time frame (Wednesday-Wednesday interval). We constructed a SymbolData class to hold weekly consolidators for volume, open interest and price that consolidate every Wednesday 00:00. To compare the weekly change of volume and open interest, we create a RateOfChange indicator that uses consolidated data to update for each feature, and set up flag variables to indicate if they are up-to-date in which are handled by Updated handler. Other information like Mapped contract and update methods are also held within the class object.

  1. def initialize(self):
  2. self.tickers = [Futures.currencies.CHF,
  3. Futures.currencies.GBP,
  4. Futures.currencies.CAD,
  5. Futures.currencies.EUR,
  6. Futures.indices.n_a_s_d_a_q100_e_mini,
  7. Futures.indices.russell2000_e_mini,
  8. Futures.indices.SP500E_MINI,
  9. Futures.indices.dow30_e_mini]
  10. self.length = len(self.tickers)
  11. self.symbol_data = {}
  12. for ticker in self.tickers:
  13. future = self.add_future(ticker,
  14. resolution = Resolution.DAILY,
  15. extended_market_hours = True,
  16. data_normalization_mode = DataNormalizationMode.BACKWARDS_RATIO,
  17. data_mapping_mode = DataMappingMode.OPEN_INTEREST,
  18. contract_depth_offset = 0
  19. )
  20. future.set_leverage(1)
  21. self.symbol_data[future.symbol] = SymbolData(self, future)
  22. class SymbolData:
  23. def __init__(self, algorithm, future):
  24. self._future = future
  25. self.symbol = future.symbol
  26. self._is_volume_ready = False
  27. self._is_oi_ready = False
  28. self._is_return_ready = False
  29. # create ROC(1) indicator to get the volume and open interest return, and handler to update state
  30. self._volume_roc = RateOfChange(1)
  31. self._oi_roc = RateOfChange(1)
  32. self._return = RateOfChange(1)
  33. self._volume_roc.updated += self.on_volume_roc_updated
  34. self._oi_roc.updated += self.on_oi_roc_updated
  35. self._return.updated += self.on_return_updated
  36. # Create the consolidator with the consolidation period method, and handler to update ROC indicators
  37. self.consolidator = TradeBarConsolidator(self.consolidation_period)
  38. self.oi_consolidator = OpenInterestConsolidator(self.consolidation_period)
  39. self.consolidator.data_consolidated += self.on_trade_bar_consolidated
  40. self.oi_consolidator.data_consolidated += lambda sender, oi: self._oi_roc.update(oi.time, oi.value)
  41. # warm up
  42. history = algorithm.history[TradeBar](future.symbol, 14, Resolution.DAILY)
  43. oi_history = algorithm.history[OpenInterest](future.symbol, 14, Resolution.DAILY)
  44. for bar, oi in zip(history, oi_history):
  45. self.consolidator.update(bar)
  46. self.oi_consolidator.update(oi)
  47. @property
  48. def is_ready(self):
  49. return self._volume_roc.is_ready and self._oi_roc.is_ready \
  50. and self._is_volume_ready and self._is_oi_ready and self._is_return_ready
  51. @property
  52. def mapped(self):
  53. return self._future.mapped
  54. @property
  55. def volume_return(self):
  56. return self._volume_roc.current.value
  57. @property
  58. def open_interest_return(self):
  59. return self._oi_roc.current.value
  60. @property
  61. def return(self):
  62. return self._return.current.value
  63. def update(self, slice):
  64. if slice.bars.contains_key(self.symbol):
  65. self.consolidator.update(slice.bars[self.symbol])
  66. oi = OpenInterest(slice.time, self.symbol, self._future.open_interest)
  67. self.oi_consolidator.update(oi)
  68. def on_volume_roc_updated(self, sender, updated):
  69. self._is_volume_ready = True
  70. def on_oi_roc_updated(self, sender, updated):
  71. self._is_oi_ready = True
  72. def on_return_updated(self, sender, updated):
  73. self._is_return_ready = True
  74. def on_trade_bar_consolidated(self, sender, bar):
  75. self._volume_roc.update(bar.end_time, bar.volume)
  76. self._return.update(bar.end_time, bar.close)
  77. # Define a consolidation period method
  78. def consolidation_period(self, dt):
  79. period = timedelta(7)
  80. dt = dt.replace(hour=0, minute=0, second=0, microsecond=0)
  81. weekday = dt.weekday()
  82. if weekday > 2:
  83. delta = weekday - 2
  84. elif weekday < 2:
  85. delta = weekday + 5
  86. else:
  87. delta = 0
  88. start = dt - timedelta(delta)
  89. return CalendarInfo(start, period)
+ Expand

We'll update each consolidators of SymbolData with Slice data in the OnData event handler, as well as rolling over behavior of mapped continuous future contracts.

  1. def on_data(self, slice):
  2. for symbol, symbol_data in self.symbol_data.items():
  3. # Update SymbolData
  4. symbol_data.update(slice)
  5. # Rollover
  6. if slice.symbol_changed_events.contains_key(symbol):
  7. changed_event = slice.symbol_changed_events[symbol]
  8. old_symbol = changed_event.old_symbol
  9. new_symbol = changed_event.new_symbol
  10. tag = f"Rollover - Symbol changed at {self.time}: {old_symbol} -> {new_symbol}"
  11. quantity = self.portfolio[old_symbol].quantity
  12. # Rolling over: to liquidate any position of the old mapped contract and switch to the newly mapped contract
  13. self.liquidate(old_symbol, tag = tag)
  14. self.market_order(new_symbol, quantity // self.securities[new_symbol].symbol_properties.contract_multiplier, tag = tag)

Then, all contracts are also assigned to either high-open interest (top 50% of changes in open interest) or low-open interest groups (bottom 50% of changes in open interest) based on lagged changes in open interest between the period from \(t-1\) to \(t\) and the period from \(t-2\) to \(t-1\).

We take the intersection of the top volume group and bottom open interest group. The trading candidates are selected from this group. Next, the Futures in the intersection are sorted based on the return in the previous week. The algorithm goes long on Futures from the high-volume, low-open interest group with the lowest return and shorts the contract with the highest return.

  1. # Select stocks with most weekly extreme return out of lowest volume change and highest OI change
  2. trade_group = set(sorted(self.symbol_data.values(), key=lambda x: x.volume_return)[:int(self.length*0.5)] +
  3. sorted(self.symbol_data.values(), key=lambda x: x.open_interest_return)[-int(self.length*0.5):])
  4. sorted_by_returns = sorted(trade_group, key=lambda x: x.return)
  5. short_symbol = sorted_by_returns[-1].mapped
  6. long_symbol = sorted_by_returns[0].mapped
  7. for symbol in self.portfolio.keys:
  8. if self.portfolio[symbol].invested and symbol not in [short_symbol, long_symbol]:
  9. self.liquidate(symbol)
  10. # Adjust for contract mulitplier for order size
  11. qty = self.calculate_order_quantity(short_symbol, -0.3)
  12. multiplier = self.securities[short_symbol].symbol_properties.contract_multiplier
  13. self.market_order(short_symbol, qty // multiplier)
  14. qty = self.calculate_order_quantity(long_symbol, 0.3)
  15. multiplier = self.securities[long_symbol].symbol_properties.contract_multiplier
  16. self.market_order(long_symbol, qty // multiplier)
+ Expand


Reference

  1. Quantpedia - Short Term Reversal with Futures

Author

Jing Wu

August 2018