Overall Statistics
Total Trades
476
Average Win
1.29%
Average Loss
-1.21%
Compounding Annual Return
8.458%
Drawdown
41.200%
Expectancy
0.460
Net Profit
267.210%
Sharpe Ratio
0.52
Loss Rate
29%
Win Rate
71%
Profit-Loss Ratio
1.06
Alpha
0.076
Beta
-0.937
Annual Standard Deviation
0.122
Annual Variance
0.015
Information Ratio
0.407
Tracking Error
0.122
Treynor Ratio
-0.068
Total Fees
$3506.90
from QuantConnect.Data import SubscriptionDataSource
from QuantConnect.Python import PythonData
from QuantConnect.Algorithm import QCAlgorithm
from QuantConnect.Data.UniverseSelection import *
import decimal as d
import numpy as np
import pandas as pd
import time
from scipy.stats import linregress, zscore
import talib
from datetime import timedelta, date, datetime
import datetime as dt
from pandas.tseries.offsets import BDay

class TechnicalMultiFactorAlgo(QCAlgorithm):

    def Initialize(self):

        self.SetStartDate(2002, 1, 1)  #Set Start Date
        self.SetEndDate(2018, 1, 1)  #Set Start Date

        self.SetCash(100000)           #Set Strategy Cash

        self.UniverseSettings.Resolution = Resolution.Daily
        self.UniverseSettings.Leverage   = 1

        self.max_per_side_assets     = 2 # so 2*N is total assets at L=2

        self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin)
        #self.AddUniverse(self.CoarseSelectionFunction)
        self.AddUniverse(StockDataSource, "sp500", self.stockDataSource)

        self.AddEquity("SPY", Resolution.Minute)

        if self.LiveMode:
            
            self.Schedule.On(self.DateRules.MonthStart("SPY"),
                self.TimeRules.At(9,5),
                Action(self.Downloader))
            
            self.Schedule.On(self.DateRules.MonthStart("SPY"),
                self.TimeRules.At(9,15),
                Action(self.Downloader))

            
            self.Schedule.On(self.DateRules.MonthStart("SPY"),
                self.TimeRules.At(9,25),
                Action(self.Strategy))
                
            self.Schedule.On(self.DateRules.MonthStart("SPY"),
                self.TimeRules.AfterMarketOpen("SPY", 0),
                Action(self.Rebalance))
        else:
            # Note we force shift into market hours, otherwise these run out of order if set before 9:31
            
            for i in range(2):
                self.Schedule.On(self.DateRules.MonthStart("SPY"),
                    self.TimeRules.At(9,25+i),
                    Action(self.Downloader))
            
            self.Schedule.On(self.DateRules.MonthStart("SPY"),
                self.TimeRules.At(9,32),
                Action(self.Strategy))
                
            self.Schedule.On(self.DateRules.MonthStart("SPY"),
                self.TimeRules.At(9,33),
                Action(self.Rebalance))

        self.Schedule.On(self.DateRules.MonthStart("SPY"), \
            self.TimeRules.BeforeMarketClose("SPY", 0), \
            Action(self.Reset_Baskets))
            
        self.universe              = []
        self.Reset_Baskets()

        self.n_hist_items = 253 * 3
        self.SetWarmUp(self.n_hist_items)
        
        self.splotName = 'Strategy Info'
        sPlot = Chart(self.splotName)
        sPlot.AddSeries(Series('Leverage',  SeriesType.Line, 0))
        self.AddChart(sPlot)
        
        self.last_month_fired_coarse    = None #we cannot rely on Day==1 like before
        self.kama = self.KAMA("SPY", 200, Resolution.Daily)
        self.sma = self.SMA("SPY", 5, Resolution.Daily)

    def Reset_Baskets(self):
        
        self.current_symbols_long  = []

        self.split_universe        = []
        self.current_subset        = 0
        self.asset_hist            = {}
        self.dfs                   = {}
        
    def Downloader(self):
        
        self.Log("Downloader %d "%self.current_subset + str(self.Time))
        
        if len(self.split_universe) == 0:
            return
        
        self.dfs = {}
        for symbol in self.split_universe[self.current_subset]:
            self.asset_hist[symbol] = self.History([symbol,], self.n_hist_items, Resolution.Daily).astype(np.float32)
            hist = self.asset_hist[symbol].unstack(level=0)
            hist.index = pd.to_datetime(hist.index)
            if len(hist.index) < self.n_hist_items:
                pass
            else:
                self.dfs[symbol] = hist
        self.current_subset += 1
        
        
    def z_scoring(self,date):
        tickers = list(self.dfs.keys())
        all_factors = FactorCollection().allFactors(list(self.dfs.values()),date,'close')[0]
        z_factors = []
        zf_dict = {}
        for factor in all_factors:  # Calculate factor z-scores => normalize for comparison...
            z_factor = (factor - np.mean(factor)) / np.std(factor)
            z_factors.append(z_factor)
        z_factors = np.array(z_factors)
        where_are_nans = np.isnan(z_factors)
        z_factors[where_are_nans] = 0.
        for i in range(len(tickers)):
            zf_dict[tickers[i]] = np.dot(z_factors.T[i], np.array([0.25,-0.5,0.5,-0.5,0.5,0.5]))
        return zf_dict

       
    def Strategy(self):
        
        self.Log("Strategy " + str(self.Time))
        if len(self.universe) == 0:
            return

        date = pd.to_datetime(self.Time)
        z_dict = self.z_scoring(date)
            
        sorted_stock = sorted(z_dict.items(), key=lambda x: x[1],reverse=True)
        sorted_symbol = [sorted_stock[i][0] for i in xrange(len(sorted_stock))]
        self.current_symbols_long = sorted_symbol[:self.max_per_side_assets]

        for symbol in self.universe:
            if symbol not in self.current_symbols_long:
                self.RemoveSecurity(symbol)
        
    
    def Rebalance(self):
        self.Log("Rebalance " + str(self.Time))
        if self.sma.Current.Value > self.kama.Current.Value:
            if len(self.current_symbols_long) > 0:
                for i in self.Portfolio.Values:
                    if (i.Invested) and (i.Symbol not in self.current_symbols_long):
                        self.Liquidate(i.Symbol)
    
                for sym in self.current_symbols_long:
                    self.SetHoldings(sym, 1./float(len(self.current_symbols_long)))
        else:
            self.Liquidate()

        self.account_leverage = self.Portfolio.TotalAbsoluteHoldingsCost / self.Portfolio.TotalPortfolioValue
        self.Plot(self.splotName,'Leverage', float(self.account_leverage))
        
    def stockDataSource(self, data):
        list = []
        for item in data:
            for symbol in item["Symbols"]:
                symbolString = Symbol.Create(symbol, SecurityType.Equity, Market.USA)
                list.append(symbolString)
        
        result = [x for x in list] 

        self.split_universe = np.array_split(result, 2) 
            
        return result


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

        # we want equal allocation in each security in our universe
        for security in changes.AddedSecurities:
            if security.Symbol not in self.universe:
                self.universe.append(security.Symbol)
                    
    def OnEndOfAlgorithm(self):
        for trade in self.TradeBuilder.ClosedTrades:
            self.Log(str(trade.Symbol)+
                    ", MAE: "+ str(trade.MAE)+
                    ", MFE: "+ str(trade.MFE)+
                    ", EOT Drawdown: "+ str(trade.EndTradeDrawdown)+
                    ", Entry Price: "+str(trade.EntryPrice)+
                    ", Entry Time: "+str(trade.EntryTime)+
                    ", Exit Price: "+str(trade.ExitPrice)+
                    ", Exit Time: "+str(trade.ExitTime)+
                    ", Quantity: "+str(trade.Quantity)+
                    ", Profit Loss: "+str(trade.ProfitLoss)+
                    ", Direction: "+str(trade.Direction)+
                    ", Duration: "+str(trade.Duration)
                    )


