| Overall Statistics |
|
Total Trades 907 Average Win 2.87% Average Loss -2.45% Compounding Annual Return 21.457% Drawdown 36.300% Expectancy 0.295 Net Profit 1810.589% Sharpe Ratio 0.766 Loss Rate 40% Win Rate 60% Profit-Loss Ratio 1.17 Alpha 0.308 Beta -7.079 Annual Standard Deviation 0.25 Annual Variance 0.062 Information Ratio 0.7 Tracking Error 0.25 Treynor Ratio -0.027 Total Fees $1726.66 |
from QuantConnect.Data.UniverseSelection import *
import math
import numpy as np
import pandas as pd
from scipy import stats
class FundamentalFactorAlgorithm(QCAlgorithm):
def slope(self,ts):
"""
Input: Price time series.
Output: Annualized exponential regression slope, multipl
"""
x = np.arange(len(ts))
log_ts = np.log(ts)
slope, intercept, r_value, p_value, std_err = stats.linregress(x, log_ts)
annualized_slope = (np.power(np.exp(slope), 250) - 1) * 100
return annualized_slope * (r_value ** 2)
def Initialize(self):
self.SetStartDate(2003, 1, 1) #Set Start Date
self.SetEndDate(2018, 3, 1) #Set Start Date
self.SetCash(10000) #Set Strategy Cash
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Cash);
self.spy = self.AddEquity("SPY", Resolution.Minute).Symbol
self.holding_months = 1
self.num_screener = 100
self.num_stocks = 3
self.formation_days = 200
self.lowmom = False
self.month_count = self.holding_months
self.Schedule.On(self.DateRules.MonthStart("SPY"), self.TimeRules.At(0, 0), Action(self.monthly_rebalance))
self.Schedule.On(self.DateRules.MonthStart("SPY"), self.TimeRules.At(10, 0), Action(self.rebalance))
# rebalance the universe selection once a month
self.rebalence_flag = 0
# make sure to run the universe selection at the start of the algorithm even it's not the manth start
self.first_month_trade_flag = 1
self.trade_flag = 0
self.symbols = None
# This version uses the average of two momentum slopes.
# Want just one? Set them both to the same number.
self.momentum_window = 60 # first momentum window.
self.momentum_window2 = 90 # second momentum window
# Limit minimum slope. Keep in mind that shorter momentum windows
# yield more extreme slope numbers. Adjust one, and you may want
# to adjust the other.
self.minimum_momentum = 60 # momentum score cap
self.exclude_days = 5
self.size_method = 2
self.use_bond_etf = True
self.bond_etf = 'TLT'
def CoarseSelectionFunction(self, coarse):
if self.rebalence_flag or self.first_month_trade_flag:
# drop stocks which have no fundamental data or have too low prices
selected = [x for x in coarse if (x.HasFundamentalData) and (float(x.Price) > 5)]
# rank the stocks by dollar volume
filtered = sorted(selected, key=lambda x: x.DollarVolume, reverse=True)
return [ x.Symbol for x in filtered[:200]]
else:
return self.symbols
def FineSelectionFunction(self, fine):
if self.rebalence_flag or self.first_month_trade_flag:
try:
filtered_fine = [x for x in fine if (x.ValuationRatios.EVToEBITDA > 0)
and (x.EarningReports.BasicAverageShares.ThreeMonths > 0)
and x.EarningReports.BasicAverageShares.ThreeMonths * (x.EarningReports.BasicEPS.TwelveMonths*x.ValuationRatios.PERatio) > 2e9]
except:
filtered_fine = [x for x in fine if (x.ValuationRatios.EVToEBITDA > 0)
and (x.EarningReports.BasicAverageShares.ThreeMonths > 0)]
top = sorted(filtered_fine, key = lambda x: x.ValuationRatios.EVToEBITDA, reverse=True)[:self.num_screener]
self.symbols = [x.Symbol for x in top]
self.rebalence_flag = 0
self.first_month_trade_flag = 0
self.trade_flag = 1
return self.symbols
else:
return self.symbols
def OnData(self, data):
pass
def monthly_rebalance(self):
self.rebalence_flag = 1
def inv_vola_calc(self,ts):
"""
Input: Price time series.
Output: Inverse exponential moving average standard deviation.
Purpose: Provides inverse vola for use in vola parity position sizing.
"""
returns = np.log(ts).diff()
stddev = returns.ewm(halflife=20, ignore_na=True, min_periods=0,
adjust=True).std(bias=False).dropna()
return 1 / stddev.iloc[-1]
def rebalance(self):
#spy_hist = self.History([self.spy], 120, Resolution.Daily).loc[str(self.spy)]['close']
#if self.Securities[self.spy].Price < spy_hist.mean():
# for symbol in self.Portfolio.Keys:
# if symbol.Value != "TLT":
#self.Liquidate()
#self.AddEquity("TLT")
#self.SetHoldings("TLT", 1)
#return
# Get data
hist_window = max(self.momentum_window,
self.momentum_window2) + self.exclude_days
if self.symbols is None: return
stocks = self.symbols
self.Debug("Stocks " + str(len(stocks)))
#hist = self.History(context.security_list, "close", hist_window, "1d")
hist = self.History(stocks, self.formation_days, Resolution.Daily)
current = self.History(stocks, 1, Resolution.Minute)
c_data = hist["close"].unstack(level=0)
self.Debug("c_data " + str(c_data))
data_end = -1 * (self.exclude_days + 1 ) # exclude most recent data
momentum1_start = -1 * (self.momentum_window + self.exclude_days)
momentum_hist1 = c_data[momentum1_start:data_end]
momentum2_start = -1 * (self.momentum_window2 + self.exclude_days)
momentum_hist2 = c_data[momentum2_start:data_end]
# Calculate momentum scores for all stocks.
momentum_list = momentum_hist1.apply(self.slope) # Mom Window 1
self.Debug("momentum_list " + str((momentum_list)))
momentum_list2 = momentum_hist2.apply(self.slope) # Mom Window 2
# Combine the lists and make average
momentum_concat = pd.concat((momentum_list, momentum_list2))
mom_by_row = momentum_concat.groupby(momentum_concat.index)
mom_means = mom_by_row.mean()
# Sort the momentum list, and we've got ourselves a ranking table.
ranking_table = mom_means.sort_values(ascending=False)
self.Debug("ranking_table " + str(len(ranking_table)))
# Get the top X stocks, based on the setting above. Slice the dictionary.
# These are the stocks we want to buy.
buy_list = ranking_table[:self.num_stocks]
self.Debug(" buy list " + str(len(buy_list)))
final_buy_list = buy_list[buy_list > self.minimum_momentum] # those who passed minimum slope requirement
self.Debug("Final buy list " + str(len(final_buy_list)))
# Calculate inverse volatility, for position size.
inv_vola_table = c_data[buy_list.index].apply(self.inv_vola_calc)
# sum inv.vola for all selected stocks.
sum_inv_vola = np.sum(inv_vola_table)
spy_hist_150 = self.History([self.spy], 150, Resolution.Daily).loc[str(self.spy)]['close']
spy_hist_200 = self.History([self.spy], 200, Resolution.Daily).loc[str(self.spy)]['close']
if self.Securities[self.spy].Price > (spy_hist_150.mean() + spy_hist_200.mean())/2:
can_buy = True
else:
can_buy = False
equity_weight = 0.0 # for keeping track of exposure to stocks
# Sell positions no longer wanted.
for security in self.Portfolio.Keys:
if (security not in final_buy_list):
if (security.Value != self.bond_etf):
# print 'selling %s' % security
#self.Debug("Sell Security " + str(security) )
self.SetHoldings(security, 0)
vola_target_weights = inv_vola_table / sum_inv_vola
for security in final_buy_list.index:
# allow rebalancing of existing, and new buys if can_buy, i.e. passed trend filter.
if (security in self.Portfolio.Keys) or (can_buy):
if (self.size_method == 1):
weight = vola_target_weights[security]
elif (self.size_method == 2):
weight = (0.99 / self.num_stocks)
self.Debug("Number of stocks " + str(self.num_stocks) + " Rebalance security " + str(security) + " with weight " + str(weight) )
self.SetHoldings(security, weight)
equity_weight += weight
# Fill remaining portfolio with bond ETF
etf_weight = max(1 - equity_weight, 0.0)
print ('equity exposure should be %s ' % str(equity_weight))
if (self.use_bond_etf):
self.Debug("Buy ETF" )
self.AddEquity("TLT")
self.SetHoldings(self.bond_etf, etf_weight)
#=========================
#if self.symbols is None: return
#chosen_df = self.calc_return(self.symbols)
#chosen_df = chosen_df.iloc[:self.num_stocks]
#self.existing_pos = 0
#add_symbols = []
#for symbol in self.Portfolio.Keys:
# if symbol.Value == 'SPY': continue
# if (symbol.Value not in chosen_df.index):
# self.SetHoldings(symbol, 0)
#elif (symbol.Value in chosen_df.index):
# self.existing_pos += 1
#weight = 0.99/len(chosen_df)
#for symbol in chosen_df.index:
# self.AddEquity(symbol)
# self.Debug("Symbol " + str(symbol) + " Weight " + str(weight))
# self.SetHoldings(symbol, weight)