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.
def initialize(self):
self.tickers = [Futures.currencies.CHF,
Futures.currencies.GBP,
Futures.currencies.CAD,
Futures.currencies.EUR,
Futures.indices.n_a_s_d_a_q100_e_mini,
Futures.indices.russell2000_e_mini,
Futures.indices.SP500E_MINI,
Futures.indices.dow30_e_mini]
self.length = len(self.tickers)
self.symbol_data = {}
for ticker in self.tickers:
future = self.add_future(ticker,
resolution = Resolution.DAILY,
extended_market_hours = True,
data_normalization_mode = DataNormalizationMode.BACKWARDS_RATIO,
data_mapping_mode = DataMappingMode.OPEN_INTEREST,
contract_depth_offset = 0
)
future.set_leverage(1)
self.symbol_data[future.symbol] = SymbolData(self, future)
class SymbolData:
def __init__(self, algorithm, future):
self._future = future
self.symbol = future.symbol
self._is_volume_ready = False
self._is_oi_ready = False
self._is_return_ready = False
# create ROC(1) indicator to get the volume and open interest return, and handler to update state
self._volume_roc = RateOfChange(1)
self._oi_roc = RateOfChange(1)
self._return = RateOfChange(1)
self._volume_roc.updated += self.on_volume_roc_updated
self._oi_roc.updated += self.on_oi_roc_updated
self._return.updated += self.on_return_updated
# Create the consolidator with the consolidation period method, and handler to update ROC indicators
self.consolidator = TradeBarConsolidator(self.consolidation_period)
self.oi_consolidator = OpenInterestConsolidator(self.consolidation_period)
self.consolidator.data_consolidated += self.on_trade_bar_consolidated
self.oi_consolidator.data_consolidated += lambda sender, oi: self._oi_roc.update(oi.time, oi.value)
# warm up
history = algorithm.history[TradeBar](future.symbol, 14, Resolution.DAILY)
oi_history = algorithm.history[OpenInterest](future.symbol, 14, Resolution.DAILY)
for bar, oi in zip(history, oi_history):
self.consolidator.update(bar)
self.oi_consolidator.update(oi)
@property
def is_ready(self):
return self._volume_roc.is_ready and self._oi_roc.is_ready \
and self._is_volume_ready and self._is_oi_ready and self._is_return_ready
@property
def mapped(self):
return self._future.mapped
@property
def volume_return(self):
return self._volume_roc.current.value
@property
def open_interest_return(self):
return self._oi_roc.current.value
@property
def return(self):
return self._return.current.value
def update(self, slice):
if slice.bars.contains_key(self.symbol):
self.consolidator.update(slice.bars[self.symbol])
oi = OpenInterest(slice.time, self.symbol, self._future.open_interest)
self.oi_consolidator.update(oi)
def on_volume_roc_updated(self, sender, updated):
self._is_volume_ready = True
def on_oi_roc_updated(self, sender, updated):
self._is_oi_ready = True
def on_return_updated(self, sender, updated):
self._is_return_ready = True
def on_trade_bar_consolidated(self, sender, bar):
self._volume_roc.update(bar.end_time, bar.volume)
self._return.update(bar.end_time, bar.close)
# Define a consolidation period method
def consolidation_period(self, dt):
period = timedelta(7)
dt = dt.replace(hour=0, minute=0, second=0, microsecond=0)
weekday = dt.weekday()
if weekday > 2:
delta = weekday - 2
elif weekday < 2:
delta = weekday + 5
else:
delta = 0
start = dt - timedelta(delta)
return CalendarInfo(start, period)
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.
def on_data(self, slice):
for symbol, symbol_data in self.symbol_data.items():
# Update SymbolData
symbol_data.update(slice)
# Rollover
if slice.symbol_changed_events.contains_key(symbol):
changed_event = slice.symbol_changed_events[symbol]
old_symbol = changed_event.old_symbol
new_symbol = changed_event.new_symbol
tag = f"Rollover - Symbol changed at {self.time}: {old_symbol} -> {new_symbol}"
quantity = self.portfolio[old_symbol].quantity
# Rolling over: to liquidate any position of the old mapped contract and switch to the newly mapped contract
self.liquidate(old_symbol, tag = tag)
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.
# Select stocks with most weekly extreme return out of lowest volume change and highest OI change
trade_group = set(sorted(self.symbol_data.values(), key=lambda x: x.volume_return)[:int(self.length*0.5)] +
sorted(self.symbol_data.values(), key=lambda x: x.open_interest_return)[-int(self.length*0.5):])
sorted_by_returns = sorted(trade_group, key=lambda x: x.return)
short_symbol = sorted_by_returns[-1].mapped
long_symbol = sorted_by_returns[0].mapped
for symbol in self.portfolio.keys:
if self.portfolio[symbol].invested and symbol not in [short_symbol, long_symbol]:
self.liquidate(symbol)
# Adjust for contract mulitplier for order size
qty = self.calculate_order_quantity(short_symbol, -0.3)
multiplier = self.securities[short_symbol].symbol_properties.contract_multiplier
self.market_order(short_symbol, qty // multiplier)
qty = self.calculate_order_quantity(long_symbol, 0.3)
multiplier = self.securities[long_symbol].symbol_properties.contract_multiplier
self.market_order(long_symbol, qty // multiplier)
Derek Melchin
See the attached backtest for an updated version of the algorithm with the following changes:
The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.
Jing Wu
The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.
To unlock posting to the community forums please complete at least 30% of Boot Camp.
You can continue your Boot Camp training progress from the terminal. We hope to see you in the community soon!