Overall Statistics
Total Trades
379
Average Win
1.03%
Average Loss
-0.83%
Compounding Annual Return
12.481%
Drawdown
24.400%
Expectancy
0.531
Net Profit
119.162%
Sharpe Ratio
0.797
Loss Rate
31%
Win Rate
69%
Profit-Loss Ratio
1.23
Alpha
0.355
Beta
-12.562
Annual Standard Deviation
0.152
Annual Variance
0.023
Information Ratio
0.675
Tracking Error
0.152
Treynor Ratio
-0.01
Total Fees
$613.73
from math import ceil
import numpy as np
import pandas as pd
import scipy as sp

### <summary>
### Demonstration of how to estimate constituents of QC500 index based on the company fundamentals
### The algorithm creates a default tradable and liquid universe containing 500 US equities
### which are chosen at the first trading day of each month.
### </summary>
class ConstituentsQC500GeneratorAlgorithm(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(2012, 1, 1) #Set Start Date
        self.SetEndDate(2018, 9, 1) #Set End Date
        self.SetCash(100000) #Set Strategy Cash
        self.UniverseSettings.Resolution = Resolution.Daily

        # this add universe method accepts two parameters:
        # - coarse selection function: accepts an IEnumerable<CoarseFundamental> and returns an IEnumerable<Symbol>
        # - fine selection function: accepts an IEnumerable<FineFundamental> and returns an IEnumerable<Symbol>
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)

        self.spy = self.AddEquity("SPY", Resolution.Daily)
        self.Schedule.On(self.DateRules.MonthStart("SPY"), self.TimeRules.At(0, 0), self.monthly_rebalance)
        self.num_coarse = 1000
        self.num_fine = 500
        self.num_short_term = 60
        self.num_long_term = 30
        self.dollar_volume = {}
        self.rebalance = True
        self.ready = False
        self.symbolDataDict = {}
        
        self.short_term_indicator = 252 * 3
        self.long_term_indicator = 252 * 10

    def CoarseSelectionFunction(self, coarse):
        if not self.rebalance: return []
        # The stocks must have fundamental data
        # The stock must have positive previous-day close price
        # The stock must have positive volume on the previous trading day
        filtered = [x for x in coarse if x.HasFundamentalData
                                      and x.Volume > 0
                                      and x.Price > 0]
        # sort the stocks by dollar volume and take the top 1000
        sort_filtered = sorted(filtered, key=lambda x: x.DollarVolume, reverse=True)[:self.num_coarse]
        for i in sort_filtered:
            self.dollar_volume[i.Symbol.Value] = i.DollarVolume

        # return the symbol objects our sorted collection
        return [x.Symbol for x in sort_filtered]

    def FineSelectionFunction(self, fine):
        if not self.rebalance: return []
        # The company's headquarter must in the U.S.
        # The stock must be traded on either the NYSE or NASDAQ
        # At least half a year since its initial public offering
        # The stock's market cap must be greater than 500 million
        filtered_fine = [x for x in fine if  (x.CompanyReference.CountryId == "USA")
                                        and (x.CompanyReference.PrimaryExchangeID == "NYS" or x.CompanyReference.PrimaryExchangeID == "NAS")
                                        and ((self.Time - x.SecurityReference.IPODate).days > 180)
                                        and x.EarningReports.BasicAverageShares.ThreeMonths * (x.EarningReports.BasicEPS.TwelveMonths*x.ValuationRatios.PERatio) > 5e8]

        count = len(filtered_fine)
        if count == 0: return []
        
        # select stocks with top dollar volume in every single sector
        for i in filtered_fine:
            i.DollarVolume = self.dollar_volume[i.Symbol.Value]
        percent = float(self.num_fine/count)
        group_by_code = {}
        top_list = []
        for code in ["N", "M", "U", "T", "B", "I"]:
            group_by_code[code] = list(filter(lambda x: x.CompanyReference.IndustryTemplateCode == code, filtered_fine))
            top = sorted(group_by_code[code], key=lambda x: x.DollarVolume, reverse = True)[:ceil(len(group_by_code[code])*percent)]
            top_list.append(top)
        joined_list = top_list[0]
        for ls in top_list[1:]:
            joined_list  += ls
        self.symbols = [x.Symbol for x in joined_list][:self.num_fine]
        self.Log(",".join(sorted(i.Value for i in self.symbols)))
        self.ready = True
        return self.symbols

    def OnData(self, data):
        pass

    def monthly_rebalance(self):
        now = self.Time
        if now.month == 1:
            self.rebalance = True
            self.ready = False
        
    def OnData(self, data):
        if self.ready:
            self.ready = False
            self.rebalance = False

            for symbol, symbolData in self.symbolDataDict.items():
                # update the indicator value for securities already in the portfolio
                if symbol not in self.addedSymbols:
                    symbolData.MOMST.Update(IndicatorDataPoint(symbol, self.Time, self.Securities[symbol].Close))
                # liquidate removed securities
                if symbol in self.removedSymbols:
                    self.Liquidate(symbol)
                
            self.addedSymbols = []
            self.removedSymbols = []
            
            sorted_symbolData = sorted(self.symbolDataDict, key=lambda x: self.symbolDataDict[x].MOMST.Current.Value)
            long_stocks = sorted_symbolData[:60]
            
            for symbol in long_stocks:
                self.symbolDataDict[symbol].MOMLT.Update(IndicatorDataPoint(symbol, self.Time, self.Securities[symbol].Close))
            
            sorted_symbolData2 = sorted(long_stocks, key=lambda x: self.symbolDataDict[x].MOMLT.Current.Value)
            buy_stocks = sorted_symbolData2[:30]

            for long_stock in buy_stocks:
                self.SetHoldings(long_stock, 1/len(buy_stocks))
            stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested]
            # liquidate stocks not in the list 
            for i in stocks_invested:
                if i not in buy_stocks:
                    self.Liquidate(i)


    def OnSecuritiesChanged(self, changes):
        # clean up data for removed securities
        self.removedSymbols = [x.Symbol for x in changes.RemovedSecurities]
        for removed in changes.RemovedSecurities:
            symbolData = self.symbolDataDict.pop(removed.Symbol, None)
        # warm up the indicator with history price for newly added securities
        self.addedSymbols = [x.Symbol for x in changes.AddedSecurities if x.Symbol.Value != "SPY"]
        history = self.History(self.addedSymbols, self.long_term_indicator+1, Resolution.Daily)

        for symbol in self.addedSymbols:
            if symbol not in self.symbolDataDict.keys():
                symbolData = SymbolData(symbol, self.short_term_indicator, self.long_term_indicator)
                self.symbolDataDict[symbol] = symbolData
                if str(symbol) in history.index:             
                    symbolData.WarmUpIndicator(history.loc[str(symbol)])
            
class SymbolData:
    '''Contains data specific to a symbol required by this model'''
    
    def __init__(self, symbol, lookback, lookbacklong):
        self.symbol = symbol
        self.MOMST = Momentum(lookback)
        self.MOMLT = Momentum(lookbacklong)

    def WarmUpIndicator(self, history):
        # warm up the Momentum indicator with the history request
        for tuple in history.itertuples():
            item = IndicatorDataPoint(self.symbol, tuple.Index, float(tuple.close))
            self.MOMST.Update(item) 
            self.MOMLT.Update(item)