Overall Statistics
Total Trades
113
Average Win
0.03%
Average Loss
0.00%
Compounding Annual Return
962.290%
Drawdown
14.600%
Expectancy
17.917
Net Profit
20.328%
Sharpe Ratio
13.822
Probabilistic Sharpe Ratio
86.004%
Loss Rate
3%
Win Rate
97%
Profit-Loss Ratio
18.42
Alpha
3.535
Beta
2.606
Annual Standard Deviation
0.457
Annual Variance
0.208
Information Ratio
13
Tracking Error
0.403
Treynor Ratio
2.422
Total Fees
$0.00
import numpy as np
# ----------------------------------------------------------
STOCKS = ['QQQ', 'FDN']; BONDS = ['TLT', 'TLH']; 
VOLA = 126; BASE_RET = 83; RET = 252; EXCL = 21; LEV = 1.00; 
# ----------------------------------------------------------

class DualMomentumInOut(QCAlgorithm):

    def Initialize(self):

        self.SetStartDate(2010, 2, 5)
        self.SetEndDate(2010, 3, 5)
        self.cap = 100000
        
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(
            self.SelectCoarse, self.SelectFine
        )
        
        self.BND1 = self.AddEquity('TLT', Resolution.Hour).Symbol
        self.BND2 = self.AddEquity('TLH', Resolution.Hour).Symbol

        self.SLV = self.AddEquity('SLV', Resolution.Daily).Symbol  
        self.GLD = self.AddEquity('GLD', Resolution.Daily).Symbol  
        self.XLI = self.AddEquity('XLI', Resolution.Daily).Symbol 
        self.XLU = self.AddEquity('XLU', Resolution.Daily).Symbol
        self.DBB = self.AddEquity('DBB', Resolution.Daily).Symbol  
        self.UUP = self.AddEquity('UUP', Resolution.Daily).Symbol  
        self.MKT = self.AddEquity('SPY', Resolution.Daily).Symbol          

        self.pairs = [self.SLV, self.GLD, self.XLI, self.XLU, self.DBB, self.UUP]
        
        self.bull = 1        
        self.count = 0 
        self.outday = 0        
        self.wt = {}
        self.real_wt = {}
        self.mkt = []
        self.SetWarmUp(timedelta(350))
        
        self.selected_bond = self.BND1

        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen('SPY', 100),
            self.calculate_signal)
           
            
        symbols = [self.MKT] + self.pairs
        for symbol in symbols:
            self.consolidator = TradeBarConsolidator(timedelta(days=1))
            self.consolidator.DataConsolidated += self.consolidation_handler
            self.SubscriptionManager.AddConsolidator(symbol, self.consolidator)
        
        self.history = self.History(symbols, VOLA + 1, Resolution.Daily)
        if self.history.empty or 'close' not in self.history.columns:
            return
        self.history = self.history['close'].unstack(level=0).dropna()

    def SelectCoarse(self, coarse):
        dollarvolume_array = np.array([ x.DollarVolume for x in coarse ])
        dollarvolume_percentile = np.percentile(dollarvolume_array, 75)
        # Select top 25% volume
        coarse = [x.Symbol for x in coarse if x.DollarVolume >= dollarvolume_percentile]
        return coarse

    def SelectFine(self, fine):
        # Select top 5% historical Returns over
        fine = [self.add_returns_to_security(s, RET, EXCL) for s in fine]
        returns = np.array([s.returns for s in fine])
        nan_mask = np.all(np.isnan(returns), axis=0)
        returns_p95 = np.percentile(returns[~nan_mask], 95)
        # Get top 10 momentum
        fine = sorted([s for s in fine], key=lambda s: s.returns, reverse=True)
        self.longs = [s.Symbol for s in fine if s.returns >= returns_p95][:10]
        return self.longs
        
    def OnSecuritiesChanged(self, changes):
        if self.IsWarmingUp:
            return
        # Subscribe to minute bars for added equities
        for instrument in changes.AddedSecurities:
            self.AddEquity(instrument.Symbol, Resolution.Minute)
            self.Securities[instrument.Symbol].FeeModel = ConstantFeeModel(0.0)
        # Remove equities
        for instrument in changes.RemovedSecurities:
            self.RemoveSecurity(instrument.Symbol)

    def consolidation_handler(self, sender, consolidated):
        self.history.loc[consolidated.EndTime, consolidated.Symbol] = consolidated.Close
        self.history = self.history.iloc[-(VOLA + 1):]  

    def add_returns_to_security(self, sec, period, excl):
        sec.returns = self.returns(sec.Symbol, period, excl)
        return sec
   
    def returns(self, symbol, period, excl):
        prices = self.History(symbol, TimeSpan.FromDays(period + excl), Resolution.Daily)
        if len(prices.index) < excl:
            return 0
        return prices['close'].iloc[-excl] / prices['close'].iloc[0]
        
        
    def calculate_signal(self):
        vola = self.history[[self.MKT]].pct_change().std() * np.sqrt(252)
        wait_days = int(vola * BASE_RET)
        period = int((1.0 - vola) * BASE_RET)        
        r = self.history.pct_change(period).iloc[-1]

        exit = ((r[self.SLV] < r[self.GLD]) and (r[self.XLI] < r[self.XLU]) and  (r[self.DBB] < r[self.UUP]))

        if exit:
            self.bull = False
            self.outday = self.count
        if self.count >= self.outday + wait_days:
            self.bull = True
        self.count += 1


        if self.returns(self.BND1, RET, EXCL) < self.returns(self.BND2, RET, EXCL):
            self.selected_bond = self.BND2
            
        elif self.returns(self.BND1, RET, EXCL) > self.returns(self.BND2, RET, EXCL):
            self.selected_bond = self.BND1
    
        if not self.bull:
            self.trade([self.selected_bond])

        elif self.bull:
            self.trade(self.longs)    

                    
    def trade(self, new_securities):
        existing = [sym for sym, sec in self.Securities.items() if sec.Invested]
        self.Debug("long: {}, rebalancing into {}".format(self.bull, new_securities))
        for security in set(existing+new_securities):
            if security not in new_securities:
                self.Liquidate(security)
            else:
                self.SetHoldings(security, LEV/len(new_securities))

    def OnEndOfDay(self):
        self.Plot('Security Count', 'Invested', len([sym for sym, sec in self.Securities.items() if sec.Invested]))
        self.Plot('Security Count', 'Universe', len(self.ActiveSecurities))
        self.Plot('Security Count', 'Longs', len(self.longs))
        account_leverage = self.Portfolio.TotalAbsoluteHoldingsCost / self.Portfolio.TotalPortfolioValue
        self.Plot('Leverage', 'Total Account', account_leverage)