Overall Statistics
Total Trades
690
Average Win
0.03%
Average Loss
-0.02%
Compounding Annual Return
45.151%
Drawdown
1.200%
Expectancy
0.314
Net Profit
3.110%
Sharpe Ratio
3.755
Probabilistic Sharpe Ratio
82.145%
Loss Rate
41%
Win Rate
59%
Profit-Loss Ratio
1.22
Alpha
0.29
Beta
-0.759
Annual Standard Deviation
0.08
Annual Variance
0.006
Information Ratio
2.475
Tracking Error
0.127
Treynor Ratio
-0.395
Total Fees
$1911.61
import pandas as pd
import numpy as np
from collections import deque

class EmaCrossUniverseSelectionAlgorithm(QCAlgorithm):

    def Initialize(self): 
        '''
        Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm.
        All algorithms must initialized.
        '''

        self.SetStartDate(2017,6,1)  #Set Start Date
        self.SetEndDate(2017,6,30)    #Set End Date
        self.SetCash(1000000)           #Set Strategy Cash

        # ADJUST RESOLUTION TO CHANGE ORDER TYPES
        self.UniverseSettings.Resolution = Resolution.Daily
        self.UniverseSettings.Leverage = 2

        self.coarse_count = 100
        self.averages = {}
        self.TOLERANCE = 0.025  # +/- target weight
        
        # this add universe method accepts two parameters:
        # - coarse selection function: accepts an IEnumerable<CoarseFundamental> and returns an IEnumerable<Symbol>
        self.AddUniverse(self.CoarseSelectionFunction)
        
        self.symbols = None
        self.spy = self.AddEquity("SPY", Resolution.Minute).Symbol 
        
        self.ema_fast_window = 2
        self.ema_slow_window = 10
        
        self.LOOKBACK = 11 # 60
        self.equity_arr = deque([0], maxlen=252)
     
    #======================================================================    
        # schedule rebalance
        
        # make buy list
        self.Schedule.On(
            #self.DateRules.EveryDay(self.spy),
            #self.DateRules.MonthStart(self.spy),
            self.DateRules.WeekStart(self.spy),
            self.TimeRules.AfterMarketOpen(self.spy, 30),
            Action(self.rebalance),
        )    
    #======================================================================
    # REF: https://www.quantconnect.com/docs/algorithm-reference/universes#Universes-Coarse-Universe-Selection
    
    def CoarseSelectionFunction(self, coarse):
        """
        This is the coarse selection function which filters the stocks. 
        
        This may be split between coarse and fine selection but does 
        not seem necessary at this point.
        See REF: https://www.quantconnect.com/docs/algorithm-reference/universes#Universes-Fundamentals-Selection
        """
        
        # -------------------------------------------------
        # only trade stocks that have fundamental data
        # price greater than $5 USD and
        # Dollar Volume greater than $10MM USD
        # Can be adjusted however you want, see the above reference.
        
        filterCoarse = [x for x in coarse if x.HasFundamentalData and x.Price > 5 and x.DollarVolume > 10000000]        
        
        # -------------------------------------------------
        # Add SymbolData to self.averages dict keyed by Symbol
        # Call history for new symbols to warm up the SymbolData internals 
        
        tmp_symbols = [cf.Symbol for cf in filterCoarse if cf.Symbol not in self.averages]
        # need more than longest indicator data to compute rolling 
        history = self.History(tmp_symbols, self.LOOKBACK, Resolution.Daily) 
        if not history.empty:
            history = history.close.unstack(level=0)

            for symbol in tmp_symbols:
                if str(symbol) in history.columns:
                    self.averages[symbol] = SymbolData(self, history[symbol])

        # Updates the SymbolData object with current EOD price
        for cf in filterCoarse:
            avg = self.averages.get(cf.Symbol, None)
            if avg is not None:
                avg.Update(cf) # add prices by passing the cf object 
        
        # --------------------------------------------------
        # sort dict by mmi metric and convert to dataframe
        
        mmiDict = {symbol:value.Metric for symbol, value in self.averages.items()}
        # want to get the lowest values so remove the reveres=True flag
        # sort and convert to df
        sorted_by_metric = pd.DataFrame(
            sorted(mmiDict.items(), key=lambda x: x[1]), columns=['symbol','metric']
        ).iloc[:self.coarse_count] 
        # we need the Symbol.Value to compare symbols
        sorted_by_metric_symbols = [x.Value for x in sorted_by_metric["symbol"]]
        #self.Log(f"[{self.Time}] sorted by metric | shape {sorted_by_metric.shape}:\n{sorted_by_metric}")
        
        # --------------------------------------------------
        # extract symbols that are in uptrend
        
        #ema_uptrend = list(filter(lambda x: x.is_uptrend, self.averages.values()))
        # self.Debug(f"[{self.Time}] uptrend syms: {len(ema_uptrend_sym)} |\n {ema_uptrend_sym}")
                
        # --------------------------------------------------
        # extract symbols from uptrending list
        
        # This is the official symbol list. It contains the QC
        # symbol object 
        #symbols = [x.symbol for x in ema_uptrend if x.symbol.Value in sorted_by_metric_symbols]
        symbols = [x for x in sorted_by_metric["symbol"]]
        self.Debug(f"[{self.Time}] number of selected symbols: {len(symbols)}")
        #if len(symbols) > 0:
        #    self.Debug(f"{self.Time} type: {type(symbols)} |\n{symbols}")

        # --------------------------------------------------
        # assign symbols to global parameter so rebalancing
        #   can be handled in OnSecuritiesChanged() function
        self.symbols = symbols 
        return symbols # we need to return only the symbol objects

    #======================================================================
    # scheduled rebalance
    
    def update_portfolio_value(self):
        """
        Update portfolio equity value tracked by self.equity_arr by appending portfolio value to list
        """
        self.equity_arr.append(self.Portfolio.TotalPortfolioValue)
        return
    
    def check_current_weight(self, symbol):
        """
        Check current symbol portfolio weight

        :param symbol: str
        :return current_weight: float
        """
        P = self.Portfolio
        current_weight = float(P[symbol].HoldingsValue) / float(P.TotalPortfolioValue)
        return current_weight    
    
    def rebalance(self):
        
        if self.symbols is None:
            return
        
        self.Debug(f"[{self.Time}] init rebalance")
        
        # columns = []
        prices_list = []
        for k,v in self.averages.items():
            tmp_hist = self.averages[k].Window
            prices_list.append(tmp_hist)
            
        self.Log(f"{self.Time} series list:\n{str(prices_list[0])}")    
        self.Log(f"{self.Time} series list:\n{str(prices_list)}")            
        prices = pd.concat(prices_list, axis=1).dropna()
        
        returns = np.log(prices).diff().dropna()
        self.Log(f"{self.Time} returns:\n{returns.head()}")
        
        #self.update_prices()  # update prices
        self.update_portfolio_value()  # update portfolio value
        
        for sec in self.symbols:
            # get current weights
            current_weight = self.check_current_weight(sec)
            target_weight = 1/len(self.symbols) # * returns[sec].iloc[-1]

            # if current weights outside of tolerance send new orders
            tol = self.TOLERANCE * target_weight
            lower_bound = target_weight - tol
            upper_bound = target_weight + tol

            if (current_weight < lower_bound) or (current_weight > upper_bound):
                self.SetHoldings(sec, target_weight)
                
        return

    #======================================================================
    # this event fires whenever we have changes to our universe
    
    def OnSecuritiesChanged(self, changes):
        
        # liquidate removed securities
        for security in changes.RemovedSecurities:
            if security.Invested:
                self.Liquidate(security.Symbol)
            
    def OnData(self, data):
        pass

