Overall Statistics
Total Trades
2028
Average Win
0.20%
Average Loss
-0.12%
Compounding Annual Return
6.429%
Drawdown
8.500%
Expectancy
0.320
Net Profit
48.338%
Sharpe Ratio
1.024
Probabilistic Sharpe Ratio
50.168%
Loss Rate
50%
Win Rate
50%
Profit-Loss Ratio
1.64
Alpha
0.048
Beta
-0.002
Annual Standard Deviation
0.044
Annual Variance
0.002
Information Ratio
-1.734
Tracking Error
0.623
Treynor Ratio
-19.33
Total Fees
$6954.20
Estimated Strategy Capacity
$9400.00
# The investment universe consists of 11 cryptocurrencies (the full list can be found in the paper).
# The raw carry metric is defined as the negative of the sum total coin issuance over the preceding seven days,
# divided by the coins outstanding at the beginning of those seven days (the data are available at coinmetrics.io).
# After that, the raw carry factor is standardized by the volatility of the underlying currency return, separately for each currency.
# The portfolio is equally weighted, where the absolute weight is 10% divided by n,
# where 10% is the gross exposure limit (only 10% of the portfolio is invested in cryptocurrencies), and n is the number of currencies available for investment.
# The weight is positive when the longitudinally standardized value factor is above zero and negative when this factor is below zero;
# this allows portfolios that can be net long or short the market.
# However, it is not possible to short cryptocurrencies, and the practical application would require, for example, a long-only strategy.
# The portfolio is rebalanced weekly. Last but not least, there are two weighting schemes, the second one is risk-based and more information about it is in the paper,
# we have chosen the equally-weighted strategy for representative purposes.
# 
# QC Implementation:
#   - The raw carry metric is defined as sum total coin issuance over the preceding seven days, divided by the coins outstanding at the beginning of those seven days. => negative part is ommited.

class FactorInvestingCryptos(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetCash(100000)
        
        # Coin issuance seven days data and data about first of those seven days
        self.data = {}
        self.period = 7
        self.count_days = 1
        self.mom = {}
        
        self.symbols = {'BTC' : 'BTCUSD',
                        'ETH' : 'ETHUSD', 
                        'LTC' : 'LTCUSD', 
                        'DASH' : 'DASHUSD',
                        'ETC' : 'ETCUSD',
                        'XMR' : 'XMRUSD', #Monero
                        'ZEC' : 'ZECUSD',
                        'XRP': 'XRPUSD'}
        
        self.SetBrokerageModel(BrokerageName.Bitfinex)  
                        
        for symbol in self.symbols:
            currency = self.symbols[symbol]
            data = self.AddCrypto(currency, Resolution.Daily, Market.Bitfinex)
            
            self.AddData(CryptoNetworkData, symbol, Resolution.Daily)
            
            self.data[symbol] = SymbolData(self.period)
            
            self.mom[currency] = self.MOM(currency, self.period, Resolution.Daily)
            
        self.SetBenchmark('BTCUSD')    
        self.Schedule.On(self.DateRules.EveryDay('BTCUSD'), self.TimeRules.AfterMarketOpen('BTCUSD'), self.Rebalance)

    def OnData(self, data):
        # Store daily data.
        for symbol in self.symbols:
            if symbol in data:
                if data[symbol]:
                    coin_issuance = data[symbol].Price
                    if coin_issuance != 0:
                        self.data[symbol].update(coin_issuance)
        
    def Rebalance(self):
        if self.count_days == 7:
            self.count_days = 1
        else:
            self.count_days = self.count_days + 1
            return
        
        long = []
        short = []
        
        for symbol in self.symbols:
            currency = self.symbols[symbol]
            
            if self.data[symbol].is_ready():
                carry_metric = self.data[symbol].carry_metric()
                
                if carry_metric > 0:
                    long.append(currency)
                else:
                    short.append(currency)
        
        # Trade Execution
        self.Liquidate()
        
        long_length = len(long)
        short_length = len(short)
        
        for symbol in self.mom:
            price = self.Securities[symbol].Price
            if symbol in long:
                if price == 0: return
                if self.mom[symbol].IsReady and self.mom[symbol].Current.Value > 0:
                    self.SetHoldings(symbol, 0.1/long_length)
            
            if symbol in short:
                if price == 0: return
                if self.mom[symbol].IsReady and self.mom[symbol].Current.Value < 0:
                    self.SetHoldings(symbol, -0.1/long_length)
        
class SymbolData():
    def __init__(self, period):
        self.SevenDaysData = RollingWindow[float](period)
        
    def update(self, value):
        self.SevenDaysData.Add(value)
        
    def carry_metric(self):
        seven_days_data = [x for x in self.SevenDaysData]

        # return -1 * (sum(seven_days_data) / seven_days_data[-1])
        return (sum(seven_days_data) / seven_days_data[-1])
        
    def is_ready(self):
        return self.SevenDaysData.IsReady
        
# Crypto network data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
# Data source: https://coinmetrics.io/community-network-data/
class CryptoNetworkData(PythonData):
    def GetSource(self, config, date, isLiveMode):
        return SubscriptionDataSource("anstochibamu.github.io/wp-content/uploads/backtesting_data/cryptos/{0}_network_data.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)

    # File exmaple:
    # date;AdrActCnt;BlkCnt;BlkSizeByte;BlkSizeMeanByte;CapMrktCurUSD;DiffMean;FeeMeanNtv;FeeMeanUSD;FeeMedNtv;FeeMedUSD;FeeTotNtv;FeeTotUSD;HashRate;IssContNtv;IssContPctAnn;IssContUSD;IssTotNtv;IssTotUSD;NVTAdj;NVTAdj90;PriceBTC;PriceUSD;ROI1yr;ROI30d;SplyCur;SplyExpFut10yrCMBI;SplyFF;TxCnt;TxTfrCnt;TxTfrValAdjNtv;TxTfrValAdjUSD;TxTfrValMeanNtv;TxTfrValMeanUSD;TxTfrValMedNtv;TxTfrValMedUSD;TxTfrValNtv;TxTfrValUSD;VtyDayRet180d;VtyDayRet30d;VtyDayRet60d
    # 2020-10-04;472428;6373;236724055;37144.83838067;39770015476.68894984938121513;3253577961269075.5;0.0059853576088997875;2.108877310104448675043701222;0.002595411;0.914465555100157778991;5898.28262640756875328;2078197.362997773340673611063;239.9958183136524;13264.5625;4.28948;4673627.9956135026858125;13264.5625;4673627.9956135026858125;43.17691049;25.628952086342867;0.033035064603976656;352.339400233781;99.98284398079014;-8.878309096289607;112874164.65573;161002502.63354883;108729987.128107404906339777;985452;663574;2614225.134803792;921094516.0728433572443652976;6.26362963;2206.92350712073959853103;0.21962698328610455;77.3832395661807212638788078;4156381.766019126322793441;1464457058.581802442738964697;0.04140807102894915;0.04751249797286342;0.04822674537536073
    def Reader(self, config, line, date, isLiveMode):
        data = CryptoNetworkData()
        data.Symbol = config.Symbol

        if not line[0].isdigit(): return None
        split = line.split(',')
        
        data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1)
        data['SplyCur'] = float(split[27])
        data.Value = float(split[27])
            
        return data