“该策略涉及为11种加密货币构建每周重新平衡的投资组合,结合等权重基准和基于动量、价值和套利的因子投资组合,并对货币中的负权重进行调整。”

I. 策略概要

投资范围包括11种加密货币,该策略涉及构建等权重基准投资组合和基于动量、价值和套利的因子复合投资组合。基准在每个重新平衡日将10%的风险敞口预算平均分配给所有可用代币,并持有投资组合直至下一次重新平衡。因子型投资组合与基准相结合,以创建增强型投资组合。如果因子投资组合导致任何货币出现负权重,则将其调整为零。投资组合每周重新平衡。

II. 策略合理性

尽管样本期仅四年多,但由于加密货币市场的高波动性,该研究得出了有意义的结论。研究表明,动量、价值和套利因子相结合,可提供强大的风险调整后回报,超越了单个动量策略的表现。尽管动量单独表现优于套利和价值,但这些三个因子的结合增强了整体回报,证实了它们的互补性。这表明混合动量、价值和套利可以提高表现,表明这些因子在加密货币投资组合中具有有效的作用,因为它们分散了风险并产生了优于单独使用动量的回报。

III. 来源论文

‘Know When to Hodl ‘Em, Know When to Fodl ‘Em’: An Investigation of Factor Based Investing in the Cryptocurrency Space [点击查看论文]

<摘要>

自Fama和French(1992)的开创性工作以来,人们至少已经知道,存在特定的属性,即所谓的因子,这些因子有助于预测个别资产高于更广泛市场回报的回报。由于这些预测性特征是样本外产生的(当前可观察的因子值预测未来回报),投资者可以通过构建与因子一致的投资组合来获得超额回报。此类因子最初在回报的横截面中引入,并侧重于个别股权证券,此后,此类因子的有效性也在资产类别层面上得到了证明,并且发现不仅在横截面中有效,而且在纵向(针对个别资产,随时间推移)上也有效。价值、动量和套利等因子被发现广泛适用于不同的资产类别、证券范围、国家和时间段,以至于Asness等人在其2013年发表于《金融杂志》的具有影响力的论文中简单地将其命名为“价值和动量无处不在”。我们的论文首次将基于动量、价值和套利的因子投资应用于加密货币。我们表明,这些相同的因子在这个相对较新且未开发的资产类别中是有效的,允许构建可以获得超过整个加密货币“市场”超额回报的投资组合。

IV. 回测表现

年化回报38.3%
波动率13.2%
β值0.229
夏普比率2.91
索提诺比率1.163
最大回撤N/A
胜率64%

V. 完整的 Python 代码

