该策略投资于51种市值超过100万美元的活跃加密货币,使用5分钟的已实现波动率计算。首先,计算每日已实现波动率和正负半方差,接着得出符号跳跃度量。最终构建已实现符号跳跃度量(RSJ),根据RSJ值将加密货币分为五组,做多最低组,做空最高组。策略每日按等权重重新调整。

策略概述

投资领域由从FirstRateData获取的市值超过100万美元的51种交易最活跃的加密货币构成(研究过滤为“基于美元”)。已实现波动率的计算时间间隔为5分钟。首先,计算每日已实现波动率(回报的平方和)以及正负半方差(回报平方乘以指标,判断回报为正或负——见论文第6页)。接着,计算定义为正半方差减去负半方差的符号跳跃度量。最后,构建已实现符号跳跃度量(RSJ),其为符号跳跃除以已实现波动率。每天根据RSJ将加密货币分为五组,做多RSJ值最低的五分之一,做空RSJ值最高的五分之一。策略按等权重每日重新调整。

策略合理性

最常见的风险度量是波动率,它衡量回报的正负变化。波动率包括“好的”波动率(正半方差)和“坏的”波动率(负半方差)。研究发现,在预测未来已实现波动率时,正半方差比负半方差更重要。作者指出,较高的正半方差或较高的符号跳跃变化会导致未来较高的已实现波动率。相反,较高的负半方差和较小的符号跳跃变化会导致未来较低的波动率。

更大的“好”波动率和符号跳跃可能被认为是有吸引力的,尤其是在投资者希望跳跃会重复发生时。然而,这种赌注往往缺乏实质性的支持,导致更大的不确定性。总体来看,投资者基于过去表现并将其推测到未来的常规模式在加密货币市场中也成立。然而,现实并不那么乐观。较大的符号跳跃是未来表现的负面预测因素。

论文来源

Good Volatility, Bad Volatility, and the Cross Section of Cryptocurrency Returns [点击浏览原文]

<摘要>

本文研究了加密货币已实现变化度量(RVM)的分布特性及其对未来回报的可预测性。我们展示了加密货币波动性的持久性以及波动率预测中的不对称性的重要性。符号跳跃变化占加密货币回报二次变化的约18%。已实现符号跳跃(RSJ)强烈预测了未来超额回报的横截面变化。根据RSJ对加密货币进行排序得出的投资组合,在未来超额回报中表现出统计上和经济上显著的差异。即使在控制了加密货币市场特征和现有的风险因素后,这种跳跃风险溢价仍然显著。通过控制多个加密货币特征,标准横截面回归证实了加密货币回报的RSJ可预测性。投资者的关注解释了已实现跳跃风险对未来加密货币回报的可预测性。

回测表现

年化收益率54.81%
波动率12.36%
Beta-0
夏普比率4.43
索提诺比率-0.934
最大回撤N/A
胜率48%

完整python代码

import numpy as np
from datetime import datetime, timedelta
from AlgorithmImports import *
#endregion

