Overall Statistics
Total Trades
120
Average Win
1.09%
Average Loss
-1.19%
Compounding Annual Return
-5.379%
Drawdown
28.200%
Expectancy
-0.113
Net Profit
-9.194%
Sharpe Ratio
-0.049
Probabilistic Sharpe Ratio
4.848%
Loss Rate
54%
Win Rate
46%
Profit-Loss Ratio
0.92
Alpha
0.052
Beta
-0.041
Annual Standard Deviation
0.195
Annual Variance
0.038
Information Ratio
-2.157
Tracking Error
0.703
Treynor Ratio
0.233
Total Fees
$4491.26
Estimated Strategy Capacity
$690000.00
Lowest Capacity Asset
LUNAUSDT E3
#$@$@ ---------------------------------------------------------------- 
#$@$@ -------------      Author; Emmanuel Azubuike      --------------- 
#$@$@ ---------------------------------------------------------------- 
class ScanWin():
    
    def __init__(self, wT, wL):
        self.window    = None
        self.winLength = wL
        if (wT is "int"):self.window = RollingWindow[int](wL)
        elif (wT is "bool"):self.window = RollingWindow[bool](wL)
        elif (wT is "float"):self.window = RollingWindow[float](wL)
        elif (wT is "TradeBar"):self.window = RollingWindow[TradeBar](wL)
    def isAbove(self, series): return (self.window[0] > series[0])
    def isBelow(self, series): return (self.window[0] < series[0])
    def Add(self,value): 
        self.window.Add(value)
    def IsReady(self):
        return (self.window is not None) and \
               (self.window.Count >= self.winLength) 
    def __getitem__(self, index):
        return self.window[index]
