| 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