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