Overall Statistics
Total Trades
1620
Average Win
2.93%
Average Loss
-1.88%
Compounding Annual Return
48.817%
Drawdown
89.200%
Expectancy
0.234
Net Profit
563.752%
Sharpe Ratio
1.061
Probabilistic Sharpe Ratio
25.208%
Loss Rate
52%
Win Rate
48%
Profit-Loss Ratio
1.56
Alpha
0.027
Beta
0.733
Annual Standard Deviation
0.887
Annual Variance
0.786
Information Ratio
-0.41
Tracking Error
0.746
Treynor Ratio
1.284
Total Fees
$5847.70
Estimated Strategy Capacity
$8000.00
Lowest Capacity Asset
LEOUSD 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
        
        #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"
        
        self.symbols = []
        self.resolution = Resolution.Hour
        
        #Parameters
        self.window = int(self.GetParameter("window"))
        self.rebalance = int(self.GetParameter("rebalance"))
        self.t_count = -1
        self.week_count = 0
        
        self.symbols = list(self.market_data.iloc[self.week_count,:])
        self.symbols = list(map(self.aux_func, self.symbols))
        
        #Additional variables
        #List to store previous weights
        self.last_w = [0 for i in range(len(self.symbols))]
        self.use_last = True
        
        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.symbols)
        
        self.SetBrokerageModel(BrokerageName.Bitfinex)
        self.SetBenchmark("BTCUSD")
        return
    
    def initializeSymbols(self):
        self.symbols_objects = []
        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("Symbol not available: "+str(symbol))
        return
    
    def initRollingWindow(self):
        c = 0
        unavailable = 0
        self.rolling = [RollingWindow[float](self.window) for symbol in self.symbols]
        for symbol in 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:
                    unavailable += 1
                    self.Debug("Unavailable data for: "+symbol)
                    break
            c += 1
        self.rolling[0:-unavailable]
        return
    
    def OnData(self, data):
        self.t_count += 1
        if self.t_count % self.rebalance == 0:
            self.SpecificTime()
            
            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.symbols = list(self.market_data.iloc[self.week_count,:])
                self.symbols = list(map(self.aux_func, self.symbols))
                self.initializeSymbols()
                self.initRollingWindow()
                self.portfolioSize = len(self.symbols)
            
        c = 0
        for symbol in self.symbols:
            if data.ContainsKey(symbol):
                self.rolling[c].Add(data[symbol].Close)
                c+=1
        
        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]
            self.Log(len(l))
            if len(l) < self.window:
                flag = False
        if not flag:
            return
        #try:
        
        Ri = []
        c = 0
        for symbol in self.symbols:
            Ri.append([i for i in self.rolling[c]][::-1])
            c+=1
        Ri = np.array(Ri).transpose()
        self.Log(Ri)
        Ri = StockReturnsComputing(Ri, self.window, self.portfolioSize)
        Ei = np.mean(Ri, axis = 0)
        #except:
        #    self.Log(str("Error during data extraction"))
        #    return
        
        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)
        
        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.75*w[i]))
        if self.use_last:
            self.SetHoldings(targets)
        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