The strategy trades crypto-assets, longing the smallest market-cap quintile and shorting the largest, using value-weighted portfolios rebalanced weekly, excluding stablecoins and select assets with limited fundamental data.

I. STRATEGY IN A NUTSHELL

Rank crypto-assets by market capitalization weekly. Go long on the smallest-cap quintile and short the largest-cap quintile, using value-weighted portfolios rebalanced every Tuesday.

II. ECONOMIC RATIONALE

Smaller-cap cryptos earn higher returns due to lower liquidity and higher risk. The size factor, proven in equities, delivers a persistent premium in crypto markets, independent of other risk exposures.

III. SOURCE PAPER

Is There a Value Premium in Cryptoasset Markets? [Click to Open PDF]

Luca Liebi, University of St. Gallen – Swiss Institute of Banking and Finance

<Abstract>

This study examines if blockchain fundamentals determine cryptoasset prices. Previous research has shown that non-fundamental factors affect cryptoasset prices however, whether blockchain fundamentals affect cryptoasset prices, lacks empirical research. Using data for 652 cryptoassets, I specify active addresses-to-network value as a valuation metric that captures transaction benefits. I find anomalous returns that increase with active addressesto-network value ratio, a proxy for the value anomaly. Cryptoassets with a high active addresses-to-network value ratio yield on average 3.7 percentage points higher weekly returns than cryptoassets with low active addresses-to-network value ratio, and comparable size. A four-factor model directed at capturing the value pattern in average returns performs better than the three-factor model. Importantly, my results suggest that cryptoasset prices are related to their blockchain fundamentals.

IV. BACKTEST PERFORMANCE

Annualised Return134.2%
Volatility70.31%
Beta-0.003
Sharpe Ratio1.91
Sortino Ratio-1.031
Maximum DrawdownN/A
Win Rate45%

V. FULL PYTHON CODE

from AlgorithmImports import *
from typing import List, Dict
#endregion
class SizeInCryptocurrencies(QCAlgorithm):
    def Initialize(self) -> None:
        self.SetStartDate(2015, 1, 1)
        self.SetCash(1_000_000)
        
        self.period: int = 30
        self.quantile: int = 5
        self.percentage_traded: float = 0.1
        self.leverage: int = 5
                        
        self.crypto_symbols: Dict[str, str] = {
            'BTC' : 'BTCUSD',
            'ETH' : 'ETHUSD', 
            'LTC' : 'LTCUSD',
            'ETC' : 'ETCUSD',
            'ZEC' : 'ZECUSD',
            'EOS' : 'EOSUSD',
            'OMG' : 'OMGUSD',
            'NEO' : 'NEOUSD',
            'BAT' : 'BATUSD',
            'ZRX' : 'ZRXUSD',
            'TRX' : 'TRXUSD',
        }
        
        self.weight: Dict[str, float] = {}
        self.SetBrokerageModel(BrokerageName.Bitfinex)
        self.cap_mrkt_cur_usd: Dict[str, float] = {}
        
        for crypto, ticker in self.crypto_symbols.items():
            data: Securities = self.AddCrypto(ticker, Resolution.Daily, Market.Bitfinex)
            data.SetLeverage(self.leverage)
            
            self.AddData(CryptoNetworkData, crypto, Resolution.Daily).Symbol
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        
    def OnData(self, data: Slice) -> None:
        crypto_data_last_update_date: Dict[Symbol, datetime.date] = CryptoNetworkData._last_update_date
        cap_mrkt_cur_usd: Dict[str, float] = {}
        weight: Dict[str, float] = {}
        # Store daily data.
        for crypto, ticker in self.crypto_symbols.items():
            if ticker in data and data[ticker]:
                if crypto in crypto_data_last_update_date:
                    if self.Securities[crypto].GetLastData() and self.Time.date() <= crypto_data_last_update_date[crypto]:
                        cap_mrkt_cur: float = self.Securities[crypto].GetLastData().Capmrktcurusd
                        
                        if cap_mrkt_cur != 0:
                            cap_mrkt_cur_usd[ticker] = cap_mrkt_cur
        # tuesday
        if self.Time.date().weekday() == 1:
            # Number in quantile variable shouldn't be 0.
            if len(cap_mrkt_cur_usd) < self.quantile:
                self.Liquidate()
                return
            
            sorted_by_cap: List[str] = [x[0] for x in sorted(cap_mrkt_cur_usd.items(), key = lambda item: item[1], reverse=True)]
            quantile: int = int(len(sorted_by_cap) / self.quantile)
            
            # Long (short) the quintile of crypto-assets with the lowest (highest) size measure.
            long: List[str] = sorted_by_cap[-quantile:]
            short: List[str] = sorted_by_cap[:quantile] 
            
            for i, portfolio in enumerate([long, short]):
                mc_sum: float = sum(list(map(lambda x: cap_mrkt_cur_usd[x], portfolio)))
                for ticker in portfolio:
                    weight[ticker] = ((-1)**i) * cap_mrkt_cur_usd[ticker] / mc_sum
            
            # Trade execution
            invested: List[str] = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
            for symbol in invested:
                if symbol not in weight:
                    self.Liquidate(symbol)
            portfolio: List[PortfolioTarget] = [PortfolioTarget(ticker, w * self.percentage_traded) for ticker, w in weight.items() if ticker in data and data[ticker]]
            self.SetHoldings(portfolio)
        
