Overall Statistics |
Total Trades 890 Average Win 2.92% Average Loss -2.51% Compounding Annual Return 22.302% Drawdown 36.000% Expectancy 0.301 Net Profit 2022.312% Sharpe Ratio 0.793 Loss Rate 40% Win Rate 60% Profit-Loss Ratio 1.16 Alpha 0.32 Beta -7.512 Annual Standard Deviation 0.248 Annual Variance 0.062 Information Ratio 0.727 Tracking Error 0.248 Treynor Ratio -0.026 Total Fees $1812.40 |
from QuantConnect.Data.UniverseSelection import * import math import numpy as np import pandas as pd import decimal as d 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)) #self.Schedule.On(self.DateRules.EveryDay("SPY"), self.TimeRules.At(10, 0), Action(self.daily_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 self.open_stop_market_orders = {} # 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): # 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 #elf.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("==========================================================================") 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 self.Debug("===================: " + str(self.open_stop_market_orders)) # Sell positions no longer wanted. for security in self.Portfolio.Keys: #self.Debug("Portfolio key: " + str(security) + " value: " + str(security.Value)) if (security not in final_buy_list): if (security.Value != self.bond_etf): #self.Debug(" Sell security :" + str(security) + " Invested: " + str(self.Portfolio[security].Invested) + " [2] In open_stop_market_orders: " + str(str(security) in self.open_stop_market_orders.keys())) #an tin eixa apo prin kai den tin thelw twra kai den exei ginei Stop Loss if self.Portfolio[security].Invested and str(security) in self.open_stop_market_orders.keys() : shortOrder = self.open_stop_market_orders[str(security)] shortOrder.Cancel("Short filled") self.open_stop_market_orders.pop(str(security),None) self.Debug("{0}: Short order is filled. ".format(shortOrder.OrderType) + str(security)) self.SetHoldings(security, 0) vola_target_weights = inv_vola_table / sum_inv_vola quantity = self.Portfolio.TotalPortfolioValue* d.Decimal(.33) for security in final_buy_list.index: # allow rebalancing of existing, and new buys if can_buy, i.e. passed trend filter. self.Debug("** Invested: "+ str(self.Portfolio[security].Invested) + " or can_buy: " + str(can_buy) ) if (self.Portfolio[security].Invested) 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) self.MarketOrder(security, quantity/self.Securities[security].Price) #self.Debug("333333") self.Debug("[3] In open_stop_market_orders " + str(str(security) in self.open_stop_market_orders.keys()) ) if str(security) in self.open_stop_market_orders.keys(): #self.Debug("4444") longOrder = self.open_stop_market_orders[security] updateOrderFields = UpdateOrderFields() updateOrderFields.StopPrice = self.Securities[security].Price * d.Decimal(.5) updateOrderFields.Tag = "Update #{0}".format(len(longOrder.UpdateRequests) + 1) longOrder.Update(updateOrderFields) self.Debug("Updated price " + str(longOrder.Get(OrderField.StopPrice)) + " security "+ str(security)) else: newTicket = self.StopMarketOrder(security, -quantity/self.Securities[security].Price , self.Securities[security].Price * d.Decimal(.5)) #self.Debug("StopMarketOrder price " + str(self.Securities[security].Price * d.Decimal(.9))) self.open_stop_market_orders[str(security)] = newTicket #self.Debug("===================: " + str(self.open_stop_market_orders)) #self.Debug("Open market orders size: " + str(len(self.open_stop_market_orders))) self.Debug("{0}: Submitting StopMarketOrder. ".format(newTicket.OrderType) + str(security)) equity_weight += weight # Fill remaining portfolio with bond ETF etf_weight = max(0.99 - 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) def OnOrderEvent(self, orderEvent): order = self.Transactions.GetOrderById(orderEvent.OrderId) self.Debug("{0}: {1}: {2}".format(self.Time, order.Type, orderEvent))