Overall Statistics
Total Trades
1746
Average Win
0.35%
Average Loss
-0.47%
Compounding Annual Return
3.991%
Drawdown
36.900%
Expectancy
0.050
Net Profit
29.059%
Sharpe Ratio
0.289
Probabilistic Sharpe Ratio
3.192%
Loss Rate
40%
Win Rate
60%
Profit-Loss Ratio
0.76
Alpha
0
Beta
0
Annual Standard Deviation
0.254
Annual Variance
0.065
Information Ratio
0.289
Tracking Error
0.254
Treynor Ratio
0
Total Fees
$9530.14
Estimated Strategy Capacity
$1800000.00
Lowest Capacity Asset
FUV WO2JC60VYY5H
import numpy as np  
import pandas as pd
from itertools import groupby
from math import ceil

class CalmAsparagusAnt(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2015, 3, 11)  # Set Start Date
        self.SetCash(1000000)  # Set Strategy Cash
        self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin)
        ief = self.AddEquity("IEF", Resolution.Daily).Symbol
        tlt = self.AddEquity("TLT", Resolution.Daily).Symbol
        self.UniverseSettings.Resolution = Resolution.Daily
        self.BONDS = [ief, tlt]
        self.TARGET_SECURITIES = 25  
        self.TOP_ROE_QTY = 100 #First sort by ROE
        self.numberOfSymbolsCoarse = 3000
        self.numberOfSymbolsFine = 1500
        self.dollarVolumeBySymbol = {}
        self.activeUniverse = []
        self.lastMonth = -1
        self.trend_up = False
        
        # This is for the trend following filter  
        self.SPY = self.AddEquity("SPY", Resolution.Daily).Symbol
        
        self.TF_LOOKBACK = 126  
        self.TF_CURRENT_LOOKBACK = 20
        
        history = self.History(self.SPY, self.TF_LOOKBACK, Resolution.Daily)
        self.spy_ma50_slice = self.SMA(self.SPY, self.TF_CURRENT_LOOKBACK, Resolution.Daily)
        self.spy_ma200_slice = self.SMA(self.SPY, self.TF_LOOKBACK, Resolution.Daily)
        for tuple in history.loc[self.SPY].itertuples():
            self.spy_ma50_slice.Update(tuple.Index, tuple.close)
            self.spy_ma200_slice.Update(tuple.Index, tuple.close)    
        
        self.trend_up = self.spy_ma50_slice.Current.Value > self.spy_ma200_slice.Current.Value
    
        # This is for the determining momentum  
        self.MOMENTUM_LOOKBACK_DAYS = 126 #Momentum lookback  
        self.MOMENTUM_SKIP_DAYS = 2  
    
        # Initialize any other variables before being used  
        self.stock_weights = pd.Series()  
        self.bond_weights = pd.Series()
    
        # Should probably comment out the slippage and using the default  
        # set_slippage(slippage.FixedSlippage(spread = 0.0))  
        # Create and attach pipeline for fetching all data  
        # Schedule functions  
        # Separate the stock selection from the execution for flexibility  
        
        self.Schedule.On(self.DateRules.MonthEnd("SPY", 0),
                 self.TimeRules.BeforeMarketClose(self.SPY, 0),       
                 self.SelectStocksAndSetWeights)
 
        self.Schedule.On(self.DateRules.EveryDay("SPY"),
                 self.TimeRules.BeforeMarketClose(self.SPY, 0),       
                 self.RecordVars)
                 
                 
        self.lastMonth = -1
        self.AddUniverse(self.Coarse, self.Fine)

    def OnData(self, data):
        pass
    
    def SelectStocksAndSetWeights(self):
        # Get pipeline output and select stocks  
        #df = algo.pipeline_output('pipeline')  
        ''' Pick a new Universe ?????? every week '''
        
        current_holdings = [x.Key for x in self.Portfolio if x.Value.Invested]
        # Define our rule to open/hold positions  
        # top momentum and don't open in a downturn but, if held, then keep  
        rule = 'top_quality_momentum & (trend_up or (not trend_up & index in @current_holdings))'  
        stocks_to_hold = self.activeUniverse
        self.trend_up = self.spy_ma50_slice.Current.Value > self.spy_ma200_slice.Current.Value
        if self.trend_up == False:
            return
        # Set desired stock weights  
        # Equally weight  
        stock_weight = 1.0 / (self.TARGET_SECURITIES)  
        self.weights = {}
        for x in stocks_to_hold:
            self.weights[x] = stock_weight
        # Set desired bond weight  
        # Open bond position to fill unused portfolio balance  
        # But always have at least 1 'share' of bonds  
        ### bond_weight = max(1.0 - context.stock_weights.sum(), stock_weight) / len(context.BONDS)  
        bond_weight = (1.0 - stock_weight) / len(self.BONDS)  
        for x in self.BONDS:
            self.weights[x] = bond_weight
        self.Trade()
    
    def Trade(self):
        makeInvestments = sorted([x for x in self.weights.keys()], key = lambda x: self.weights[x], reverse = False)
        total = 0
        for stock in makeInvestments:
            weight = self.weights[stock]
            if weight == 0:
                self.Liquidate(stock)
            else:
                self.SetHoldings(stock, weight)
            total += weight
        self.Debug(total)
    
    def RecordVars(self):
        pass
    
    def OnSecuritiesChanged(self, changes):
        for security in changes.AddedSecurities:
            symbol = security.Symbol
            if symbol == self.SPY or symbol in self.BONDS:
                continue
            if symbol not in self.activeUniverse:
                self.activeUniverse.append(symbol)
            
        for security in changes.RemovedSecurities:
            symbol = security.Symbol
            self.Liquidate(symbol)
            if symbol in self.activeUniverse:
                self.activeUniverse.remove(symbol)

    def Coarse(self, coarse):
        if self.lastMonth == self.Time.month:
            return Universe.Unchanged
        
        sortedByDollarVolume = sorted([x for x in coarse if x.HasFundamentalData and x.Volume > 0 and x.Price > 0],
                                     key = lambda x: x.DollarVolume, reverse=True)[:self.numberOfSymbolsCoarse]

        self.dollarVolumeBySymbol = {x.Symbol:x.DollarVolume for x in sortedByDollarVolume}

        # If no security has met the QC500 criteria, the universe is unchanged.
        # A new selection will be attempted on the next trading day as self.lastMonth is not updated
        if len(self.dollarVolumeBySymbol) == 0:
            return Universe.Unchanged

        # return the symbol objects our sorted collection
        return list(self.dollarVolumeBySymbol.keys())
    
    def Fine(self, fine):
        sortedBySector = sorted([x for x in fine if x.CompanyReference.CountryId == "USA"
                                        and x.CompanyReference.PrimaryExchangeID in ["NYS","NAS"]
                                        and (self.Time - x.SecurityReference.IPODate).days > 180
                                        and x.MarketCap > 5e8],
                               key = lambda x: x.CompanyReference.IndustryTemplateCode)

        count = len(sortedBySector)

        # If no security has met the QC500 criteria, the universe is unchanged.
        # A new selection will be attempted on the next trading day as self.lastMonth is not updated
        if count == 0:
            return Universe.Unchanged

        # Update self.lastMonth after all QC500 criteria checks passed
        self.lastMonth = self.Time.month

        percent = self.numberOfSymbolsFine / count
        sortedByDollarVolume = []

        # select stocks with top dollar volume in every single sector
        for code, g in groupby(sortedBySector, lambda x: x.CompanyReference.IndustryTemplateCode):
            y = sorted(g, key = lambda x: self.dollarVolumeBySymbol[x.Symbol], reverse = True)
            c = ceil(len(y) * percent)
            sortedByDollarVolume.extend(y[:c])

        sortedByDollarVolume = sorted(sortedByDollarVolume, key = lambda x: self.dollarVolumeBySymbol[x.Symbol], reverse=True)
        Q1500US = [x for x in sortedByDollarVolume[:self.numberOfSymbolsFine]]
        
        df = {}
        stocks = []
        df["Stocks"] = []
        df["Cash Return"] = []
        df["FcfYield"] = []
        df["ROIC"] = []
        df["LtdToEq"] = []
        for x in Q1500US:
            symbol = x.Symbol
            stocks.append(symbol)
            df["Stocks"].append(symbol)
            df["Cash Return"].append(x.ValuationRatios.CashReturn)
            df["FcfYield"].append(x.ValuationRatios.FCFYield)
            df["ROIC"].append(x.OperationRatios.ROIC.ThreeMonths)
            df["LtdToEq"].append(x.OperationRatios.LongTermDebtEquityRatio.ThreeMonths)
        df = pd.DataFrame.from_dict(df)
        df["Cash Return"] = df["Cash Return"].rank()
        df["FcfYield"] = df["FcfYield"].rank()
        df["ROIC"] = df["ROIC"].rank()
        df["LtdToEq"] = df["LtdToEq"].rank()
        df["Value"] = (df["Cash Return"] + df["FcfYield"]).rank()
        df["Quality"] = df["ROIC"] + df["LtdToEq"] + df["Value"]
        
        top_quality = df.sort_values(by=["Quality"])
        top_quality = top_quality["Stocks"][:self.TOP_ROE_QTY]
        
        topStocks = [x for x in top_quality]
        history = self.History(topStocks, self.MOMENTUM_LOOKBACK_DAYS+self.MOMENTUM_SKIP_DAYS, Resolution.Daily)
        returns_overall = {}
        returns_recent = {}
        for symbol in topStocks:
            mompLong = MomentumPercent(self.MOMENTUM_LOOKBACK_DAYS+self.MOMENTUM_SKIP_DAYS)
            mompShort = MomentumPercent(self.MOMENTUM_SKIP_DAYS)
            for tuple in history.loc[symbol].itertuples():
                mompLong.Update(tuple.Index, tuple.close)
                mompShort.Update(tuple.Index, tuple.close)
            returns_overall[symbol] = mompLong.Current.Value
            returns_recent[symbol] = mompShort.Current.Value
        
        final = sorted(topStocks, key = lambda x: returns_overall[x] - returns_overall[x], reverse = True)
        final = final[:self.TARGET_SECURITIES]
        return final