class TheRealizedJumpsPredictCryptocurrencyReturns(QCAlgorithm):

    def Initialize(self) -> None:
        self.SetStartDate(2017, 1, 1)
        self.SetCash(1000000)
        
        self.min_prices: int = 60 * 6 # m minuties * n hours
        self.quantile: int = 5
        self.portfolio_percentage: float = .1
        self.leverage: int = 5

        self.cryptos: List[str] = [
            "BTC", # Bitcoin
            "EOS", # EOS
            "ETH", # Ethereum
            "LTC", # Litecoin
            "NEO", # Neo
            "OMG", # OMG Network
            "TRX", # Tron
            "XLM", # Stellar
            "XMR", # Monero
            "XRP", # XRP
            "XVG", # Verge
            "ZRX"  # Ox
        ]
        
        self.data: Dict[Symbol, SymbolData] = {}
        self.SetBrokerageModel(BrokerageName.Bitfinex)
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        
        for crypto in self.cryptos:
            crypto_ticker: str = crypto + 'USD'
            data: Securities = self.AddCrypto(crypto_ticker, Resolution.Minute, Market.Bitfinex)
            data.SetFeeModel(CustomFeeModel())
            data.SetLeverage(self.leverage)

            crypto_symbol: Symbol = data.Symbol
            network_symbol: Symbol = self.AddData(CryptoNetworkData, crypto, Resolution.Daily).Symbol

            self.data[crypto_symbol] = SymbolData(network_symbol)
        
        self.curr_day: int = -1

    def OnData(self, data: Slice) -> None:
        crypto_data_last_update_date: Dict[Symbol, datetime.date] = CryptoNetworkData.get_last_update_date()

        # rebalance daily
        if self.Time.day != self.curr_day: 
            self.curr_day = self.Time.day

            realized_signed_jump: Dict[Symbol, float] = {}

            for crypto_symbol, symbol_obj in self.data.items():
                network_symbol: Symbol = symbol_obj.network_symbol
                # make sure crypto data are ready and new data are still comming
                if self.Securities[network_symbol].GetLastData() and self.Time.date() > crypto_data_last_update_date[network_symbol]:
                    self.Liquidate()
                    return

                if symbol_obj.is_ready(self.min_prices):
                    # calculate crypto's realized signed jump
                    realized_signed_jump_value: float = symbol_obj.realized_signed_jump()

                    # clear crypto prices from last day
                    symbol_obj.prices.clear()

                    if realized_signed_jump_value != None:
                        realized_signed_jump[crypto_symbol] = realized_signed_jump_value

            weight: Dict[Symbol, float] = {}

            # trade only, when there are enough cryptos with realized signed jump value
            if len(realized_signed_jump) > self.quantile:
                # perform selection
                quantile: int = int(len(realized_signed_jump) / self.quantile)

                sorted_by_jump: List[Symbol] = [x[0] for x in sorted(realized_signed_jump.items(), key=lambda item: item[1])]
                
                # long bottom quantile
                long: List[Symbol] = sorted_by_jump[:quantile]

                # short top quantile
                short: List[Symbol] = sorted_by_jump[-quantile:]

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

            # trade execution
            portfolio: List[PortfolioTarget] = [PortfolioTarget(symbol, self.portfolio_percentage * w) for symbol, w in weight.items() if symbol in data and data[symbol]]
            if len(portfolio) != 0:
                self.SetHoldings(portfolio, True)
            
        # update prices each minute
        for crypto_symbol, symbol_obj in self.data.items():
            network_symbol: Symbol = symbol_obj.network_symbol
            
            if crypto_symbol in data.Bars and data[crypto_symbol]:
                # update prices
                price: float = data[crypto_symbol].Value
                self.data[crypto_symbol].update_prices(price)
            
            if network_symbol in data and data[network_symbol]:
                # get market capitalization
                cap_mrkt_cur_usd: float = data[network_symbol].Value
                self.data[crypto_symbol].update_cap(cap_mrkt_cur_usd)

class SymbolData():
    def __init__(self, network_symbol: Symbol) -> None:
        self.network_symbol: Symbol = network_symbol
        self.prices: List[float] = []
        self.cap_mrkt_cur_usd: Union[None, float] = None
            
    def update_prices(self, price: float) -> None:
        self.prices.append(price)
        
    def update_cap(self, cap_mrkt_cur_usd: float) -> None:
        self.cap_mrkt_cur_usd = cap_mrkt_cur_usd
            
    def is_ready(self, min_prices: int) -> bool: 
        return self.cap_mrkt_cur_usd and len(self.prices) >= min_prices

    def realized_signed_jump(self) -> Union[None, float]:
        # the time interval for the realized volatility computation is 5 minutes
        # compute the daily realized volatility (sum of squared returns)
        interval_length: int = 5
        prices_length: int = len(self.prices)

        # split minute prices to intervals with same length and for each interval calculate it's return
        intervals_returns: List[float] = [self.calc_return(self.prices[i:i+interval_length]) for i in range(0, prices_length, interval_length)]

        realized_volatility: float = sum(list(map(lambda interval_return: interval_return ** 2, intervals_returns)))

        # can't perform division by zero
        if realized_volatility == 0:
            return None
        
        positive_semivariance, negative_semivariance = 0, 0

        # calculate positive and negative semivarince
        for interval_return in intervals_returns:
            if interval_return > 0:
                # positive semivariance contains is sum of positive interval returns squared
                positive_semivariance += (interval_return ** 2)
            else:
                # negative semivariance contains is sum of negatie interval returns squared
                negative_semivariance += (interval_return ** 2)

        signed_jump: float = positive_semivariance - negative_semivariance
        realized_signed_jump: float = signed_jump / realized_volatility

        return realized_signed_jump

    def calc_return(self, prices: List[float]) -> float:
        return (prices[-1] - prices[0]) / prices[0]

# 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:
        self.cap_mrkt_cur_usd_index = None
        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 = ['CapMrktCurUSD']

            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 not in CryptoNetworkData._last_update_date:
                CryptoNetworkData._last_update_date[config.Symbol] = datetime(1,1,1).date()
            if data.Time.date() > CryptoNetworkData._last_update_date[config.Symbol]:
                CryptoNetworkData._last_update_date[config.Symbol] = data.Time.date()

        except:
            return None

        return data

# Custom fee model.
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters: OrderFeeParameters) -> OrderFee:
        fee: float = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))

Leave a Reply

Discover more from Quant Buffet

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

Continue reading