Overall Statistics
Total Trades
13187
Average Win
0.11%
Average Loss
-0.12%
Compounding Annual Return
114.418%
Drawdown
32.600%
Expectancy
0.262
Net Profit
537.995%
Sharpe Ratio
1.967
Probabilistic Sharpe Ratio
78.098%
Loss Rate
35%
Win Rate
65%
Profit-Loss Ratio
0.94
Alpha
0.626
Beta
0.343
Annual Standard Deviation
0.441
Annual Variance
0.194
Information Ratio
0.296
Tracking Error
0.558
Treynor Ratio
2.527
Total Fees
$29484.98
Estimated Strategy Capacity
$40000.00
Lowest Capacity Asset
SXPUSD 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.FTX, AccountType.Cash)
        self.Settings.FreePortfolioValuePercentage = 0.05
        self.week = -1
        self.readyToTrade = False
        self.high = 0

        self.SetWarmup(timedelta(40))
        
        
        # 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):
                self.tickers.append(Symbol.Create(csv[1], SecurityType.Crypto, Market.FTX))
        
        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)
                         
        # Configs
        self.stop_loss = False
                         
        # Benchmark indicators
        self.BenchmarkCrypto = self.AddCrypto("BTCUSD", Resolution.Daily).Symbol

        self.macd = self.MACD("BTCUSD", 20, 50, 9, MovingAverageType.Exponential, Resolution.Daily)

        self.sma = self.SMA("BTCUSD", 40, Resolution.Daily)
                         
    # 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.Securities[self.BenchmarkCrypto].Price < self.sma.Current.Value:
            # f 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)