Overall Statistics
Total Trades
907
Average Win
2.87%
Average Loss
-2.45%
Compounding Annual Return
21.457%
Drawdown
36.300%
Expectancy
0.295
Net Profit
1810.589%
Sharpe Ratio
0.766
Loss Rate
40%
Win Rate
60%
Profit-Loss Ratio
1.17
Alpha
0.308
Beta
-7.079
Annual Standard Deviation
0.25
Annual Variance
0.062
Information Ratio
0.7
Tracking Error
0.25
Treynor Ratio
-0.027
Total Fees
$1726.66
from QuantConnect.Data.UniverseSelection import *
import math
import numpy as np
import pandas as pd
from scipy import stats 


class FundamentalFactorAlgorithm(QCAlgorithm):
    def slope(self,ts):
        """
        Input: Price time series.
        Output: Annualized exponential regression slope, multipl
        """
        x = np.arange(len(ts))
        log_ts = np.log(ts)
        slope, intercept, r_value, p_value, std_err = stats.linregress(x, log_ts)
        annualized_slope = (np.power(np.exp(slope), 250) - 1) * 100
        return annualized_slope * (r_value ** 2)

    def Initialize(self):

        self.SetStartDate(2003, 1, 1)  #Set Start Date
        self.SetEndDate(2018, 3, 1)  #Set Start Date       
        self.SetCash(10000)            #Set Strategy Cash
    
        
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
        self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Cash); 
        self.spy = self.AddEquity("SPY", Resolution.Minute).Symbol 
        self.holding_months = 1
        self.num_screener = 100
        self.num_stocks = 3
        self.formation_days = 200
        self.lowmom = False
        self.month_count = self.holding_months
        self.Schedule.On(self.DateRules.MonthStart("SPY"), self.TimeRules.At(0, 0), Action(self.monthly_rebalance))
        self.Schedule.On(self.DateRules.MonthStart("SPY"), self.TimeRules.At(10, 0), Action(self.rebalance))
        # rebalance the universe selection once a month
        self.rebalence_flag = 0
        # make sure to run the universe selection at the start of the algorithm even it's not the manth start
        self.first_month_trade_flag = 1
        self.trade_flag = 0 
        self.symbols = None
            
        # This version uses the average of two momentum slopes.
        # Want just one? Set them both to the same number.
        self.momentum_window = 60  # first momentum window.
        self.momentum_window2 = 90  # second momentum window
    
        # Limit minimum slope. Keep in mind that shorter momentum windows
        # yield more extreme slope numbers. Adjust one, and you may want
        # to adjust the other.
        self.minimum_momentum = 60  # momentum score cap
        self.exclude_days = 5
        self.size_method = 2
        self.use_bond_etf = True
        self.bond_etf = 'TLT'
 
    def CoarseSelectionFunction(self, coarse):
        if self.rebalence_flag or self.first_month_trade_flag:
            # drop stocks which have no fundamental data or have too low prices
            selected = [x for x in coarse if (x.HasFundamentalData) and (float(x.Price) > 5)]
            # rank the stocks by dollar volume 
            filtered = sorted(selected, key=lambda x: x.DollarVolume, reverse=True) 
    
            return [ x.Symbol for x in filtered[:200]]
        else:
            return self.symbols


    def FineSelectionFunction(self, fine):
      
        if self.rebalence_flag or self.first_month_trade_flag:
            try:
                filtered_fine = [x for x in fine if (x.ValuationRatios.EVToEBITDA > 0) 
                                                    and (x.EarningReports.BasicAverageShares.ThreeMonths > 0) 
                                                    and x.EarningReports.BasicAverageShares.ThreeMonths * (x.EarningReports.BasicEPS.TwelveMonths*x.ValuationRatios.PERatio) > 2e9]
            except:
                filtered_fine = [x for x in fine if (x.ValuationRatios.EVToEBITDA > 0) 
                                                and (x.EarningReports.BasicAverageShares.ThreeMonths > 0)] 

            top = sorted(filtered_fine, key = lambda x: x.ValuationRatios.EVToEBITDA, reverse=True)[:self.num_screener]
            self.symbols = [x.Symbol for x in top]
            
            self.rebalence_flag = 0
            self.first_month_trade_flag = 0
            self.trade_flag = 1
            return self.symbols
        else:
            return self.symbols
    
    def OnData(self, data):
        pass
    
    def monthly_rebalance(self):
        self.rebalence_flag = 1


    def inv_vola_calc(self,ts):
        """
        Input: Price time series.
        Output: Inverse exponential moving average standard deviation. 
        Purpose: Provides inverse vola for use in vola parity position sizing.
        """
        returns = np.log(ts).diff()
        stddev = returns.ewm(halflife=20, ignore_na=True, min_periods=0,
                         adjust=True).std(bias=False).dropna()
        return 1 / stddev.iloc[-1]
   

    def rebalance(self):
        #spy_hist = self.History([self.spy], 120, Resolution.Daily).loc[str(self.spy)]['close']
        #if self.Securities[self.spy].Price < spy_hist.mean():
           # for symbol in self.Portfolio.Keys:
               # if symbol.Value != "TLT":
                    #self.Liquidate()
            #self.AddEquity("TLT")
            #self.SetHoldings("TLT", 1)
            #return
        
        
        # Get data
        hist_window = max(self.momentum_window,
                      self.momentum_window2) + self.exclude_days
        if self.symbols is None: return
        stocks = self.symbols
        self.Debug("Stocks " + str(len(stocks)))
        #hist = self.History(context.security_list, "close", hist_window, "1d")
        hist = self.History(stocks, self.formation_days, Resolution.Daily)
        current = self.History(stocks, 1, Resolution.Minute)
        c_data = hist["close"].unstack(level=0)
        self.Debug("c_data " + str(c_data))
        data_end = -1 * (self.exclude_days + 1 )  # exclude most recent data

        momentum1_start = -1 * (self.momentum_window + self.exclude_days)
        momentum_hist1 = c_data[momentum1_start:data_end]
        momentum2_start = -1 * (self.momentum_window2 + self.exclude_days)
        momentum_hist2 = c_data[momentum2_start:data_end]
    
        # Calculate momentum scores for all stocks.
        momentum_list = momentum_hist1.apply(self.slope)  # Mom Window 1
        self.Debug("momentum_list " + str((momentum_list)))
        momentum_list2 =  momentum_hist2.apply(self.slope)   # Mom Window 2
        # Combine the lists and make average
        momentum_concat = pd.concat((momentum_list, momentum_list2))
        mom_by_row = momentum_concat.groupby(momentum_concat.index)
        mom_means = mom_by_row.mean()
        
        # Sort the momentum list, and we've got ourselves a ranking table.
        ranking_table = mom_means.sort_values(ascending=False)
        self.Debug("ranking_table " + str(len(ranking_table)))
        # Get the top X stocks, based on the setting above. Slice the dictionary.
        # These are the stocks we want to buy.
        buy_list = ranking_table[:self.num_stocks]
        self.Debug(" buy list " + str(len(buy_list)))
        final_buy_list = buy_list[buy_list > self.minimum_momentum] # those who passed minimum slope requirement
        self.Debug("Final buy list " + str(len(final_buy_list))) 
        # Calculate inverse volatility, for position size.
        inv_vola_table = c_data[buy_list.index].apply(self.inv_vola_calc)
        # sum inv.vola for all selected stocks.
        sum_inv_vola = np.sum(inv_vola_table)


        spy_hist_150 = self.History([self.spy], 150, Resolution.Daily).loc[str(self.spy)]['close']
        spy_hist_200 = self.History([self.spy], 200, Resolution.Daily).loc[str(self.spy)]['close']
        if self.Securities[self.spy].Price > (spy_hist_150.mean() + spy_hist_200.mean())/2:
            can_buy = True
        else:
            can_buy = False
       
    
    
        equity_weight = 0.0 # for keeping track of exposure to stocks
        
        # Sell positions no longer wanted.
        for security in self.Portfolio.Keys:
            if (security not in final_buy_list):
                if (security.Value != self.bond_etf):
                    # print 'selling %s' % security
                    #self.Debug("Sell Security " + str(security) )
                    self.SetHoldings(security, 0)
                    
        vola_target_weights = inv_vola_table / sum_inv_vola
 
        for security in final_buy_list.index:
            # allow rebalancing of existing, and new buys if can_buy, i.e. passed trend filter.
            if (security in self.Portfolio.Keys) or (can_buy): 
                if (self.size_method == 1):
                    weight = vola_target_weights[security]
                elif (self.size_method == 2):
                    weight = (0.99 / self.num_stocks)
             
                self.Debug("Number of stocks " + str(self.num_stocks) + " Rebalance security " + str(security) + " with weight " + str(weight)  )
                self.SetHoldings(security, weight)
                equity_weight += weight
        
           
    
        # Fill remaining portfolio with bond ETF
        etf_weight = max(1 - equity_weight, 0.0)
    
        print ('equity exposure should be %s ' % str(equity_weight))
    
        if (self.use_bond_etf):
            self.Debug("Buy ETF" )
            self.AddEquity("TLT")
            self.SetHoldings(self.bond_etf, etf_weight)
           


        #=========================
        #if self.symbols is None: return
        #chosen_df = self.calc_return(self.symbols)
        #chosen_df = chosen_df.iloc[:self.num_stocks]
        
        #self.existing_pos = 0
        #add_symbols = []
        #for symbol in self.Portfolio.Keys:
         #   if symbol.Value == 'SPY': continue
          #  if (symbol.Value not in chosen_df.index):  
           #     self.SetHoldings(symbol, 0)
            #elif (symbol.Value in chosen_df.index): 
             #   self.existing_pos += 1
            
        #weight = 0.99/len(chosen_df)
        #for symbol in chosen_df.index:
         #   self.AddEquity(symbol)
          #  self.Debug("Symbol " + str(symbol) + " Weight " + str(weight))
           # self.SetHoldings(symbol, weight)