Overall Statistics
Total Trades
15094
Average Win
0.11%
Average Loss
-0.08%
Compounding Annual Return
193.641%
Drawdown
34.900%
Expectancy
0.459
Net Profit
1190.771%
Sharpe Ratio
2.706
Probabilistic Sharpe Ratio
90.360%
Loss Rate
38%
Win Rate
62%
Profit-Loss Ratio
1.33
Alpha
1.123
Beta
0.377
Annual Standard Deviation
0.519
Annual Variance
0.269
Information Ratio
1.095
Tracking Error
0.602
Treynor Ratio
3.721
Total Fees
$31878.12
Estimated Strategy Capacity
$13000.00
Lowest Capacity Asset
EURUSD 2MN
from AlgorithmImports import *

class RebalancingPremiumsinCryptos(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2020, 1, 1)
        #self.SetEndDate(2020, 6, 1)
        self.SetCash(100000)
        #self.SetBrokerageModel(BrokerageName.GDAX, AccountType.Cash)
        self.SetBrokerageModel(BrokerageName.FTX, AccountType.Cash)
        self.Settings.FreePortfolioValuePercentage = 0.05
        self.week = -1
        self.readyToTrade = False
        self.high = 0
        
        
        # Get crypto USD pairs available to trade on FTX
        self.tickers = []
        url = "https://raw.githubusercontent.com/QuantConnect/Lean/master/Data/symbol-properties/symbol-properties-database.csv"
        spdb = self.Download(url).split('\n')
        # exclude FTX special pairs, stablecoins, and SPELL as it has a data issue on FTX Dec 11th 2021
        matches = ["BULL", "BEAR", "HEDGE", "HALF", "SPELL", "USDCUSD", "USDTUSD", "DAIUSD"]
        #matches = ["DAIUSD", "USTUSD", "USDTUSD", "PAXUSD"]
        for line in spdb:
            csv = line.split(',')
            if csv[0] == 'ftx' and csv[1].endswith('USD') and not any(word in csv[1] for word in matches):
            #if csv[0] == 'gdax' and csv[1].endswith('USD') and not any(word in csv[1] for word in matches):
                self.tickers.append(Symbol.Create(csv[1], SecurityType.Crypto, Market.FTX))
                #self.tickers.append(Symbol.Create(csv[1], SecurityType.Crypto, Market.GDAX))
        # self.Debug(f'Current FTX Cryptos with USD {len(self.tickers)}')
        
        self.bands_by_ticker = {}
        for symbol in self.tickers:
            self.bands_by_ticker[symbol] = BollingerBands(30, 2)

        self.AddUniverse(SecurityType.Crypto, 'Universe1', Resolution.Daily, Market.FTX, self.UniverseSettings, self.Selector)
        #self.AddUniverse(SecurityType.Crypto, 'Universe1', Resolution.Daily, Market.GDAX, self.UniverseSettings, self.Selector)
            
        self.Schedule.On(self.DateRules.EveryDay(),
                         self.TimeRules.At(1, 0),
                         self.DailyRebalance)
                         
        # Configs
        self.stop_loss = False
                         
        # Benchmark indicators
        self.BenchmarkCrypto = self.AddCrypto("BTCUSD", Resolution.Daily).Symbol
        #self.macd_100_300 = self.MACD("BTCUSD", 100, 300, 9, MovingAverageType.Exponential, Resolution.Daily)
        #self.macd_50_200 = self.MACD("BTCUSD", 50, 200, 9, MovingAverageType.Exponential, Resolution.Daily)
        self.macd = self.MACD("BTCUSD", 20, 50, 9, MovingAverageType.Exponential, Resolution.Daily)
        # Plot
        #self.PlotIndicator("MACD 100-300", True, self.macd_100_300, self.macd_100_300.Signal)
        #self.PlotIndicator("BTC 100-300", self.macd_100_300.Fast, self.macd_100_300.Slow)
        self.PlotIndicator("MACD 20-50", True, self.macd, self.macd.Signal)
        self.PlotIndicator("BTC 20-50", self.macd.Fast, self.macd.Slow)
        # self.SetRiskManagement(MaximumDrawdownPercentPortfolio(0.3))
                         
    # Build universe: select the top 25 mean-reverting pairs (based on bollinger bands) with highest volume
    def Selector(self, dt):
        current_week = self.Time.isocalendar()[1]
        if current_week == self.week:
            return Universe.Unchanged
        self.week = current_week
        
        # 31 days so the last one is not in the BB when we look at where the price is
        history = self.History(self.tickers, 31, Resolution.Daily)
        volume_by_symbol = {}

        for symbol in self.tickers:
            try:
                if symbol not in history.index: continue
                cur_bands = self.bands_by_ticker[symbol]
                for time, data in history.loc[symbol].iterrows():
                    cur_bands.Update(time, data.close)
                if not cur_bands.IsReady:continue
                df = history.loc[symbol].iloc[-1]
                dollar_volume = df.close * df.volume
                price = df.close
                lower_band = cur_bands.LowerBand.Current.Value
                upper_band = cur_bands.UpperBand.Current.Value
                
                if math.isnan(dollar_volume) or (price < lower_band) or (price > upper_band):
                    continue
                volume_by_symbol[symbol] = dollar_volume
            except:
                continue

        selected = sorted(volume_by_symbol.items(), key=lambda x: x[1], reverse=True)
        universe = [x[0].Value for x in selected][:25]
        # self.Debug(f"My universe: {universe}")
        
        return universe
        
    def OnSecuritiesChanged(self, changes):
        for security in changes.RemovedSecurities:
            self.Liquidate(security.Symbol)
            # self.Debug(f"Removed {security.Symbol} from the the universe")

        for security in changes.AddedSecurities:
            self.readyToTrade = True
            self.Debug(f"Added {security.Symbol} to the universe")
            
    
    # SetHoldings method applied daily to the symbols in ActiveSecurities        
    def DailyRebalance(self):
        if self.readyToTrade:
            # Exit condition
            if self.macd.Fast.Current.Value < self.macd.Slow.Current.Value:
                self.Plot("Signals", "MACDExitCondition", 1)
                self.Liquidate()
                return
            # self.Debug(f"Daily rebalance method triggered at {self.Time}")
            self.Plot("Signals", "MACDExitCondition", 0)
            weight = 1.0/len(self.ActiveSecurities)
            
            targets = []
            
            for symbol in self.Portfolio.Keys:
                pnl = self.Securities[symbol].Holdings.UnrealizedProfitPercent
                if self.stop_loss and pnl < -0.30:
                    self.Liquidate(symbol)
                    continue
 
                targets.append(PortfolioTarget(symbol, weight))
                    

            if targets:
                self.SetHoldings(targets)