import numpy as np
from AlgorithmImports import *
from typing import List, Dict
class BlendedFactorsinCryptocurrencies(QCAlgorithm):
    def Initialize(self) -> None:
        self.SetStartDate(2015, 1, 1)
        self.SetCash(1_000_000)
        
        self.period: int = 7
        self.count_days: int = 1
        self.percentage_traded: float = 0.1
        
        self.symbols: Dict[str, str] = {
            'BTC' : 'BTCUSD',
            'ETH' : 'ETHUSD', 
            'LTC' : 'LTCUSD', 
            'ETC' : 'ETCUSD',
            'XMR' : 'XMRUSD',
            'ZEC' : 'ZECUSD'
        }
        
        self.data: Dict[str, SymbolData] = {}
        self.SetBrokerageModel(BrokerageName.Bitfinex)
                        
        for crypto, ticker in self.symbols.items():
            data: Securities = self.AddCrypto(ticker, Resolution.Daily, Market.Bitfinex)
            self.AddData(CryptoNetworkData, crypto, Resolution.Daily)
            self.data[crypto] = SymbolData(self.period)
            
    def OnData(self, data: Slice) -> None:
        crypto_data_last_update_date: Dict[Symbol, datetime.date] = CryptoNetworkData.get_last_update_date()
        # Store daily price data.
        for crypto, ticker in self.symbols.items():
            if crypto in data and data[crypto]:
                cap_mrkt_cur_usd: float = data[crypto].Capmrktcurusd
                txtfr_val_adj_usd: float = data[crypto].Txtfrvaladjusd
                coin_issuance: float = data[crypto].Price
                
                if cap_mrkt_cur_usd != 0 and txtfr_val_adj_usd != 0 and coin_issuance != 0:
                    self.data[crypto].update_data(cap_mrkt_cur_usd, txtfr_val_adj_usd, coin_issuance)
            
            if ticker in data:
                if data[ticker]:
                    self.data[crypto].update_price(data[ticker].Price)
        
        if self.Time.date().weekday() != 0:
            return
        if self.count_days == 7:
            self.count_days = 1
        else:
            self.count_days = self.count_days + 1
            return
        
        symbols_ready = [x for x in self.symbols if self.data[x].is_ready() and self.Securities[x].GetLastData() and self.Time.date() < crypto_data_last_update_date[x]]
        if len(symbols_ready) == 0:
            self.Liquidate()
            return
        
        weight: Dict[ticker, float] = {}
        partial_weight: float = self.percentage_traded / len(symbols_ready)
        
        carry_metric_long: List[str] = []
        carry_metric_short: List[str] = []
        valuation_metric_long: List[str] = []
        valuation_metric_short: List[str] = []
        momentum_long: List[str] = []
        momentum_short: List[str] = []
        
        for crypto in symbols_ready:
            ticker: str = self.symbols[crypto]
            weight[ticker] = partial_weight   # Set benchmark weight.
            
            carry_metric: float = self.data[crypto].carry_metric()
            valuation_metric: float = self.data[crypto].valuation_metric()
            momentum: float = self.data[crypto].momentum()
            
            carry_metric_long.append(ticker) if carry_metric > 0 else carry_metric_short.append(ticker)
            valuation_metric_long.append(ticker) if valuation_metric > 0 else valuation_metric_short.append(ticker)
            momentum_long.append(ticker) if momentum > 0 else momentum_short.append(ticker)
        
        for i, portfolio in enumerate([[carry_metric_long, valuation_metric_long, momentum_long], [carry_metric_short, valuation_metric_short, momentum_short]]):
            for sub_portfolio in portfolio:
                for ticker in sub_portfolio:
                    weight[ticker] += ((-1)**i) * self.percentage_traded / len(sub_portfolio)
        # 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)
        for symbol, w in weight.items():
            if symbol in data and data[symbol]:
                self.SetHoldings(symbol, w)
class SymbolData():
    def __init__(self, period: int) -> None:
        self.coin_issuance: RollingWindow = RollingWindow[float](period)
        self.transactions: RollingWindow = RollingWindow[float](period)
        self.curr_market_cap: float = 0.
        self.Price: RollingWindow = RollingWindow[float](period)
        
    def update_data(self, current_market_value: float, num_of_transactions: int, coin_issuance: float) -> None:
        self.transactions.Add(num_of_transactions)
        self.curr_market_cap = current_market_value
        self.coin_issuance.Add(coin_issuance)
        
    def update_price(self, price: float) -> None:
        self.Price.Add(price)
        
    def carry_metric(self) -> float:
        seven_days_coin_issuance: List[float] = [x for x in self.coin_issuance]
        # return -1 * (sum(seven_days_coin_issuance) / seven_days_coin_issuance[-1])
        return (sum(seven_days_coin_issuance) / seven_days_coin_issuance[-1])
    def valuation_metric(self) -> float:
        trailing_data: List[float] = [x for x in self.transactions]
        return self.curr_market_cap / np.mean(trailing_data)
    
    def momentum(self) -> float:
        prices: List[float] = [x for x in self.Price]
        return prices[0] / prices[-1] - 1
        
    def is_ready(self) -> bool:
        return self.coin_issuance.IsReady and self.transactions.IsReady and self.Price.IsReady
        
# 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
        try:
            cols:str = ['SplyCur', 'CapMrktCurUSD', 'TxTfrValAdjUSD']
            if not line[0].isdigit():
                header_split = line.split(',')
                self.col_index = [header_split.index(x) for x in cols]
                return None
            split = 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

发表评论

了解 Quant Buffet 的更多信息

立即订阅以继续阅读并访问完整档案。

继续阅读