| Overall Statistics |
|
Total Orders 225 Average Win 1.12% Average Loss -0.78% Compounding Annual Return 2.000% Drawdown 49.700% Expectancy 0.134 Start Equity 100000.00 End Equity 115676.45 Net Profit 15.676% Sharpe Ratio 0.027 Sortino Ratio 0.03 Probabilistic Sharpe Ratio 0.231% Loss Rate 54% Win Rate 46% Profit-Loss Ratio 1.45 Alpha 0.006 Beta -0.013 Annual Standard Deviation 0.201 Annual Variance 0.04 Information Ratio -0.249 Tracking Error 0.262 Treynor Ratio -0.43 Total Fees $28.25 Estimated Strategy Capacity $0 Lowest Capacity Asset BATUSD E3 Portfolio Turnover 0.18% |
# region imports
from AlgorithmImports import *
from collections import deque
from dateutil.relativedelta import relativedelta
# endregion
class SymbolData():
def __init__(self, dataset_symbol: Symbol, data_period: int) -> None:
self._period: int = data_period
self._dataset_symbol: Symbol = dataset_symbol
self._cap_mrkt_cur_usd: float|None = None
self._closes: RollingWindow = RollingWindow[float](data_period)
def update(self, close: float, market_cap: float) -> None:
self._closes.add(close)
self._cap_mrkt_cur_usd = market_cap
def is_ready(self) -> bool:
return self._closes.is_ready and self._cap_mrkt_cur_usd
def vix_is_ready(self) -> bool:
return self._closes.is_ready
def get_diff(self) -> np.ndarray:
closes: np.ndarray = np.array(list(self._closes))
diff: np.ndarray = np.diff(closes)
return diff
def get_returns(self) -> np.ndarray:
closes: np.ndarray = np.array(list(self._closes))
returns: np.ndarray = closes[:-1] / closes[1:] - 1
return returns
class RegressionData():
def __init__(self, period: int) -> None:
self._daily_data: Deque = deque(maxlen=period)
def update_data(self, vix: float, market: float, ff_data) -> None:
self._daily_data.append((vix, market, ff_data.value, ff_data.size, ff_data.market, ff_data.profitability, ff_data.investment))
def is_ready(self) -> bool:
return len(self._daily_data) == self._daily_data.maxlen
def get_regression_data(self) -> np.ndarray:
data: np.ndarray = np.array(self._daily_data)
# VIX data should be as difference.
vix_diff = np.diff(data[:, 0], prepend=0)
data[:, 0] = vix_diff
# Market returns.
market_ret = pd.Series(data[:, 1]).pct_change().values
data[:, 1] = market_ret
return data[1:]
# Quantpedia data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaFamaFrench(PythonData):
_last_update_date:Dict[Symbol, datetime.date] = {}
@staticmethod
def get_last_update_date() -> Dict[Symbol, datetime.date]:
return QuantpediaFamaFrench._last_update_date
def GetSource(self, config, date, isLiveMode):
return SubscriptionDataSource("data.quantpedia.com/backtesting_data/equity/fama_french/fama_french_5_factor.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
def Reader(self, config, line, date, isLiveMode):
data = QuantpediaFamaFrench()
data.Symbol = config.Symbol
if not line[0].isdigit():
return None
split = line.split(';')
data.Time = datetime.strptime(split[0], "%d.%m.%Y") + relativedelta(months=1)
data['market'] = float(split[1])
data['size'] = float(split[2])
data['value'] = float(split[3])
data['profitability'] = float(split[4])
data['investment'] = float(split[5])
if config.Symbol not in QuantpediaFamaFrench._last_update_date:
QuantpediaFamaFrench._last_update_date[config.Symbol] = datetime(1,1,1).date()
if data.Time.date() > QuantpediaFamaFrench._last_update_date[config.Symbol]:
QuantpediaFamaFrench._last_update_date[config.Symbol] = data.Time.date()
return data
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters: OrderFeeParameters) -> OrderFee:
fee: float = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))
# https://quantpedia.com/strategies/risk-premiums-in-the-cryptocurrency-market/
#
# The investment universe for this strategy consists of the top (the highest market capitalization) 200 cryptocurrencies.
# (Obtain cryptocurrency prices from coinmarketcap.com and equity factors from Kenneth French’s website.)
# Broad Introductory Overview of both Methods: The strategy employs a systematic approach using the primary tool of time-varying exposures (betas) to equity
# market factors. These exposures are calculated through time-series regressions, capturing the sensitivity of each cryptocurrency to factors such as market
# returns (MKTRF) and volatility (VIX). (The strategy also incorporates a rolling window approach for calculating betas, allowing dynamic adjustments to
# changing market conditions.)
# Strategy Process: The portfolios are built using the Fama-MacBeth technique described in Section 4. Betas are obtained from 180-day rolling windows. The
# methodology involves sorting cryptocurrencies into portfolios based on their beta exposures.
# Strategy Execution: reversed (!) high minus low (HML) portfolio exposures to volatility beta (VIX):
# - The low portfolio contains cryptocurrencies with a lower than 30th percentile exposure to the factor.
# - The high portfolio contains cryptocurrencies greater than the 70th percentile.
# - The sell rule involves short-selling cryptocurrencies with high (H) exposure/beta to VIX
# - Conversely, the buy rule involves going long cryptocurrencies with low (L) exposure/beta to VIX
# (This long-short approach exploits the identified risk premia in the cryptocurrency market.)
# Sorting & Weighting: The strategy involves rebalancing the portfolios annually. The sample is rebalanced at the beginning of each year. The portfolios are
# value-weighted, with a 180-day holding period.
# region imports
from AlgorithmImports import *
import data_tools
import statsmodels.api as sm
# endregion
class RiskPremiumsInTheCryptocurrencyMarket(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2018, 1, 1)
self.set_cash(100_000)
cryptos: Dict[str, str] = {
"XLMUSD": "XLM", # Stellar
"XMRUSD": "XMR", # Monero
"XRPUSD": "XRP", # XRP
"ADAUSD": "ADA", # Cardano
"DOTUSD": "DOT", # Polkadot
"UNIUSD": "UNI", # Uniswap
"LINKUSD": "LINK", # Chainlink
"ANTUSD": "ANT", # Aragon
"BATUSD": "BAT", # Basic Attention Token
"BTCUSD": "BTC", # Bitcoin
"BTGUSD": "BTG", # Bitcoin Gold
"DASHUSD": "DASH", # Dash
"DGBUSD": "DGB", # Dogecoin
"ETCUSD": "ETC", # Ethereum Classic
"ETHUSD": "ETH", # Ethereum
"LTCUSD": "LTC", # Litecoin
"MKRUSD": "MKR", # Maker
"NEOUSD": "NEO", # Neo
"PAXUSD": "PAX", # Paxful
"SNTUSD": "SNT", # Status
"TRXUSD": "TRX", # Tron
"XTZUSD": "XTZ", # Tezos
"XVGUSD": "XVG", # Verge
"ZECUSD": "ZEC", # Zcash
"ZRXUSD": "ZRX" # Ox
}
leverage: int = 10
period: int = 180
self._quantile: int = 3
self._traded_percentage: float = .1
self._rebalance_months: List[int] = [1, 7]
self._data: Dict[Symbol, SymbolData] = {}
self._regression_data: data_tools.RegressionData = data_tools.RegressionData(period)
# data subscription
for pair, ticker in cryptos.items():
data: Security = self.add_crypto(pair, Resolution.DAILY, Market.BITFINEX, leverage=leverage)
data.set_fee_model(data_tools.CustomFeeModel())
dataset_symbol = self.add_data(CoinGecko, ticker, Resolution.DAILY).symbol
self._data[data.Symbol] = data_tools.SymbolData(dataset_symbol, period)
self._market: Symbol = self.add_equity('SPY', Resolution.DAILY).symbol
self._vix: Symbol = self.add_data(CBOE, 'VIX', Resolution.DAILY).symbol
self._fama_french: Symbol = self.add_data(data_tools.QuantpediaFamaFrench, 'fama_french_5_factor', Resolution.DAILY).symbol
self._rebalance_flag: bool = False
self.settings.daily_precise_end_time = False
self.settings.minimum_order_margin_portfolio_percentage = 0.
self.schedule.on(
self.date_rules.month_start(self._market),
self.time_rules.after_market_open(self._market),
self.rebalance
)
def on_data(self, slice: Slice) -> None:
if slice.contains_key(self._market) and slice[self._market]:
# Update market, fama french and VIX data.
if self.securities[self._vix].get_last_data() and self.securities[self._fama_french].get_last_data():
ff_data: PythonData = self.securities[self._fama_french].get_last_data()
vix_data: PythonData = self.securities[self._vix].get_last_data()
self._regression_data.update_data(vix_data.value, slice[self._market].close, ff_data)
# Update crypto data.
for symbol, symbol_data in self._data.items():
dataset_symbol: Symbol = symbol_data._dataset_symbol
if self.securities[symbol].get_last_data() and self.securities[dataset_symbol].get_last_data():
symbol_data.update(self.securities[symbol].get_last_data().price, self.securities[dataset_symbol].get_last_data().MarketCap)
# Rebalance every six months.
if not self._rebalance_flag:
return
self._rebalance_flag = False
if not self._regression_data.is_ready():
return
crypto_returns_dict: Dict[Symbol, np.ndarray] = {
symbol : symbol_data.get_returns() for symbol, symbol_data in self._data.items() if symbol_data.is_ready()
}
crypto_returns: List[float] = list(zip(*[[i for i in x] for x in crypto_returns_dict.values()]))
if len(crypto_returns_dict) < self._quantile:
return
# Regression.
y: np.ndarray = np.array(crypto_returns)
x: np.ndarray = self._regression_data.get_regression_data()
model = self.multiple_linear_regression(x, y)
beta_values: np.ndarray = model.params[1]
# Store betas.
beta_by_symbol: Dict[Symbol, float] = {
sym : beta_values[n] for n, sym in enumerate(list(crypto_returns_dict.keys()))
}
long: List[Symbol] = []
short: List[Symbol] = []
# Sort by beta and divide into quantiles.
if len(beta_by_symbol) >= self._quantile:
sorted_beta: List[Symbol] = sorted(beta_by_symbol, key=beta_by_symbol.get)
quantile: int = len(sorted_beta) // self._quantile
long = sorted_beta[:quantile]
short = sorted_beta[-quantile:]
weight: Dict[Symbol, float] = {}
# Trade execution.
for i, portfolio in enumerate([long, short]):
mc_sum: float = sum(list(map(lambda x: self._data[x]._cap_mrkt_cur_usd, portfolio)))
for symbol in portfolio:
weight[symbol] = ((-1)**i) * self._traded_percentage * (self._data[symbol]._cap_mrkt_cur_usd / mc_sum)
portfolio: List[PortfolioTarget] = [
PortfolioTarget(symbol, w) for symbol, w in weight.items() if slice.contains_key(symbol) and slice[symbol]
]
self.set_holdings(portfolio, True)
def rebalance(self) -> None:
if self.time.month in self._rebalance_months:
self._rebalance_flag = True
def multiple_linear_regression(self, x: np.ndarray, y: np.ndarray):
x = sm.add_constant(x, has_constant='add')
result = sm.OLS(endog=y, exog=x).fit()
return result