# Crypto network data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
# Data source: https://coinmetrics.io/community-network-data/
class CryptoNetworkData(PythonData):
    _last_update_date: Dict[Symbol, datetime.date] = {}
    @staticmethod
    def get_last_update_date() -> Dict[Symbol, datetime.date]:
       return CryptoNetworkData._last_update_date
    def GetSource(self, config: SubscriptionDataConfig, date: datetime, isLiveMode: bool) -> SubscriptionDataSource:
        return SubscriptionDataSource(f"data.quantpedia.com/backtesting_data/crypto/{config.Symbol.Value}_network_data.csv", SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
    # File exmaple:
    # date,AdrActCnt,AdrBal1in100KCnt,AdrBal1in100MCnt,AdrBal1in10BCnt,AdrBal1in10KCnt,AdrBal1in10MCnt,AdrBal1in1BCnt,AdrBal1in1KCnt,AdrBal1in1MCnt,AdrBalCnt,AdrBalNtv0.001Cnt,AdrBalNtv0.01Cnt,AdrBalNtv0.1Cnt,AdrBalNtv100Cnt,AdrBalNtv100KCnt,AdrBalNtv10Cnt,AdrBalNtv10KCnt,AdrBalNtv1Cnt,AdrBalNtv1KCnt,AdrBalNtv1MCnt,AdrBalUSD100Cnt,AdrBalUSD100KCnt,AdrBalUSD10Cnt,AdrBalUSD10KCnt,AdrBalUSD10MCnt,AdrBalUSD1Cnt,AdrBalUSD1KCnt,AdrBalUSD1MCnt,AssetEODCompletionTime,BlkCnt,BlkSizeMeanByte,BlkWghtMean,BlkWghtTot,CapAct1yrUSD,CapMVRVCur,CapMVRVFF,CapMrktCurUSD,CapMrktFFUSD,CapRealUSD,DiffLast,DiffMean,FeeByteMeanNtv,FeeMeanNtv,FeeMeanUSD,FeeMedNtv,FeeMedUSD,FeeTotNtv,FeeTotUSD,FlowInExNtv,FlowInExUSD,FlowOutExNtv,FlowOutExUSD,FlowTfrFromExCnt,HashRate,HashRate30d,IssContNtv,IssContPctAnn,IssContPctDay,IssContUSD,IssTotNtv,IssTotUSD,NDF,NVTAdj,NVTAdj90,NVTAdjFF,NVTAdjFF90,PriceBTC,PriceUSD,ROI1yr,ROI30d,RevAllTimeUSD,RevHashNtv,RevHashRateNtv,RevHashRateUSD,RevHashUSD,RevNtv,RevUSD,SER,SplyAct10yr,SplyAct180d,SplyAct1d,SplyAct1yr,SplyAct2yr,SplyAct30d,SplyAct3yr,SplyAct4yr,SplyAct5yr,SplyAct7d,SplyAct90d,SplyActEver,SplyActPct1yr,SplyAdrBal1in100K,SplyAdrBal1in100M,SplyAdrBal1in10B,SplyAdrBal1in10K,SplyAdrBal1in10M,SplyAdrBal1in1B,SplyAdrBal1in1K,SplyAdrBal1in1M,SplyAdrBalNtv0.001,SplyAdrBalNtv0.01,SplyAdrBalNtv0.1,SplyAdrBalNtv1,SplyAdrBalNtv10,SplyAdrBalNtv100,SplyAdrBalNtv100K,SplyAdrBalNtv10K,SplyAdrBalNtv1K,SplyAdrBalNtv1M,SplyAdrBalUSD1,SplyAdrBalUSD10,SplyAdrBalUSD100,SplyAdrBalUSD100K,SplyAdrBalUSD10K,SplyAdrBalUSD10M,SplyAdrBalUSD1K,SplyAdrBalUSD1M,SplyAdrTop100,SplyAdrTop10Pct,SplyAdrTop1Pct,SplyCur,SplyExpFut10yr,SplyFF,SplyMiner0HopAllNtv,SplyMiner0HopAllUSD,SplyMiner1HopAllNtv,SplyMiner1HopAllUSD,TxCnt,TxCntSec,TxTfrCnt,TxTfrValAdjNtv,TxTfrValAdjUSD,TxTfrValMeanNtv,TxTfrValMeanUSD,TxTfrValMedNtv,TxTfrValMedUSD,VelCur1yr,VtyDayRet180d,VtyDayRet30d
    # 2009-01-09,19,19,19,19,19,19,19,19,19,19,19,19,19,0,0,19,0,19,0,0,0,0,0,0,0,0,0,0,1614334886,19,215,860,16340,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,9.44495122962963E-7,0,950,36500,100,0,950,0,1,0,0,0,0,1,0,0,0,0,11641.53218269,1005828380.584716757433,0,0,950,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,950,950,950,950,950,950,950,950,950,950,950,950,950,0,0,0,0,0,0,0,0,0,0,0,0,0,950,50,50,950,17070250,950,1000,0,1000,0,0,0,0,0,0,0,0,0,0,0,0,0
    def Reader(self, config: SubscriptionDataConfig, line: str, date: datetime, isLiveMode: bool) -> BaseData:
        data: CryptoNetworkData = CryptoNetworkData()
        data.Symbol = config.Symbol
        cols: List[str] = ['CapMrktCurUSD']
        try:
            if not line[0].isdigit():
                header_split = line.split(',')
                self.col_index = [header_split.index(x) for x in cols]
                return None
            split: str = line.split(',')
            
            data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1)
            
            for i, col in enumerate(cols):
                data[col] = float(split[self.col_index[i]])
            data.Value = float(split[self.col_index[0]])
            if config.Symbol.Value not in CryptoNetworkData._last_update_date:
                CryptoNetworkData._last_update_date[config.Symbol.Value] = datetime(1,1,1).date()
            if data.Time.date() > CryptoNetworkData._last_update_date[config.Symbol.Value]:
                CryptoNetworkData._last_update_date[config.Symbol.Value] = data.Time.date()
        except:
            return None
        
        return data

VI. Backtest Performance

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading