Overall Statistics
Total Trades
62
Average Win
5.36%
Average Loss
-2.60%
Compounding Annual Return
16.806%
Drawdown
13.300%
Expectancy
1.295
Net Profit
140.199%
Sharpe Ratio
1.218
Probabilistic Sharpe Ratio
64.027%
Loss Rate
25%
Win Rate
75%
Profit-Loss Ratio
2.06
Alpha
0.141
Beta
0.03
Annual Standard Deviation
0.118
Annual Variance
0.014
Information Ratio
0.179
Tracking Error
0.202
Treynor Ratio
4.745
Total Fees
$364.92
import pandas as pd

class ETFSharpeRotationAlgorithm(QCAlgorithm):
    # This strategy was sourced from https://www.rotationinvest.com/blog/etf-rotation-strategy-using-sharpe-ratio
    #
    # Rotating Funds:  SPY, EFA, GLD
    # Cash Filter:     TLT
    # Strategy Rules:  Invest in the top 1 fund based on the trailing 3-month Sharpe Ratio
    #                  If the top fund is below it's 150-day moving average then invest in the bond ETF (TLT)

    def Initialize(self):
        self.SetStartDate(2015, 1, 1)  # Set Start Date
        self.SetCash(100000)  # Set Strategy Cash
        
        trading_days_per_month = 21
        self.sharpe_lookback = 3 * trading_days_per_month
        
        sma_lookback = 150
        lookback = max(self.sharpe_lookback, sma_lookback)
        
        rotation_tickers = ['SPY', 'EFA', 'GLD']
        self.sma_by_rotation_symbol = {}
        self.history_by_symbol = {}
        for ticker in rotation_tickers:
            symbol = self.AddEquity(ticker, Resolution.Daily).Symbol
            self.sma_by_rotation_symbol[symbol] = self.SMA(symbol, sma_lookback, Resolution.Daily)
            
            history = self.History(symbol, lookback, Resolution.Daily).loc[symbol].close
            for time, close in history[-lookback:].iteritems():
                self.sma_by_rotation_symbol[symbol].Update(time, close)
            
            self.history_by_symbol[symbol] = history[-self.sharpe_lookback:]
            
            # Setup a consolidator for sharpe calculation
            consolidator = TradeBarConsolidator(timedelta(1))
            consolidator.DataConsolidated += self.CustomDailyHandler
            self.SubscriptionManager.AddConsolidator(symbol, consolidator)
            
        self.cash_filter_symbol = self.AddEquity("TLT", Resolution.Daily).Symbol
        
        self.month = -1
        
    def CustomDailyHandler(self, sender, consolidated):
        row = pd.Series([consolidated.Close], index=[consolidated.Time])
        appended = self.history_by_symbol[consolidated.Symbol].append(row)
        self.history_by_symbol[consolidated.Symbol] = appended.iloc[-self.sharpe_lookback:]

    def OnData(self, data):

        # Ensure we have data
        for symbol, _ in self.sma_by_rotation_symbol.items():
            if not data.ContainsKey(symbol) or data[symbol] is None:
                return
        if not data.ContainsKey(self.cash_filter_symbol) or data[self.cash_filter_symbol] is None:
            return
        
        # Rebalance monthly
        if data.Time.month == self.month:
            return
        self.month = data.Time.month
        
        # Select the symbol with the highest trailing sharpe ratio
        sharpe_by_symbol = {}
        for symbol, history in self.history_by_symbol.items():
            sharpe = history.pct_change().mean() / history.pct_change().std()
            sharpe_by_symbol[symbol] = sharpe
        top_symbol = sorted(sharpe_by_symbol.items(), key=lambda item: item[1], reverse=True)[0][0]
        
        # If the top_symbol is above it's SMA, invest it in. Otherwise, invest in the cash filter (TLT)
        sma = self.sma_by_rotation_symbol[top_symbol].Current.Value
        if data[symbol].Price >= sma:
            for symbol, _ in self.sma_by_rotation_symbol.items():
                if self.Securities[symbol].Invested and symbol != top_symbol:
                    self.Liquidate(symbol)
            if self.Securities[self.cash_filter_symbol].Invested:
                self.Liquidate(self.cash_filter_symbol)
            
            self.SetHoldings(top_symbol, 1)
        else:
            for symbol, _ in self.sma_by_rotation_symbol.items():
                self.Liquidate(symbol)
            self.SetHoldings(self.cash_filter_symbol, 1)