Overall Statistics
Total Trades
210
Average Win
0.84%
Average Loss
-0.47%
Compounding Annual Return
4.709%
Drawdown
9.600%
Expectancy
0.245
Net Profit
8.004%
Sharpe Ratio
0.402
Probabilistic Sharpe Ratio
16.781%
Loss Rate
56%
Win Rate
44%
Profit-Loss Ratio
1.82
Alpha
0.043
Beta
-0.032
Annual Standard Deviation
0.091
Annual Variance
0.008
Information Ratio
-0.652
Tracking Error
0.243
Treynor Ratio
-1.156
Total Fees
$9338.81
Estimated Strategy Capacity
$18000000000.00
Lowest Capacity Asset
RTY XHYQYCUDLM9T
#region imports
from AlgorithmImports import *
#endregion

class ShortTermReversalWithFutures(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2019, 1, 1)                                 
        self.SetEndDate(2020, 9, 1)                               
        self.SetCash(10000000) 

        self.tickers = [Futures.Currencies.CHF, 
                        Futures.Currencies.GBP, 
                        Futures.Currencies.CAD, 
                        Futures.Currencies.EUR,
                        Futures.Indices.NASDAQ100EMini, 
                        Futures.Indices.Russell2000EMini, 
                        Futures.Indices.SP500EMini, 
                        Futures.Indices.Dow30EMini]
        self.length = len(self.tickers)

        self.symbol_data = {}
        
        for ticker in self.tickers:
            future = self.AddFuture(ticker,
                    resolution = Resolution.Daily,
                    extendedMarketHours = True,
                    dataNormalizationMode = DataNormalizationMode.BackwardsRatio,
                    dataMappingMode = DataMappingMode.OpenInterest,
                    contractDepthOffset = 0
                )
            future.SetLeverage(1)
            self.symbol_data[future.Symbol] = SymbolData(self, future)

    def OnData(self, slice):
        for symbol, symbol_data in self.symbol_data.items():
            # Update SymbolData
            symbol_data.Update(slice)

            # Rollover
            if slice.SymbolChangedEvents.ContainsKey(symbol):
                changed_event = slice.SymbolChangedEvents[symbol]
                old_symbol = changed_event.OldSymbol
                new_symbol = changed_event.NewSymbol
                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.MarketOrder(new_symbol, quantity // self.Securities[new_symbol].SymbolProperties.ContractMultiplier, tag = tag)
        
        # Check if weekly consolidated bars are at their updatest
        if not all([symbol_data.IsReady for symbol_data in self.symbol_data.values()]):
            return
        
        # Flag to avoid undesired rebalance
        for symbol_data in self.symbol_data.values():
            symbol_data._is_volume_ready = False
            symbol_data._is_oi_ready = False
            symbol_data._is_return_ready = False
        
        # 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.VolumeReturn)[:int(self.length*0.5)] +
            sorted(self.symbol_data.values(), key=lambda x: x.OpenInterestReturn)[-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.CalculateOrderQuantity(short_symbol, -0.3)
        multiplier = self.Securities[short_symbol].SymbolProperties.ContractMultiplier
        self.MarketOrder(short_symbol, qty // multiplier)

        qty = self.CalculateOrderQuantity(long_symbol, 0.3)
        multiplier = self.Securities[long_symbol].SymbolProperties.ContractMultiplier
        self.MarketOrder(long_symbol, qty // multiplier)


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.OnVolumeRocUpdated
        self._oi_roc.Updated += self.OnOiRocUpdated
        self._return.Updated += self.OnReturnUpdated

        # 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.DataConsolidated += self.OnTradeBarConsolidated
        self.oi_consolidator.DataConsolidated += 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 IsReady(self):
        return self._volume_roc.IsReady and self._oi_roc.IsReady \
            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 VolumeReturn(self):
        return self._volume_roc.Current.Value

    @property
    def OpenInterestReturn(self):
        return self._oi_roc.Current.Value

    @property
    def Return(self):
        return self._return.Current.Value
    
    def Update(self, slice):
        if slice.Bars.ContainsKey(self.Symbol):
            self.consolidator.Update(slice.Bars[self.Symbol])
            
            oi = OpenInterest(slice.Time, self.Symbol, self._future.OpenInterest)
            self.oi_consolidator.Update(oi)

    def OnVolumeRocUpdated(self, sender, updated):
        self._is_volume_ready = True

    def OnOiRocUpdated(self, sender, updated):
        self._is_oi_ready = True

    def OnReturnUpdated(self, sender, updated):
        self._is_return_ready = True
    
    def OnTradeBarConsolidated(self, sender, bar):
        self._volume_roc.Update(bar.EndTime, bar.Volume)
        self._return.Update(bar.EndTime, 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)