Overall Statistics |
Total Trades 1359 Average Win 3.65% Average Loss -2.34% Compounding Annual Return 154.188% Drawdown 56.500% Expectancy 0.342 Net Profit 8437.558% Sharpe Ratio 2.084 Probabilistic Sharpe Ratio 82.396% Loss Rate 48% Win Rate 52% Profit-Loss Ratio 1.56 Alpha 0.529 Beta 0.6 Annual Standard Deviation 0.624 Annual Variance 0.389 Information Ratio 0.028 Tracking Error 0.539 Treynor Ratio 2.167 Total Fees $96184.97 Estimated Strategy Capacity $25000.00 Lowest Capacity Asset BTCUSD E3 |
from io import StringIO from datetime import datetime import pandas as pd import numpy as np from scipy import optimize class CasualFluorescentYellowCaterpillar(QCAlgorithm): def Initialize(self): #Backtest dates self.SetStartDate(2017, 1, 2) # Set Start Date #self.SetEndDate(2017, 5, 30) #Algorithm cash self.SetCash(4000) #Download returns from each strategy csv = self.Download('https://raw.githubusercontent.com/sergiosierram/SharpSignal/main/data/market_cap.csv') self.market_data = pd.read_csv(StringIO(csv), index_col=0) self.aux_func = lambda x: x+"USD" #This function is used to append USD to the cryptos #Parameters self.window = int(self.GetParameter("window")) self.rebalance = int(self.GetParameter("rebalance")) self.topx = int(self.GetParameter("topx")) self.t_count = -1 #This counter is for rebalancing purposes self.week_count = 0 #This counter is for indexing the dataset self.symbols = [] self.last_symbols = [] self.resolution = Resolution.Hour #Additional variables #List to store previous weights self.last_w = [0 for i in range(len(self.symbols))] self.use_last = True #Get the first list of top X symbols self.symbols = list(self.market_data.iloc[self.week_count,0:self.topx]) self.symbols = list(map(self.aux_func, self.symbols)) self.initializeSymbols() self.initRollingWindow() self.Rf=0 # April 2019 average risk free rate of return in USA approx 3% annRiskFreeRate = self.Rf/100 self.r0 = (np.power((1 + annRiskFreeRate), (1.0 / 360.0)) - 1.0) * 100 self.portfolioSize = len(self.rolling) self.SetBrokerageModel(BrokerageName.Bitfinex) self.SetBenchmark("BTCUSD") return def initializeSymbols(self): self.symbols_objects = [] prev = len(self.symbols) for symbol in self.symbols: try: data = self.AddCrypto(symbol, self.resolution, Market.Bitfinex) data.SetFeeModel(CustomFeeModel(self)) self.symbols_objects.append(data.Symbol) except: self.symbols.remove(symbol) #self.Debug("Rm: "+str(symbol)) real = len(self.symbols) #self.Debug(str(real)+"/"+str(prev)) return def initRollingWindow(self): unavailable = 0 self.rolling = [RollingWindow[float](self.window) for symbol in self.symbols] prev = len(self.rolling) for c, symbol in enumerate(self.symbols): df = pd.DataFrame() while df.empty: try: df = self.History(self.Symbol(symbol), self.window) d = df['close'].to_list() for x in d: self.rolling[c].Add(x) except: #del self.rolling[c] #del self.symbols[c] #self.Debug("No data: "+symbol) break real = len(self.rolling) #self.Debug(str(real)+"/"+str(prev)) return def OnData(self, data): self.t_count += 1 if self.t_count % self.rebalance == 0: #self.Debug("Starting rebalance") self.SpecificTime() try: day, month, year = list(map(int, self.market_data.index[self.week_count].split('/'))) prevd = datetime(year+2000, month, day) day, month, year = list(map(int, self.market_data.index[self.week_count+1].split('/'))) nextd = datetime(year+2000, month, day) currentd = self.Time if currentd > prevd and currentd <= nextd: pass else: self.week_count += 1 self.last_symbols = list(self.symbols) self.symbols = list(self.market_data.iloc[self.week_count,0:self.topx]) self.symbols = list(map(self.aux_func, self.symbols)) self.initializeSymbols() self.initRollingWindow() self.portfolioSize = len(self.rolling) except: #This try except is to avoid problems with the last row of the dataset pass for c, symbol in enumerate(self.symbols): if data.ContainsKey(symbol): self.rolling[c].Add(data[symbol].Close) return def SpecificTime(self): #Check the len of the rolling windows flag = True for roll in self.rolling: l = [i for i in roll][::-1] if len(l) < self.window: flag = False if not flag: return Ri = [] for c, symbol in enumerate(self.symbols): Ri.append([i for i in self.rolling[c]][::-1]) Ri = np.array(Ri).transpose() Ri = StockReturnsComputing(Ri, self.window, self.portfolioSize) Ei = np.mean(Ri, axis = 0) cov = np.cov(Ri, rowvar=False) #initialization xOptimal =[] minRiskPoint = [] expPortfolioReturnPoint =[] maxSharpeRatio = 0 #compute maximal Sharpe Ratio and optimal weights result = MaximizeSharpeRatioOptmzn(Ei, cov, self.r0, self.portfolioSize) xOptimal.append(result.x) w = list(xOptimal[0]) w = [ 0 if wx < 0.0000001 else wx for wx in w ] self.Debug(w) self.LiquidateOldSymbols() if not self.use_last: self.Liquidate() targets = [] for i in range(len(w)): currency = self.symbols[i] if not self.use_last: self.SetHoldings(currency, w[i]) else: targets.append(PortfolioTarget(currency, 0.7*w[i])) if self.use_last: self.SetHoldings(targets) return def LiquidateOldSymbols(self): for symbol in self.last_symbols: if symbol not in self.symbols: self.Log("Not in last: "+symbol) self.Liquidate(symbol) return def OnEndOfAlgorithm(self): self.Liquidate() return # Custom fee model. class CustomFeeModel(FeeModel): def GetOrderFee(self, parameters): fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.002 return OrderFee(CashAmount(fee, "USD")) def MaximizeSharpeRatioOptmzn(MeanReturns, CovarReturns, RiskFreeRate, PortfolioSize): # define maximization of Sharpe Ratio using principle of duality def f(x, MeanReturns, CovarReturns, RiskFreeRate, PortfolioSize): funcDenomr = np.sqrt(np.matmul(np.matmul(x, CovarReturns), x.T) ) funcNumer = np.matmul(np.array(MeanReturns),x.T)-RiskFreeRate func = -(funcNumer / funcDenomr) return func #define equality constraint representing fully invested portfolio def constraintEq(x): A=np.ones(x.shape) b=1 constraintVal = np.matmul(A,x.T)-b return constraintVal #define bounds and other parameters xinit=np.repeat(0.33, PortfolioSize) cons = ({'type': 'eq', 'fun':constraintEq}) lb = 0 ub = 1 bnds = tuple([(lb,ub) for x in xinit]) #invoke minimize solver opt = optimize.minimize (f, x0 = xinit, args = (MeanReturns, CovarReturns,\ RiskFreeRate, PortfolioSize), method = 'SLSQP', \ bounds = bnds, constraints = cons, tol = 10**-3) return opt def StockReturnsComputing(StockPrice, Rows, Columns): StockReturn = np.zeros([Rows-1, Columns]) for j in range(Columns): # j: Assets for i in range(Rows-1): # i: Daily Prices StockReturn[i,j]=((StockPrice[i+1, j]-StockPrice[i,j])/StockPrice[i,j])*100 return StockReturn