class StockDataSource(PythonData):
    
    def GetSource(self, config, date, isLive):
        url = "https://www.dropbox.com/s/nh5e5b51d4rmabj/ETFS_live.csv?dl=1" if isLive else \
                "https://www.dropbox.com/s/1hk2oxeban0lt5u/ETFS.csv?dl=1"
                
        return SubscriptionDataSource(url, SubscriptionTransportMedium.RemoteFile)
    
    def Reader(self, config, line, date, isLive):
        if not (line.strip() and line[0].isdigit()): return None
        
        stocks = StockDataSource()
        stocks.Symbol = config.Symbol
        
        csv = line.split(',')
        if isLive:
            stocks.Time = date
            stocks["Symbols"] = csv
        else:   
            stocks.Time = datetime.strptime(csv[0], "%m/%d/%Y")
            stocks["Symbols"] = csv[1:]
        return stocks      
                    
        
class FactorCollection(object):
    
    def cut_dataframe(self,df,start_date=None,end_date=None):
        all_dates = df.index.values
        if not end_date:
            end_date = all_dates.max()
        if not start_date:
            start_date = all_dates.min()
        cut_df = df.ix[df.index.searchsorted(start_date):(1+df.index.searchsorted(end_date))]
        return cut_df
         
    def slopeWeekly(self,df,date,column_name):
        day = df.index.asof(pd.to_datetime(date))
        cut_df = self.cut_dataframe(df,end_date=day)
        ema = pd.ewma(cut_df[column_name], span=50).ix[-25:]
        ema_dates = ema.index.values
        ema_time_delta_days = pd.Timedelta(ema_dates.max()-ema_dates.min()).total_seconds()/3600/24
        slope = (ema.values[-1] - ema.values[0]) / ema_time_delta_days
        return slope

    def volumentumWeekly(self,df,date,column_name):
        day = df.index.asof(pd.to_datetime(date))
        friday1 = df.index.asof(day - timedelta(days=(day.weekday() - 4) % 7, weeks=0))
        friday2 = df.index.asof(day - timedelta(days=(day.weekday() - 4) % 7, weeks=1))
        one_week = [day - timedelta(i) for i in range(7)]
        six_months = [day - timedelta(i) for i in range(180)]
        avg_week_volume = df.loc[one_week].dropna()[column_name].mean()
        avg_six_months_volume = df.loc[six_months].dropna()[column_name].mean()
        return (df[column_name].loc[friday1] - df[column_name].loc[friday2]) * avg_week_volume / avg_six_months_volume

    def volumentumMonthly(self,df,date,column_name):
        day = df.index.asof(pd.to_datetime(date))
        end_of_month1 = pd.to_datetime(date(day.year,day.month,1)) - timedelta(days=1)
        end_of_month1 = df.index.asof(pd.to_datetime(end_of_month1))
        end_of_month2 = pd.to_datetime(date(end_of_month1.year,end_of_month1.month,1)) - timedelta(days=1)
        end_of_month2 = df.index.asof(pd.to_datetime(end_of_month2))
        one_month = [df.index.asof(day - timedelta(i)) for i in range(30)]
        twelve_months = [df.index.asof(day - timedelta(i)) for i in range(360)]
        avg_monthly_volume = df.loc[one_month].dropna()[column_name].mean()
        avg_twelve_months_volume = df.loc[twelve_months].dropna()[column_name].mean()
        return (df[column_name].loc[end_of_month1] - df[column_name].loc[end_of_month2]) \
               * avg_monthly_volume / avg_twelve_months_volume

    def momentumNMo(self,df,date,column_name,number_of_months):
        end_date = df.index.asof(pd.to_datetime(date))
        start_date = end_date - timedelta(days=30*number_of_months)
        start_date = df.index.asof(start_date)
        cut_df = self.cut_dataframe(df,start_date=start_date,end_date=date)[column_name]
        cut_df_minus1 = self.cut_dataframe(df,start_date=start_date-BDay(1),end_date=end_date-BDay(1))[column_name]
        len_df = len(cut_df)
        len_df_minus1 = len(cut_df_minus1)
        len_min = min(len_df,len_df_minus1)
        daily_returns = cut_df.values[:len_min] / cut_df_minus1.values[:len_min]
        return daily_returns.mean()

    def meanReversion(self,df,date,column_name,n_days,N_days):
        day = df.index.asof(pd.to_datetime(date))
        n_day_range = [df.index.asof(day - timedelta(i)) for i in range(n_days)]
        N_day_range = [df.index.asof(day - timedelta(i)) for i in range(N_days)]
        avg_n_days = df.loc[n_day_range].dropna()[column_name].mean()
        avg_N_days = df.loc[N_day_range].dropna()[column_name].mean()
        return avg_n_days / avg_N_days - 1.0

    def highLowRange(self,df,date,column_name):
        day = df.index.asof(pd.to_datetime(date))
        fifty_two_day_range = [df.index.asof(day - timedelta(i)) for i in range(52*5)]
        fifty_two_week_low = df.loc[fifty_two_day_range].dropna()['low'].min()
        fifty_two_week_high = df.loc[fifty_two_day_range].dropna()['high'].max()
        current_price = df.loc[day][column_name]
        return (current_price - fifty_two_week_low) / (fifty_two_week_high - fifty_two_week_low)

    def moneyFlow(self,df,date):
        day = df.index.asof(pd.to_datetime(date))
        close = df.loc[day]['close']
        low = df.loc[day]['low']
        high = df.loc[day]['high']
        volume = df.loc[day]['volume']
        return (((close - low) - (high - close)) / (high - low)) * volume

    def moneyFlowPersistency(self,df,date,number_of_months):
        day = df.index.asof(pd.to_datetime(date))
        day_range = [df.index.asof(day - timedelta(i)) for i in range(number_of_months*30)]
        money_flows = np.array([self.moneyFlow(df,day1 - BDay(1)) for day1 in day_range])
        signs_of_money_flows = np.sign(money_flows)
        return (signs_of_money_flows[signs_of_money_flows>0]).sum() / (number_of_months*30)

    def slopeDaily(self,df,date,column_name):
        day = df.index.asof(pd.to_datetime(date))
        cut_df = self.cut_dataframe(df,end_date=day)
        ema = pd.ewma(cut_df[column_name], span=10).ix[-5:]
        ema_dates = ema.index.values
        ema_time_delta_days = pd.Timedelta(ema_dates.max()-ema_dates.min()).total_seconds()/3600/24
        slope = (ema.values[-1] - ema.values[0]) / ema_time_delta_days
        return slope

    def slopeMonthly(self,df,date,column_name):
        day = df.index.asof(pd.to_datetime(date))
        cut_df = self.cut_dataframe(df,end_date=day)
        ema = pd.ewma(cut_df[column_name], span=300).ix[-150:]
        ema_dates = ema.index.values
        ema_time_delta_days = pd.Timedelta(ema_dates.max()-ema_dates.min()).total_seconds()/3600/24
        slope = (ema.values[-1] - ema.values[0]) / ema_time_delta_days
        return slope

    def pxRet(self,df,date,column_name, number_of_days):
        day = df.index.asof(pd.to_datetime(date))
        day_minus_n_days = df.index.asof(day - timedelta(days=number_of_days))
        cut_df = self.cut_dataframe(df,end_date=day,start_date=day_minus_n_days)[column_name]
        return (cut_df.values[-1] - cut_df.values[0])/cut_df.values[0]

    def currPxRet(self,df,date,column_name):
        day = df.index.asof(pd.to_datetime(date))
        price = df.loc[day][column_name]
        day_start = df.index.asof(day - timedelta(days=3*360))
        price_mean = self.cut_dataframe(df,start_date=day_start, end_date=day)[column_name].mean()
        return 1.0 - price_mean / price

    def nDayADR(self,df,date,column_name,number_of_days):
        day = df.index.asof(pd.to_datetime(date))
        day_minus_n_days = df.index.asof(day - timedelta(days=number_of_days))
        cut_df = self.cut_dataframe(df,end_date=day,start_date=day_minus_n_days)[column_name]
        cut_df_minus1 = self.cut_dataframe(df,end_date=day,start_date=day_minus_n_days)[column_name]
        return (cut_df.values / cut_df_minus1.values).mean()

    def nDayADP(self,df,date,column_name,number_of_days):
        day = df.index.asof(pd.to_datetime(date))
        day_minus_n_days = df.index.asof(day - timedelta(days=number_of_days))
        cut_df = self.cut_dataframe(df,end_date=day,start_date=day_minus_n_days)[column_name]
        cut_df_minus1 = self.cut_dataframe(df,end_date=day,start_date=day_minus_n_days)[column_name]
        return (cut_df.values - cut_df_minus1.values).mean()

    def pxRet2(self,df,date,column_name,N_days,n_days):
        return -0.5 * self.pxRet(df,date,column_name,N_days) + 0.5 * self.pxRet(df,date,column_name,n_days)

    def currPxRetSlope(self,df,date,column_name):
        return -0.5 * self.currPxRet(df,date,column_name) + 0.5 * self.slopeDaily(df,date,column_name)

    def allFactors(self,df_list,date,column_name):
        """All Factors as an array"""
        f1 = []
        f10 = []
        f12 = []
        f14 = []
        f20 = []
        f28 = []
        for df in df_list:
            if not df.empty:
                f1.append(float(self.slopeWeekly(df, date, column_name)))
                f10.append(float(self.highLowRange(df, date, column_name)))
                f12.append(float(self.moneyFlowPersistency(df, date, 1)))
                f14.append(float(self.moneyFlowPersistency(df, date, 6)))
                f20.append(float(self.pxRet(df, date, column_name, 90)))
                f28.append(float(self.currPxRetSlope(df, date, column_name)))

        factor_names = ['f1', 'f10', 'f12', 'f14', 'f20', 'f28']
                         
        return [f1,f10,f12,f14,f20,f28], factor_names