Quant BuffetRelax, Not Over Thinking

Growth of Addresses with Balance Factor in Cryptos

Log in to collect

Academic paper

Value Premium, Network Adoption, and Factor Pricing of Crypto Assets

AuthorsLin William Cong; George Andrew Karolyi; Ke Tang; Weiyi Zhao

Institute
  • Cornell University
  • ?Cornell University - Samuel Curtis Johnson Graduate School of Management
  • Tsinghua University
  • ?Institute of Economics, School of Social Sciences, Tsinghua University

Strategy in a nutshell

The investment universe consists of cryptocurrencies from CoinMarketCap.com for which network adoption data are available. Stablecoins, and coins with zero price, market capitalization, or trading volume in all periods, are excluded. Network adoption indicators are sourced from Intotheblock. First, compute the weekly growth of addresses with balance, defined as the log difference between the most recent total addresses with balance and the total addresses with balance from the previous week. Cryptocurrencies are then sorted into quintiles based on this growth. The strategy goes long the highest quintile and short the lowest quintile. Portfolios are value-weighted and rebalanced weekly.

Economic rationale

The number of addresses with balance serves as a proxy for cryptocurrency adoption. While both total addresses and total addresses with balance are examined, the latter is a more accurate measure of active adoption. Empirical results show that cryptocurrencies with the highest growth in addresses with balance outperform those with the lowest growth. This demonstrates that trading strategies can be effectively constructed using fundamental network adoption data of individual cryptocurrencies.

Backtest performance

Annualised return66.86%
Volatility17.08%
Beta0.031
Sharpe ratio3.91
Sortino ratio-0.215
Win rate59%

Full Python code

from AlgorithmImports import *
from typing import List, Dict
#endregion

import data_tools

class CrosssectionalMomentumInLargeCryptos(QCAlgorithm):

def Initialize(self) -> None:
self.SetStartDate(2015, 1, 1)
self.SetCash(1_000_000)

self.period: int = 7         # need n of daily addresses
self.quantile: int = 5
self.portfolio_percentage: float = .1
self.leverage: int = 10

self.cryptos: List[str] = [
    "ANT",  # Aragon
    "BAT",  # Basic Attention Token
    "BTC",  # Bitcoin
    "DAI",  # Dai
    "DASH", # Dash
    "DOGE", # Dogecoin
    "EOS",  # EOS
    "ETH",  # Ethereum
    "FUN",  # FUN Token
    "LTC",  # Litecoin
    "MKR",  # Maker
    "OMG",  # OMG Network
    "SNT",  # Status
    "ZEC",  # Zcash
    "ZRX"   # Ox
]

self.data: Dict[str, data_tools.SymbolData] = {}
self.weight: Dict[str, float] = {}
self.SetBrokerageModel(BrokerageName.Bitfinex)

for crypto in self.cryptos:
    crypto_symbol: str = crypto + 'USD'
    data: Securities = self.AddCrypto(crypto_symbol, Resolution.Daily, Market.Bitfinex)
    data.SetLeverage(self.leverage)
    
    network_symbol: Symbol = self.AddData(data_tools.CryptoNetworkData, crypto, Resolution.Daily).Symbol
    address_symbol: Symbol = self.AddData(data_tools.CryptoAddressesData, crypto, Resolution.Daily).Symbol
    self.data[crypto_symbol] = data_tools.SymbolData(network_symbol, address_symbol, self.period)

self.rebalance_flag: bool = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.Schedule.On(self.DateRules.WeekStart("BTCUSD"), self.TimeRules.At(9, 30), self.Rebalance)

def OnData(self, data: Slice) -> None:
curr_date: datetime.date = self.Time.date()

# daily updating of crypto prices and market capitalization(CapMrktCurUSD)
for crypto, symbol_obj in self.data.items():
    network_symbol: Symbol = symbol_obj.network_symbol
    address_symbol: Symbol = symbol_obj.address_symbol
    
    if network_symbol in data and data[network_symbol]:
        # get market capitalization
        cap_mrkt_cur_usd: float = data[network_symbol].Value
        if cap_mrkt_cur_usd != 0.:
            self.data[crypto].update_cap(cap_mrkt_cur_usd, curr_date)
        
    if address_symbol in data and data[address_symbol]:
        # get value of total with balance addresses 
        addresses_count: int = data[address_symbol].Value
        if addresses_count != 0.:
            self.data[crypto].update_addresses_count_values(addresses_count, curr_date)

if not self.rebalance_flag:
    return

# trade execution
invested: List[str] = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
for ticker in invested:
    if ticker not in self.weight:
        self.Liquidate(ticker)

for ticker, w in self.weight.items():
    if ticker in data and data[ticker]:
        self.SetHoldings(ticker, w * self.portfolio_percentage)

self.rebalance_flag = False
self.weight.clear()

def Rebalance(self) -> None:
self.rebalance_flag = True

curr_date: datetime.date = self.Time.date()
crypto_data_last_update_date: Dict[Symbol, datetime.date] = data_tools.CryptoNetworkData.get_last_update_date()
crypto_addresses_last_update_date: Dict[Symbol, datetime.date] = data_tools.CryptoAddressesData.get_last_update_date()

address_growth: Dict[str, float] = {}

for crypto, symbol_obj in self.data.items():
    network_symbol: Symbol = symbol_obj.network_symbol
    address_symbol: Symbol = symbol_obj.address_symbol

    if network_symbol not in crypto_data_last_update_date or address_symbol not in crypto_addresses_last_update_date:
        continue

    # crypto doesn't have enough data
    if self.Securities[network_symbol].GetLastData() and self.Time.date() > crypto_data_last_update_date[network_symbol] or \
        self.Securities[address_symbol].GetLastData() and self.Time.date() > crypto_addresses_last_update_date[address_symbol]:
        continue

    if symbol_obj.is_ready():
        # calculate address growth for current crypto
        if symbol_obj.cap_mrkt_cur_usd != 0:
            address_growth[crypto] = symbol_obj.address_growth()

# not enough cryptos for selection    
if len(address_growth) < self.quantile:
    self.Liquidate()    
    return

# perform selection
quantile: int = int(len(address_growth) / self.quantile)
sorted_by_perf: List[str] = [x[0] for x in sorted(address_growth.items(), key=lambda item: item[1])]

# long highest quantile
long: List[str] = sorted_by_perf[-quantile:]
# short lowest quantile
short: List[str] = sorted_by_perf[:quantile]

# value weighting
for i, portfolio in enumerate([long, short]):
    mc_sum:float = sum(list(map(lambda ticker: self.data[ticker].cap_mrkt_cur_usd, portfolio)))
    for ticker in portfolio:
        self.weight[ticker] = ((-1) ** i) * self.data[ticker].cap_mrkt_cur_usd / mc_sum