Overall Statistics
Total Orders
719
Average Win
0.26%
Average Loss
-0.18%
Compounding Annual Return
0.837%
Drawdown
5.700%
Expectancy
0.147
Start Equity
1000000
End Equity
1087041.18
Net Profit
8.704%
Sharpe Ratio
-0.251
Sortino Ratio
-0.26
Probabilistic Sharpe Ratio
0.203%
Loss Rate
53%
Win Rate
47%
Profit-Loss Ratio
1.43
Alpha
-0.002
Beta
-0.046
Annual Standard Deviation
0.025
Annual Variance
0.001
Information Ratio
-0.714
Tracking Error
0.13
Treynor Ratio
0.134
Total Fees
$2079.74
Estimated Strategy Capacity
$1800000.00
Lowest Capacity Asset
NG XBLBSKM1RSN5
Portfolio Turnover
0.89%
#region imports
from AlgorithmImports import *
#endregion


class CommodityFutureTrendFollowing(QCAlgorithm):
    '''
    Two Centuries of Trend Following

    This paper demonstrates the existence of anomalous excess returns based on trend following strategies
    across commodities, currencies, stock indices, and bonds, and over very long time scales.

    Reference:
    [1] Y. Lempérière, C. Deremble, P. Seager, M. Potters, J. P. Bouchaud, "Two centuries of trend following", April 15, 2014.
        URL: https://arxiv.org/pdf/1404.3274.pdf
    '''
    def initialize(self):
        self.set_start_date(2010, 1, 1)
        self.set_end_date(2020, 1, 1)
        self.set_cash(1000000)
        self._leverage = 3

        tickers = [Futures.Grains.WHEAT,
                   Futures.Grains.CORN,
                   Futures.Meats.LIVE_CATTLE,
                   Futures.Energies.CRUDE_OIL_WTI,
                   Futures.Energies.NATURAL_GAS,
                   Futures.Softs.SUGAR_11,
                   Futures.Metals.COPPER]

        # Container to store the SymbolData object and rebalancing timer for each security
        self._data = {}
        self._next_rebalance = {}

        for ticker in tickers:
            # subscribe to continuous future data and set desired leverage
            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(self._leverage) 

            # Create a monthly consolidator for each security
            self.consolidate(future.symbol, CalendarType.MONTHLY, self._calendar_handler)

            # Create a SymbolData object for each security to store relevant indicators 
            # and calculate quantity of contracts to Buy/Sell
            self._data[future.symbol] = SymbolData(future)

            # Set monthly rebalance
            self._next_rebalance[future.symbol] = self.time

        # Set decay rate equal to 5 months (150 days) and warm up period
        self.set_warm_up(150)

    def _calendar_handler(self, bar):
        '''
        Event Handler that updates the SymbolData object for each security when a new monthly bar becomes available
        '''
        self._data[bar.symbol].update(bar)

    def on_data(self, data):
        '''
        Buy/Sell security every month
        '''
        if self.is_warming_up:
            return

        for symbol, changed_event in  data.symbol_changed_events.items():
            old_symbol = changed_event.old_symbol
            if self.portfolio[old_symbol].invested:
                self.liquidate(old_symbol)

        for symbol, symbol_data in self._data.items():
            if self.time < self._next_rebalance[symbol] or not data.bars.contains_key(symbol):
                continue
            quantity = self._leverage * symbol_data.quantity // symbol_data.contract_multiplier
            if quantity != 0:
                self.market_order(symbol_data.mapped, quantity)
            
            self._next_rebalance[symbol] = Expiry.end_of_month(self.time)


class SymbolData:
    '''
    Contains the relevant indicators used to calculate number of contracts to Buy/Sell
    '''
    def __init__(self, future):
        self._future = future
        self._ema = ExponentialMovingAverage("MonthEMA", 5)

        # Volatility estimation is defined as the EMA of absolute monthly price changes
        # Use Momentum indicator to get absolute monthly price changes
        # Then use the IndicatorExtensions.EMA and pass the momentum indicator values to get the volatility
        self._mom = Momentum("MonthMOM", 1)
        # Note: self._vol will automatically be updated with self._mom
        self._vol = IndicatorExtensions.EMA(self._mom, 5)

        self.quantity = 0
    
    @property
    def mapped(self):
        return self._future.mapped

    @property
    def contract_multiplier(self):
        return self._future.symbol_properties.contract_multiplier

    def update(self, bar):
        self._ema.update(bar.time, bar.value)
        self._mom.update(bar.time, bar.value)
        
        if self._ema.is_ready and self._vol.is_ready:
            # Equation 1 in [1]
            signal = (bar.value - self._ema.current.value) / self._vol.current.value
            # Equation 2 in [1], assuming price change is the same next step
            self.quantity = np.sign(signal) * self._mom.current.value / abs(self._vol.current.value)

        return self.quantity != 0