Overall Statistics
Total Trades
346
Average Win
0.96%
Average Loss
-0.44%
Compounding Annual Return
36.942%
Drawdown
20.800%
Expectancy
0.870
Net Profit
87.854%
Sharpe Ratio
1.628
Probabilistic Sharpe Ratio
77.987%
Loss Rate
41%
Win Rate
59%
Profit-Loss Ratio
2.18
Alpha
0.096
Beta
0.141
Annual Standard Deviation
0.159
Annual Variance
0.025
Information Ratio
-1.637
Tracking Error
0.545
Treynor Ratio
1.836
Total Fees
$1696.75
Estimated Strategy Capacity
$210000.00
Lowest Capacity Asset
SOLUSD 2MN
from AlgorithmImports import *

class RebalancingPremiumsinCryptos(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2020, 1, 1)
        self.SetEndDate(2022, 1, 1)
        self.SetCash(100000)
        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"]
        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):
                self.tickers.append(Symbol.Create(csv[1], SecurityType.Crypto, Market.FTX))
        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.Schedule.On(self.DateRules.EveryDay(),
                         self.TimeRules.At(1, 0),
                         self.DailyRebalance)
                         
        # 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:
            self.Debug(f"Daily rebalance method triggered at {self.Time}")
            weight = 1.0/len(self.ActiveSecurities)
            
            targets = []
            
            for symbol in self.ActiveSecurities.Keys:
                if self.CurrentSlice.ContainsKey(symbol) and self.CurrentSlice[symbol] is not None:
                    
                    #Drawdown Protection - don't let any crypto drop by more than 30% on a single day
                    if self.Portfolio.Invested:
                        if self.CurrentSlice[symbol].High > self.high:
                            self.high = self.CurrentSlice[symbol].High
                        close = self.CurrentSlice[symbol].Close
                        dd = (close - self.high) / close
                        if dd < -0.30:
                            self.Liquidate(symbol)
                            self.high = 0
                    
                    else:
                        targets.append(PortfolioTarget(symbol, weight))
                    

            if targets:
                self.SetHoldings(targets)