#======================================================================

class SymbolData(object):
    """
    This is the symbol data class. I have updated it to be able to 
    take advantage of what the MMI indicator is used for and that is
    trending securities. In this example I implemented a simple
    short term EMA crossover strategy, but it is easy enough to
    change.
    """
    def __init__(self, qccontext, history):

        self.FastPeriod= qccontext.ema_fast_window #30
        self.SlowPeriod= qccontext.ema_slow_window #90
        #self.MainPeriod=90
        self.Window = history
        self.EndTime = history.index[-1]
        
        # ema crossover attributes
        self.is_uptrend = False
        #self.ema_fast = ExponentialMovingAverage(8)
        #self.ema_slow = ExponentialMovingAverage(21)
        self.tolerance = 1.0 # QC example is 1.01
        
        # other key attributes
        self.dollar_volume = None
        self.symbol = None
        self.qc = qccontext
        

    def Update(self, cf):
        """
        Update the indicators for symbol.
        
        This function updates the symbol and dollar volume attribute
        which are needed for filtering, and also updates the history
        dataframe and the EMA crossover indicators and attributes.
        """
        
        # assign symbol attribute and dollar volume
        self.symbol = cf.Symbol
        self.dollar_volume = cf.DollarVolume
        
        if self.EndTime >= cf.EndTime:
            return
        
        # Convert CoarseFundamental to pandas.Series
        to_append = pd.Series([cf.AdjustedPrice], name=str(cf.Symbol),
            index=pd.Index([cf.EndTime], name='time'))
            
        self.Window.drop(self.Window.index[0], inplace=True)
        self.Window = pd.concat([self.Window, to_append], ignore_index=True)
        self.EndTime = cf.EndTime

        """# after warmup update ema indicators with current values to determine
        # if they are uptrending
        if self.ema_fast.Update(cf.EndTime, cf.AdjustedPrice) and self.ema_slow.Update(cf.EndTime, cf.AdjustedPrice):
            fast = self.ema_fast.Current.Value
            slow = self.ema_slow.Current.Value
            # self.qc.Debug(f"[{self.qc.Time}] symbol: {cf.Symbol} | fast: {round(self.ema_fast.Current.Value,2)} | slow: {round(self.ema_slow.Current.Value,2)}")
            self.is_uptrend = fast > slow * self.tolerance"""
            
        
    @property
    def Metric(self):
        """
        This computes the MMI value for the security.
        """
        
        # create returns to feed indicator
        returns = self.Window.pct_change().dropna()
        
        # compute the metric
        x = returns.rolling(self.FastPeriod).mean().iloc[-1]
            
        return x # should be single numeric