| Overall Statistics |
|
Total Orders 4000 Average Win 0.60% Average Loss -0.54% Compounding Annual Return -7.977% Drawdown 65.600% Expectancy -0.041 Start Equity 100000.00 End Equity 54265.14 Net Profit -45.735% Sharpe Ratio -0.559 Sortino Ratio -0.635 Probabilistic Sharpe Ratio 0.000% Loss Rate 55% Win Rate 45% Profit-Loss Ratio 1.11 Alpha -0.075 Beta -0 Annual Standard Deviation 0.134 Annual Variance 0.018 Information Ratio -0.683 Tracking Error 0.214 Treynor Ratio 806.005 Total Fees $1141.86 Estimated Strategy Capacity $0 Lowest Capacity Asset ANTUSD E3 Portfolio Turnover 7.28% |
# region imports
from AlgorithmImports import *
# 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: Optional[float] = 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 is not None
def get_momentum(self) -> float:
return self._closes[0] / self._closes[self._closes.count - 1] - 1
def get_returns(self) -> np.ndarray:
return pd.Series(list(self._closes)[::-1]).pct_change().dropna().values# https://quantpedia.com/strategies/volatility-scaled-momentum-in-cryptocurrencies
#
# The investment universe for this strategy consists of all cryptocurrencies with available market capitalization data. The selection criteria exclude cryptocurrencies
# with market capitalizations below $1 million or zero trading volumes.
# (Historical daily data for all cryptocurrencies available in CoinMarketCap.)
# Fundamental Recapitulation: The strategy employs various metrics (see Table 1: Characteristic Definitions), notably momentum indicators calculated over one-week (selected
# for reporting this variant), two-week, and three-week periods. Volatility scaling is applied by adjusting each portfolio’s exposure inversely to its realized standard
# deviation of returns from the prior week (see 2.3 Volatility-Scaling). This scaling uses an Eq. (1) formula that maintains the same unconditional volatility as the
# original portfolio. (The strategy does not rely on traditional buy and sell signals but instead uses the momentum and volatility scaling methodology to determine
# portfolio positions.)
# Selected Variation Computational Process: Cryptocurrencies are sorted into quintiles based on their momentum scores. So, cryptocurrencies are sorted into quintiles based
# on the value of the corresponding characteristic (one-week momentum) and form a value-weighted portfolio for each quintile.
# Trading Strategy Execution: A long-short portfolio is created by taking a long position in the highest (fifth) quintile and a short position in the lowest (first) quintile.
# Rebalancing & Weighting: The strategy involves weekly rebalancing to adjust for changes in momentum and volatility. Positions are value-weighted within each quintile.
# Volatility scaling adjusts the capital allocated to each portfolio, stabilizing the risk exposure over time. A leverage cap 1× is applied to manage risk, especially
# during high-volatility periods. (Transaction costs are considered by accounting for a 25 basis point cost, ensuring the strategy remains viable after expenses.)
#
# QC Implementation changes:
# - Observation period is set to 3 week.
# region imports
from AlgorithmImports import *
import data_tools
# endregion
class VolatilityScaledMomentumInCryptocurrencies(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 = 3 * 7
self._target: float = .1
self._quantile: int = 5
self._traded_percentage: float = .1
self._data: Dict[Symbol, data_tools.SymbolData] = {}
# data subscription
for pair, ticker in cryptos.items():
data: Security = self.add_crypto(pair, Resolution.DAILY, Market.BITFINEX, leverage=leverage)
data.set_fee_model(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._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.week_start(self._market),
self.time_rules.after_market_open(self._market),
self._rebalance
)
def on_data(self, slice: Slice) -> None:
# Update crypto data.
for symbol, symbol_data in self._data.items():
dataset_symbol: Symbol = symbol_data._dataset_symbol
if slice.contains_key(symbol) and slice[symbol] and slice.contains_key(dataset_symbol) and slice[dataset_symbol]:
symbol_data.update(slice[symbol].price, slice[dataset_symbol].market_cap)
# Monthly rebalance.
if not self._rebalance_flag:
return
self._rebalance_flag = False
# Calculate momentum over period.
momentum: Dict[Symbol, float] = {
symbol: symbol_data.get_momentum()
for symbol, symbol_data in self._data.items()
if symbol_data.is_ready()
}
if len(momentum) < self._quantile:
self.log('Not enough data for further calculation.')
return
# Sort and divide.
sorted_momentum: List[tuple[Any, Any]] = sorted(momentum.items(), key=lambda x:x[1], reverse=True)
quantile: int = len(sorted_momentum) // self._quantile
long: List[Symbol] = list(map(lambda x:x[0], sorted_momentum))[:quantile]
short: List[Symbol] = list(map(lambda x:x[0], sorted_momentum))[-quantile:]
# Value weighting.
weight: Dict[Symbol, float] = {}
for i, portfolio in enumerate([long, short]):
mc_sum: float = sum([self._data[symbol]._cap_mrkt_cur_usd for symbol in portfolio])
for symbol in portfolio:
weight[symbol] = ((-1)**i) * self._data[symbol]._cap_mrkt_cur_usd / mc_sum
# Calculate portfolio volatility.
if len(weight) != 0:
returns: List[np.ndarray] = [self._data[symbol].get_returns() for symbol in list(weight.keys())]
weights: np.ndarray = np.array(list(weight.values()))
port_vol: float = self._portfolio_volatility(weights, returns)
target_leverage: float = min(self._target / port_vol, 1)
# Trade execution.
targets: List[PortfolioTarget] = [PortfolioTarget(symbol, w * target_leverage) for symbol, w in weight.items() if slice.contains_key(symbol) and slice[symbol]]
self.set_holdings(targets, True)
def _rebalance(self) -> None:
self._rebalance_flag = True
def _portfolio_volatility(self, weights: np.ndarray, daily_returns: np.ndarray) -> float:
# Calculate the volatility of portfolio.
returns_array: np.ndarray = np.column_stack(daily_returns)
covariance_matrix: np.ndarray = np.cov(returns_array, rowvar=False) * 252
result: float = np.sqrt(np.dot(weights.T, np.dot(covariance_matrix, weights)))
return result
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters: OrderFeeParameters) -> OrderFee:
fee: float = parameters.security.price * parameters.order.absolute_quantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))