| Overall Statistics |
|
Total Orders 3189 Average Win 2.18% Average Loss -2.06% Compounding Annual Return -18.058% Drawdown 92.900% Expectancy 0.017 Start Equity 100000.00 End Equity 12715.00 Net Profit -87.285% Sharpe Ratio -0.083 Sortino Ratio -0.083 Probabilistic Sharpe Ratio 0.003% Loss Rate 51% Win Rate 49% Profit-Loss Ratio 1.06 Alpha -0.028 Beta -0.173 Annual Standard Deviation 0.487 Annual Variance 0.237 Information Ratio -0.215 Tracking Error 0.517 Treynor Ratio 0.233 Total Fees $556.62 Estimated Strategy Capacity $0 Lowest Capacity Asset XVGUSD E3 Portfolio Turnover 10.68% |
#region imports
from AlgorithmImports import *
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) -> None:
self._closes.Add(close)
def update_cap(self, cap_mrkt_cur_usd: float) -> None:
self._cap_mrkt_cur_usd = cap_mrkt_cur_usd
def is_ready(self) -> bool:
return self._closes.IsReady 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
# Source: https://coinmetrics.io/newdata/
class DailyCustomData(PythonData):
def GetSource(self, config, date, isLiveMode):
return SubscriptionDataSource(f'data.quantpedia.com/backtesting_data/crypto/{config.Symbol.Value}_dataset_data.csv', SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
_last_update_date:Dict[Symbol, datetime.date] = {}
word_index:Dict[Symbol, int] = {}
@staticmethod
def get_last_update_date() -> Dict[Symbol, datetime.date]:
return DailyCustomData._last_update_date
def Reader(self, config, line, date, isLiveMode):
data = DailyCustomData()
data.Symbol = config.Symbol
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(',')
# find index of searched word
# Parse the CSV file's columns into the custom data class
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 DailyCustomData._last_update_date:
DailyCustomData._last_update_date[config.Symbol] = datetime(1,1,1).date()
if data.Time.date() > DailyCustomData._last_update_date[config.Symbol]:
DailyCustomData._last_update_date[config.Symbol] = data.Time.date()
return data
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))# https://quantpedia.com/strategies/intermediate-uncertainty-crypto-strategy/
#
# The investment universe for this strategy includes a basket of major cryptocurrencies, such as Bitcoin and Ethereum, and other high-liquidity digital assets.
# (Collect daily cryptocurrency data from coinmarketcap.com.)
# Rationale: This strategy’s primary tool is a nonlinear regression model that captures the relationship between cryptocurrency returns and the market
# uncertainty index. The methodology involves fitting a polynomial regression model (e.g., quadratic or cubic) to historical data to understand how
# cryptocurrency returns vary with changes in market uncertainty.
# Calculation: Pre-ranking VIX betas and prices are value-weighted averages from the week each decile portfolio was formed, and excess returns are
# value-weighted averages of the following week’s excess returns for the decile portfolios.
# 0. Construct ten portfolios sorted by VIX-beta estimates, using Eq. (1) with the weekly individual cryptocurrency returns over the one-month T-bill rate as
# the dependent variable (outcome) and the volatility innovation that is non-tradable factor ∆VIX as the independent variable (or predictor).
# 1. Construct, trade, and rebalance the long-short portfolio (5+6)−(1+10): taking long positions in portfolios 5 and 6 and short positions in portfolios 1 and 10.
# Rebalancing occurs weekly to ensure the portfolio remains aligned with the strategy’s objectives. Position sizes are calculated using a value-weighted approach.
#
# QC Implementation changes:
# - Investment universe constists of 27 cryptocurrencies with network data available.
# region imports
from AlgorithmImports import *
import data_tools
import statsmodels.api as sm
from typing import List, Dict
# endregion
class IntermediateUncertaintyCryptoStrategy(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2015, 1, 1)
self.set_cash(100_000)
self.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
# "FUNUSD": "FUN", # FUN Token
"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
self.period: int = 52
self.quantile: int = 10
self.long_indices: List = [4, 5]
self.short_indices: List = [0, 9]
self.data: Dict[Symbol, SymbolData] = {}
# data subscription
for pair, ticker in self.cryptos.items():
data = self.add_crypto(pair, Resolution.DAILY, Market.BITFINEX, leverage=leverage)
data.set_fee_model(data_tools.CustomFeeModel())
dataset_symbol = self.add_data(CoinGecko, ticker).Symbol
self.data[data.Symbol] = data_tools.SymbolData(dataset_symbol, self.period)
self.vix: Symbol = self.add_data(CBOE, 'VIX', Resolution.Daily).Symbol
self.data[self.vix] = data_tools.SymbolData(None, self.period)
self.rebalance_flag: bool = False
self.settings.daily_precise_end_time = False
self.settings.minimum_order_margin_portfolio_percentage = 0.
self.current_week: int = -1
def OnData(self, slice: Slice) -> None:
# weekly rebalance
if self.time.isocalendar().week == self.current_week:
return
self.current_week = self.time.isocalendar().week
# update weekly price
for symbol, symbol_data in self.data.items():
dataset_symbol: Symbol = symbol_data._dataset_symbol
if symbol == self.vix:
self.data[symbol].update(self.securities[symbol].get_last_data().close)
continue
if slice.contains_key(symbol) and slice[symbol]:
self.data[symbol].update(slice[symbol].close)
if self.securities[dataset_symbol].get_last_data():
self.data[symbol].update_cap(self.securities[dataset_symbol].get_last_data().MarketCap)
if not self.data[self.vix].vix_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.data[self.vix].get_diff()
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, reverse=True)
quantiles: np.ndarray = np.array_split(sorted_beta, self.quantile)
for n in range(len(quantiles)):
if n in self.long_indices:
long.extend(quantiles[n])
elif n in self.short_indices:
short.extend(quantiles[n])
weight: Dict[Symbol, float] = {}
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.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 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