Overall Statistics
Total Trades
2021
Average Win
1.44%
Average Loss
-0.84%
Compounding Annual Return
209.258%
Drawdown
70.100%
Expectancy
0.086
Net Profit
60.521%
Sharpe Ratio
3.696
Probabilistic Sharpe Ratio
60.690%
Loss Rate
60%
Win Rate
40%
Profit-Loss Ratio
1.71
Alpha
5.061
Beta
0.933
Annual Standard Deviation
1.353
Annual Variance
1.831
Information Ratio
4.128
Tracking Error
1.227
Treynor Ratio
5.363
Total Fees
$5028.27
Estimated Strategy Capacity
$430000.00
Lowest Capacity Asset
XRPUSD 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):
        self.SetStartDate(2021, 4, 19)  # Set Start Date
        self.SetEndDate(2021, 9, 18)
        self.SetCash(4000)
        
        self.symbols = ['BTCUSD','ETHUSD', 'XRPUSD', 'SOLUSD', 'LUNAUSD']
        
        self.warm_up = len(self.symbols)
        self.warm_up_count = 1
        self.window = 8
        self.rebalance = 4
        self.last_w = [0 for i in range(len(self.symbols))]
        self.use_last = True
        
        #Download returns from each strategy
        #Ris_csv = self.Download('https://raw.githubusercontent.com/sergiosierram/SharpSignal/main/data/CryptoDataReturns.csv')
        #self.Ris = pd.read_csv(StringIO(Ris_csv), index_col=0)
        
        #Initialize symbols
        self.initializeSymbols()
        
        #self.Ris = self.Ris.to_numpy() #.transpose()
        
        #set risk free asset rate of return
        self.Rf=0 # April 2019 average risk  free rate of return in USA approx 3%
        annRiskFreeRate = self.Rf/100
        
        #compute daily risk free rate in percentage
        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 OnData(self, data):
        if self.warm_up_count < self.warm_up:
            self.warm_up_count += 1
            return
        self.warm_up_count += 1
        
        if self.warm_up_count % self.rebalance == 0:
            try:
                #Ri = self.Ris[0:self.warm_up_count, :]
                #Ei = np.mean(Ri, axis = 0)
                if self.window == 0:
                    self.window = self.warm_up_count
                    
                Rix = self.History(self.symbols_objects, self.window)
                Ri = []
                for symbol in self.symbols:
                    d = Rix.loc[symbol]['close'].to_list()
                    Ri.append(d)
                Ri = np.array(Ri).transpose()
                Ri = StockReturnsComputing(Ri, self.window, self.portfolioSize)
                Ei = np.mean(Ri, axis = 0)
                #self.Debug(Ei)
            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 ]
            
            Rix2 = self.History(self.symbols_objects, self.window)
            adjust = []
            for symbol in self.symbols:
                d1 = Rix.loc[symbol]['close'].to_list()
                d1 = d1[-1]
                
                d2 = Rix2.loc[symbol]['close'].to_list()
                d2 = d2[-1]
                adjust.append(0.9-((d2-d1)/d1))
            
            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, adjust[i]*w[i]))
            if self.use_last:
                self.SetHoldings(targets)
        return
    
    def initializeSymbols(self):
        #self.symbols = [ name+"USD" for name in self.Ris.columns.tolist()]
        #self.Debug(str(self.symbols))
        
        self.symbols_objects = []
        for symbol in self.symbols:
            data = self.AddCrypto(symbol, Resolution.Hour, Market.Bitfinex)
            data.SetFeeModel(CustomFeeModel(self))
            self.symbols_objects.append(data.Symbol)
        return
    
    def OnEndOfAlgorithm(self):
        self.Liquidate()
        try:
            #Ri = self.Ris[0:self.warm_up_count, :]
            #Ei = np.mean(Ri, axis = 0)
            if self.window == 0:
                    self.window = self.warm_up_count
            Rix = self.History(self.symbols_objects, self.window)
            Ri = []
            for symbol in self.symbols:
                d = Rix.loc[symbol]['close'].to_list()
                Ri.append(d)
            Ri = np.array(Ri).transpose()
            Ri = StockReturnsComputing(Ri, self.window, self.portfolioSize)
            Ei = np.mean(Ri, axis = 0)
            #self.Debug(Ei)
        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)

# 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