Overall Statistics
Total Trades
320
Average Win
9.58%
Average Loss
-7.16%
Compounding Annual Return
95.369%
Drawdown
41.100%
Expectancy
0.461
Net Profit
6968.899%
Sharpe Ratio
1.732
Probabilistic Sharpe Ratio
82.122%
Loss Rate
38%
Win Rate
62%
Profit-Loss Ratio
1.34
Alpha
0.695
Beta
0.037
Annual Standard Deviation
0.425
Annual Variance
0.181
Information Ratio
-0.517
Tracking Error
0.732
Treynor Ratio
19.883
Total Fees
$1258550.95
Estimated Strategy Capacity
$6300000.00
# The investment universe consists of 1 cryptocurrency, Bitcoin.
# 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.
# It is not possible to short cryptocurrencies, and the practical application would require, a long-only strategy.
# The portfolio is rebalanced weekly. Last but not least, there are two weighting schemes, the second one is risk-based,
# 
# 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 BitcoinAndLove(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_period = 14
        
        self.SetBrokerageModel(BrokerageName.Bitfinex)  
        
        self.symbols = {'BTC' : 'BTCUSD'}                
        self.symbol = 'BTC'
        self.currency = self.symbols[self.symbol]
        data = self.AddCrypto(self.currency, Resolution.Daily, Market.Bitfinex)
        self.AddData(CryptoNetworkData, self.symbol, Resolution.Daily)
        self.data[self.symbol] = SymbolData(self.period)
        self.mom = self.MOM(self.currency, self.mom_period, Resolution.Daily)
        
        self.MAs = [
            self.MOM(self.symbol, 1, Resolution.Daily),
            self.MOM(self.symbol, 2, Resolution.Daily),
            self.MOM(self.symbol, 4, Resolution.Daily),
            self.MOM(self.symbol, 10, Resolution.Daily),
            self.MOM(self.symbol, 20, Resolution.Daily)
            ]
            
        self.SetBenchmark(self.currency)    
        self.Schedule.On(self.DateRules.EveryDay(self.currency), self.TimeRules.AfterMarketOpen(self.currency), self.Rebalance)

    def OnData(self, data):
        # Store daily data.
        if self.symbol in data:
            if data[self.symbol]:
                coin_issuance = data[self.symbol].Price
                if coin_issuance != 0:
                    self.data[self.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
        
        price = self.Securities[self.currency].Price
        if price == 0: return
        
        long_signal_count = 0
        for ma in self.MAs:
            if ma.IsReady:
                if price > ma.Current.Value:
                    long_signal_count += 1
        else:
            self.Liquidate()
        
        w = (1 / len(self.MAs)) * long_signal_count
        
        if self.data[self.symbol].is_ready() and self.mom.IsReady:
            carry_metric = self.data[self.symbol].carry_metric()
            if carry_metric > 0 and self.mom.Current.Value > 10:
                self.SetHoldings(self.currency, w)
        
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