Overall Statistics |
Total Trades 2261 Average Win 0.17% Average Loss -0.12% Compounding Annual Return 1.377% Drawdown 9.100% Expectancy 0.158 Net Profit 23.826% Sharpe Ratio 0.366 Probabilistic Sharpe Ratio 0.333% Loss Rate 52% Win Rate 48% Profit-Loss Ratio 1.41 Alpha 0.012 Beta -0.006 Annual Standard Deviation 0.032 Annual Variance 0.001 Information Ratio -0.431 Tracking Error 0.181 Treynor Ratio -1.91 Total Fees $39123.50 |
import numpy as np from collections import deque class RSharpeStrategy(QCAlgorithm): def Initialize(self): self.SetStartDate(2005, 1, 1) # Set Start Date self.SetEndDate(2020, 12, 31) # Set End Date self.SetCash(1000000) # Set Strategy Cash self.UniverseSettings.Leverage = 1 self.UniverseSettings.Resolution = Resolution.Daily self.AddUniverse(self.CoarseSelectionFilter) self.coarse_count = 10 self.max_holding = timedelta(days=50) # max holding day self.delay_reopen = timedelta(days=10) # reopen after X days self.RS = { }; # buffer of rolling sharpe indicator for each symbol self.traded = { };# register dates of traded securites #parameters for rolling sharpe ratio self.sharpe_period=90 self.RoC_max=0.5 self.sharpe_min_avg=0.1 self.sharpe_min_hold=0.5 self.rank_top=5 # the top X of the rank self.num_stocks_min=5000 # minimum number of ready stocks in all stocks self.SetSecurityInitializer(lambda x: x.SetMarketPrice(self.GetLastKnownPrice(x))) self.SetWarmup(self.sharpe_period) # Filter function def CoarseSelectionFilter(self, coarse): avg_sharpe = 0 # average rolling sharpe of all stocks num_stocks = 0 # number of ready stocks for stk in coarse: if stk.Symbol not in self.RS : self.RS[stk.Symbol] = RollingSharpe(symbol=stk.Symbol,period=self.sharpe_period) ################################################################################ # history warmup may hold one iteration for more than 10 mins causing error stop if stk.Price > 2 and False: # initialize the indicator with the daily history close price history = self.History([stk.Symbol], timedelta(days=self.sharpe_period), Resolution.Daily) if len(history)>0 : for time, row in history.loc[stk.Symbol].iterrows(): self.RS[stk.Symbol].Update(time, row["close"]) #else: ###### # Updates indicators self.RS[stk.Symbol].Update(time=stk.EndTime, price=stk.Price) if stk.Price > 2 and self.RS[stk.Symbol].IsReady : avg_sharpe+=self.RS[stk.Symbol].Value num_stocks+=1 if avg_sharpe!=0 and num_stocks!=0: avg_sharpe/=num_stocks # Filter the values of the dict: # 1. bull's market: average rolling Sharpe ratio of all stocks > sharpe_min # 2. price > 2 # 3. last sharpe_period's performance < RoC_max if num_stocks >= self.num_stocks_min and avg_sharpe > self.sharpe_min_avg : new = list(filter(lambda x: x.price>2 and x.IsReady and x.roc.Current.Value<self.RoC_max, self.RS.values())) # Sorts the values of the dict in descending order new.sort(key=lambda x: x.Value, reverse=True) else: # it's bear's market new=[ ] # check current holdings to close old=[ ] if self.Portfolio.Invested : for stk in self.Portfolio.Values: if self.Portfolio[stk.Symbol].Price > 2 and self.Portfolio[stk.Symbol].Invested and\ self.Time - self.RS[stk.Symbol].LastOpen < self.max_holding and\ self.RS[stk.Symbol].Value >= self.sharpe_min_hold: old.append(self.RS[stk.Symbol]) else: self.RS[stk.Symbol].LastClose = self.Time # securites number control if len(new)>0: if len(old)>0: new = list(filter(lambda x: x not in old, new)) new = new[:min(self.rank_top,self.coarse_count-len(old))] for x in new: if self.Time - x.LastClose >= self.delay_reopen: x.LastOpen = self.Time self.Log('New symbol: ' + str(x.Symbol) + ' Sharpe: current ' + str(round(x.Value,2)) + ' AvgRoll ' + str(round(avg_sharpe,2))) self.Log('AvgSharpe: '+str(round(avg_sharpe,2))+' Ready Stocks: '+str(num_stocks)+ ' total '+str(len(self.RS))+' len: new '+str(len(new))+' old '+str(len(old))) return [ x.Symbol for x in old+new ] # this event fires whenever we have changes to our universe def OnSecuritiesChanged(self, changes): # liquidate removed securities for security in changes.RemovedSecurities: if security.Invested: self.Liquidate(security.Symbol) # set equal allocation in each security in our universe for security in changes.AddedSecurities: self.SetHoldings(security.Symbol, 1/self.coarse_count/2) def OnData(self, data): '''OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here. Arguments: data: Slice object keyed by symbol containing the stock data ''' # if not self.Portfolio.Invested: # self.SetHoldings("SPY", 1) class RollingSharpe(object): def __init__(self, symbol, period): self.Symbol = symbol self.Time = datetime.min self.Value = 0 self.price = 0 self.IsReady = False self.period = period self.input = deque(maxlen=period) # limite number of inputs self.roc = RateOfChange(symbol, period) self.LastOpen = datetime(2000, 1, 1) self.LastClose = datetime(2000, 1, 1) def __repr__(self): return "{0} -> IsReady: {1}. Time: {2}. Value: {3}".format(self.Name, self.IsReady, self.Time, self.Value) # calculate Sharpe ratio for series x def sharpe(self, x, r = 0, scale = np.sqrt(250)): x=np.asfarray(x) if x.ndim > 1 : # more than 1 dimention (1 column) print("x is not a vector or univariate time series") return if np.isnan(x).any() : print("NaNs in x") return if x.shape[0] == 1: # only 1 row return(np.nan) else : if len(x)<self.period: return(np.nan) else: y = np.diff(x) return(scale * (np.mean(y) - r)/np.std(y)) def Update(self, time, price): self.price = price # append to the right because np.diff() uses right to minus left self.input.append(price) self.Time = time # calculate last x period's Sharpe ratio self.Value = self.sharpe(self.input) self.roc.Update(time, price) self.IsReady = len(self.input) == self.input.maxlen and self.roc.IsReady