#$@$@ ---------------------------------------------------------------- 
#$@$@ -------------      Author; Emmanuel Azubuike      --------------- 
#$@$@ ---------------------------------------------------------------- 
from AlgorithmImports import *
import operator
import math
from ScanWin import *
class EMACrossoverUniverse(QCAlgorithm):
    #$@$@ ---------------------------------------------------------------- 
    def Initialize(self):
        self.InitA()
        self.InitB()
        self.Initc()
        self.Initd()        
    #$@$@ ------------Set backtest parameters--------------------
    def Initd(self):
        self.SetStartDate(2020, 1, 1)
        self.SetCash(100000)
        self.SetBenchmark(Symbol.Create("BTCUSDT", SecurityType.Crypto, Market.Bitfinex))

    def Initc(self):
        self.UniverseSettings.Resolution = Resolution.Daily
        self.symDataDict = { }
        #self.UniverseTickers = ["SOLUSDT", "LUNAUSDT", "ADAUSDT", "BTCUSDT"]
        self.UniverseTickers = ["LUNAUSDT"]
        universeSymbols = []
        for symbol in self.UniverseTickers:

            universeSymbols.append(Symbol.Create(symbol, SecurityType.Crypto, Market.Bitfinex))
            Symbol.BuyingPowerModel = SecurityMarginModel(3.3)
            
        self.SetUniverseSelection(ManualUniverseSelectionModel(universeSymbols))

    #$@$@ ----------Set Algo parameters----------
    def InitA(self):
        
        self.emaSlowPeriod    = int(self.GetParameter('emaSlowPeriod'))
        self.emaFastPeriod    = int(self.GetParameter('emaFastPeriod'))
        self.warmupPeriod     = self.emaSlowPeriod
        self.maxExposurePct   = float(self.GetParameter("maxExposurePct"))/100
        self.maxHoldings      = int(self.GetParameter("maxHoldings"))
        self.minAmountToBeInvested = int(self.GetParameter("minAmountToBeInvested"))
    
    #$@$@ ----------Set Asset parameters----------
    def InitB(self):    
        #self.symbol = "BTCUSDT" 
        self.SetBrokerageModel(BrokerageName.Bitfinex, AccountType.Margin) #AccountType.Cash)
        self.SetAccountCurrency("USDT")
        #self.AddCrypto(self.symbol, Resolution.Daily)
        self.EnableAutomaticIndicatorWarmUp = True
        self.SetWarmUp(timedelta(self.warmupPeriod))
        self.SelectedSymbolsAndWeights = {}
    
    #$@$@ ------------------------------------------------------------
    @property
    def PAC(self):
        numHoldings = len([x.Key for x in self.Portfolio if self.IsInvested(x.Key)])
        return numHoldings >= self.maxHoldings
    
    #$@$@ --------------------Check for  signals----------------------------
    def OnData(self, dataSlice):
        #$@$@ loop through the symbols in the slice
        for symbol in dataSlice.Keys:
            if symbol in self.symDataDict:
                symbolData = self.symDataDict[symbol] 
                ## Update the symbol
                symbolData.OnSymbolData(self.Securities[symbol].Price, dataSlice[symbol])
                if self.IsInvested(symbol):
                    symbolData.MOP()
                else:
                    ## First check if we are at capacity for new positions
                    if(not self.PAC):            
                        if( symbolData.BuySignalFired() ):
                            self.ONP(symbolData.symbol)
                        if( symbolData.SellSignalFired() ):
                            self.OpenNewSellPosition(symbolData.symbol)
                            symbolData.OnNewPositionOpened()
    
    #$@$@ ---rebalance portfolio of holdings with equal weighting-----------------------
    def ReHo(self, rebalanceCurrHoldings=False):
        for symbol in self.SelectedSymbolsAndWeights:
            symbolWeight = round(1/len(self.SelectedSymbolsAndWeights),4)
            self.SetWH(symbol,symbolWeight)
        sortedSymbolsAndWeights = {k: v for k, v in sorted(self.SelectedSymbolsAndWeights.items(), key=lambda item: item[1], reverse=True)}
        for symbol in sortedSymbolsAndWeights:
            self.SetSymbolHoldings(symbol)
    
    #$@$@ ---rebalance sells portfolio of holdings with equal weighting-----------------------
    def RebalanceHoldings(self, rebalanceCurrHoldings=False):
        for symbol in self.SelectedSymbolsAndWeights:
            symbolWeight = round(1/len(self.SelectedSymbolsAndWeights),4)
            self.SetWH(symbol,symbolWeight)
        sortedSymbolsAndWeights = {k: v for k, v in sorted(self.SelectedSymbolsAndWeights.items(), key=lambda item: item[1], reverse=True)}
        for symbol in sortedSymbolsAndWeights:
            self.SetSymbolHoldingsSell(symbol)
    
    def SetSymbolHoldings(self, symbol):
        adjustedWeight = self.SelectedSymbolsAndWeights[symbol]
        cash = self.Portfolio.TotalPortfolioValue - self.Portfolio.TotalHoldingsValue
        percent = adjustedWeight * self.maxExposurePct
        cost = self.Portfolio.TotalPortfolioValue * percent
        orderMsg = f"{symbol} | alloc. ({round(adjustedWeight*100,2)}% adjusted) "
        if (cost > cash):
            percent = self.GetTruncatedValue(cash / self.Portfolio.TotalPortfolioValue, 3)
        if(self.Portfolio[symbol].Invested): 
            orderMsg = f"[Re-Balancing] {orderMsg}"
        else:
            orderMsg = f"[NEW Addition] {orderMsg}"
        self.SetHoldings(symbol, percent, tag=orderMsg)   
    
    def SetSymbolHoldingsSell(self, symbol):
        adjustedWeight = self.SelectedSymbolsAndWeights[symbol]
        cash = self.Portfolio.TotalPortfolioValue - self.Portfolio.TotalHoldingsValue
        percent = adjustedWeight * self.maxExposurePct
        cost = self.Portfolio.TotalPortfolioValue * percent
        orderMsg = f"{symbol} | alloc. ({round(adjustedWeight*100,2)}% adjusted) "
        if (cost > cash):
            percent = self.GetTruncatedValue(cash / self.Portfolio.TotalPortfolioValue, 3)
        if(self.Portfolio[symbol].Invested): 
            orderMsg = f"[Re-Balancing] {orderMsg}"
        else:
            orderMsg = f"[NEW Addition] {orderMsg}"
        self.SetHoldings(symbol, -percent, tag=orderMsg)   
    #$@$@ --------–---## Allocate the specified weight (pct) of the portfolio value to 
    ## the specified symbol This weight will first be adjusted to consider
    ## cost basis, whether the position is already open and has profit.
    ## We are doing this to solve the problem where re-balancing causes winners
    ## to reduce in position size.-----–--------–--------–--------–--------–--------–--------–
    def SetWH(self,symbol,symbolWeight):
        if( self.Portfolio.Invested ):
            TCB = sum( [x.Value.HoldingsCost for x in self.Portfolio if x.Value.Invested] )
        else: 
            TCB = 0.0             
        CA = self.Portfolio.TotalPortfolioValue - self.Portfolio.TotalHoldingsValue
        # CA  = self.Portfolio.CashBook["USDT"].Amount
        WB = TCB + CA
        ATI    = WB * symbolWeight 

        if(self.Portfolio[symbol].Invested):
            profitPct           = self.Portfolio[symbol].UnrealizedProfitPercent
            adjustedATI = ATI * (1 + profitPct)
            adjustedWeight      = adjustedATI / self.Portfolio.TotalPortfolioValue
        else:
            adjustedWeight = ATI / self.Portfolio.TotalPortfolioValue
        symbolWeight   = self.GetTruncatedValue(symbolWeight,3)
        adjustedWeight = self.GetTruncatedValue(adjustedWeight,3)

        self.SelectedSymbolsAndWeights[symbol] = adjustedWeight

    #$@$@ --------------## Adding the symbol to dictionary to ensure
    ## that it gets processed while rebalancing -----------------------------------
    def ONP(self, symbol):
        self.SelectedSymbolsAndWeights[symbol] = 0
        self.ReHo()    
    def OpenNewSellPosition(self, symbol):
        self.SelectedSymbolsAndWeights[symbol] = 0
        self.RebalanceHoldings()  
        
    #$@$@ -------------## Removing the symbol from our dictionary will ensure
    ## that it wont get processed while rebalancing ----------------------------------------
    def ExitBuypositions(self, symbol, exitMsg=""):
        profitPct   = round(self.Securities[symbol].Holdings.UnrealizedProfitPercent,2)
        self.Liquidate(symbol, tag=f"Closed Buy {symbol.Value} ({profitPct}% profit) [{exitMsg}]")
        self.SelectedSymbolsAndWeights.pop(symbol)
     
        return
    
    def ExitSellpositions(self, symbol, exitMsg=""):
        profitPct   = round(self.Securities[symbol].Holdings.UnrealizedProfitPercent,2)
        self.Liquidate(symbol, tag=f"CLosed Sell {symbol.Value} ({profitPct}% profit) [{exitMsg}]")
        self.SelectedSymbolsAndWeights.pop(symbol)
     
        return

    #$@$@ -----Create new symboldata object and add to dictionary-------------------------------------------------
    def OnSecuritiesChanged(self, changes):
        for security in changes.AddedSecurities:
            symbol  = security.Symbol
            if( symbol in self.UniverseTickers and \
                symbol not in self.symDataDict.keys()):
                self.symDataDict[symbol] = SymbolData(symbol, self) 

    def GetTruncatedValue(self, value, decPlaces):
        truncFactor  = 10.0 ** decPlaces
        return math.trunc(value * truncFactor) / truncFactor
        
    def IsInvested(self, symbol):
        return self.Portfolio[symbol].Invested and self.Portfolio[symbol].Quantity * self.Securities[symbol].Price > self.minAmountToBeInvested

