Overall Statistics
Total Orders
53232
Average Win
0.04%
Average Loss
-0.02%
Compounding Annual Return
-3.049%
Drawdown
90.400%
Expectancy
0.132
Start Equity
100000000.00
End Equity
73529739.83
Net Profit
-26.470%
Sharpe Ratio
0.007
Sortino Ratio
0.006
Probabilistic Sharpe Ratio
0.176%
Loss Rate
61%
Win Rate
39%
Profit-Loss Ratio
1.90
Alpha
-0.244
Beta
0.193
Annual Standard Deviation
0.328
Annual Variance
0.108
Information Ratio
-1.963
Tracking Error
0.648
Treynor Ratio
0.011
Total Fees
$978466.04
Estimated Strategy Capacity
$0
Lowest Capacity Asset
BATUSD E3
Portfolio Turnover
1.82%
# https://quantpedia.com/strategies/rebalancing-premium-in-cryptocurrencies/
#
# The investment universe consists of 27 cryptocurrencies: BAT (Basic Attention Token), BTC (Bitcoin), BTG (Bitcoin Gold),
# DAI (Dai), DATA (Data Coin), DGB (DigiByte), EOS (EIS.io), ETH (Ethereum), FUN (FUN Token), IOTA (Iota), LRC (Loopring token),
# LTC (Litecoin), MANA (Mana coin), NEO (Neo), OMG (OMG, Formally known as OmiseGo), REQ (Request), SAN (Santiment Network Token),
# SNT (Status), TRX (Tron), WAX (Wax), XLM (Stellar), XMR (Monero), XRP (Ripple), XVG (Verge), ZEC (Zcash), ZIL (Zilliqa) and ZRX (0x).
# Two portfolios are created. The first portfolio is the daily rebalanced portfolio of all 27 cryptos to ensure that the assets have equal weights.
# The second portfolio is not rebalanced at all: an investor buys the equally-weighted crypto portfolio and lets the weights drift.
# Then the investor goes long the first portfolio and shorts the second portfolio with 70% weight.
#
# QC Implementation:
#   - BTGUSD is not traded due to data error.

from AlgorithmImports import *

class RebalancingPremiumInCryptocurrencies(QCAlgorithm):

    def initialize(self) -> None:
        self.SetStartDate(2015, 1, 1)
        self.SetCash(100000000)
        
        self.cryptos = [
            "BTCUSD", 
            "BATUSD", 
            # "BTGUSD", 
            "DAIUSD",
            "DGBUSD", "EOSUSD",
            "ETHUSD", "FUNUSD",
            "LTCUSD", "NEOUSD",
            "OMGUSD", "SNTUSD",
            "TRXUSD", "XLMUSD",
            "XMRUSD", "XRPUSD",
            "XVGUSD", "ZECUSD",
            "ZRXUSD", "LRCUSD",
            "REQUSD", "SANUSD",
            # "WAXUSD", "ZILUSD",
            "IOTAUSD", 
            "MANAUSD",
            "DATAUSD"
        ]

        self.short_side_percentage = 0.7
        self.data = {}
        self.SetBrokerageModel(BrokerageName.Bitfinex)
        
        for crypto in self.cryptos:
            # GDAX is coinmarket, but it doesn't support this many cryptos, so we choose Bitfinex
            data = self.AddCrypto(crypto, Resolution.Minute, Market.Bitfinex)
            data.SetFeeModel(CustomFeeModel())
            data.SetLeverage(10)
            
            self.data[crypto] = SymbolData()

        self.was_traded_already = False         # wait for the price data to come only once
        self.prev_short_portfolio_equity = 0    # short leg equity tracking

    def OnData(self, data):
        if not (self.Time.hour == 9 and self.Time.minute == 30):
            return
        
        all_cryptos_are_ready = True       # data warmup flag

        # check if all cryptos has ready data
        for crypto in self.cryptos:
            if crypto in data and data[crypto]:
                # update crypto price for weight calculation
                self.data[crypto].last_price = data[crypto].Value
            # if there is at least one crypto, which doesn't have data, then don't trade and break cycle
            else:
                all_cryptos_are_ready = False
                break
        
        if all_cryptos_are_ready or self.was_traded_already:
            self.was_traded_already = True
            
            # long strategy equity calculation
            long_portfolio_equity = self.Portfolio.TotalPortfolioValue
            long_equity_to_trade = long_portfolio_equity / len(self.cryptos)
            
            # short strategy equity calculation
            short_portfolio_equity = self.Portfolio.TotalPortfolioValue * self.short_side_percentage
            short_equity_to_trade = short_portfolio_equity / len(self.cryptos)

            # trading/rebalance
            for crypto, symbol_obj in self.data.items():
                if crypto in data and data[crypto]:
                    # short strategy
                    if not self.Portfolio[crypto].Invested:
                        short_q = np.floor(short_equity_to_trade / symbol_obj.last_price)
                        if abs(short_q) >= self.Securities[crypto].SymbolProperties.MinimumOrderSize:
                            self.MarketOrder(crypto, -short_q)

                    # long strategy
                    long_q = np.floor(long_equity_to_trade / symbol_obj.last_price)
                    # currency was traded before
                    if symbol_obj.quantity is not None:
                        # calculate quantity difference
                        diff_q = long_q - symbol_obj.quantity
                    
                        # rebalance position
                        if abs(diff_q) >= self.Securities[crypto].SymbolProperties.MinimumOrderSize:
                            self.MarketOrder(crypto, diff_q)
                            
                            # change new quantity
                            symbol_obj.quantity += diff_q
                    else:
                        # rebalance position
                        if abs(long_q) >= self.Securities[crypto].SymbolProperties.MinimumOrderSize:
                            self.MarketOrder(crypto, long_q)
                        
                            # change new quantity
                            symbol_obj.quantity = long_q
    
class SymbolData():
    def __init__(self):
        self.last_price = None
        self.quantity = None
    
# Custom fee model.
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))