#$@$@ ----------------------------------------
# SymbolData Class
#$@$@ ----------------------------------------
class SymbolData():
    
    def __init__(self, theSymbol, algo):

        #$@$@ Algo / Symbol / Price reference
        self.algo       = algo
        self.symbol     = theSymbol
        self.lastPrice  = 0
        self.price      = 0

        self.Init2() ## Initialize indicators
            
    #$@$@ ----------------------------------------
    def Init2(self):

        self.indicators = { 'EMA_FAST'  : self.algo.EMA(self.symbol,self.algo.emaFastPeriod,Resolution.Daily),
                            'EMA_SLOW'  : self.algo.EMA(self.symbol,self.algo.emaSlowPeriod,Resolution.Daily)}
        
        for key, indicator in self.indicators.items():
            self.algo.WarmUpIndicator(self.symbol, indicator, Resolution.Minute)

        self.emaFastWindow  = ScanWin("float", 2)
        self.emaSlowWindow  = ScanWin("float", 2)
        self.lastPriceWindow    = ScanWin("float", 2)
        
    #$@$@ ----------------------------------------
    def OnSymbolData(self, lastKnownPrice, tradeBar):
        self.lastPrice = lastKnownPrice
        self.UpdateRW()
        
    #$@$@ --------------To update Rolling window--------------------------
    def UpdateRW(self):
        self.emaFastWindow.Add(self.indicators['EMA_FAST'].Current.Value)
        self.emaSlowWindow.Add(self.indicators['EMA_SLOW'].Current.Value)
        self.lastPriceWindow.Add(self.lastPrice)
        
    #$@$@ ----------------------------------------
    def IsReady(self):
        return (self.indicators['EMA_FAST'].IsReady and self.indicators['EMA_SLOW'].IsReady)
            
        
    #$@$@ -------------Entry signal for buys---------------------------
    def BuySignalFired(self):
        if( self.IsReady() ):
            if( self.emaFastWindow.isAbove(self.emaSlowWindow) and \
                self.lastPriceWindow.isAbove(self.emaFastWindow) ):
                return True
        
        return False
   
    #$@$@ -------------Entry signal for Sells---------------------------
    def SellSignalFired(self):
        if( self.IsReady() ):
            if ( self.lastPriceWindow.isBelow(self.emaSlowWindow) and \
                 self.emaSlowWindow.isAbove(self.emaFastWindow) ):
                return True
        
        return False
        
    #$@$@ ---------Exit signal for buys, Entry for Sells-------------------------------
    def ExitBuySignals(self):
        if( self.IsReady() ):
            if ( self.lastPriceWindow.isBelow(self.emaSlowWindow) or \
                 self.emaSlowWindow.isAbove(self.emaFastWindow) ):
                return True
        
        return False
    
    def ExitSellSignals(self):
        if( self.IsReady() ):
            if( self.emaFastWindow.isAbove(self.emaSlowWindow) or \
                self.lastPriceWindow.isAbove(self.emaFastWindow) ):
                return True
        
        return False
   #$@$@ ---------------------------------------------------------
    def OnNewPositionOpened(self):            
        return
    
    #$@$@ ----------Manage open Positions-------------------------------------------------------------------
    def MOP(self):

        if(self.ExitBuySignals()):        
            self.ExitBuypositions(exitMsg="Exit Buy Signal Fired")
            
        if(self.ExitSellSignals()):        
            self.ExitSellpositions(exitMsg="Exit Sell Signal Fired")
        
    #$@$@ ----------------------------------------
    def ExitBuypositions(self, exitMsg): 
        self.algo.ExitBuypositions(self.symbol, exitMsg)
    
    def ExitSellpositions(self, exitMsg): 
        self.algo.ExitSellpositions(self.symbol, exitMsg)