Created with Highcharts 12.1.2Equity201020112012201320142015201620172018201920202021202220232024202520260100k200k300k-50000.10.2-20201M2M0100k200k02550
Overall Statistics
Total Orders
60340
Average Win
0.11%
Average Loss
-0.11%
Compounding Annual Return
2.196%
Drawdown
49.000%
Expectancy
0.014
Start Equity
100000
End Equity
139062.22
Net Profit
39.062%
Sharpe Ratio
0.081
Sortino Ratio
0.096
Probabilistic Sharpe Ratio
0.005%
Loss Rate
51%
Win Rate
49%
Profit-Loss Ratio
1.06
Alpha
-0.045
Beta
0.7
Annual Standard Deviation
0.185
Annual Variance
0.034
Information Ratio
-0.433
Tracking Error
0.162
Treynor Ratio
0.022
Total Fees
$3414.57
Estimated Strategy Capacity
$16000.00
Lowest Capacity Asset
NARI XEPKR8BAHSH1
Portfolio Turnover
10.17%
# https://quantpedia.com/strategies/intraday-vix-betas-predict-stocks-returns/
#
# The investment universe consists of all stocks with a bid-ask spread of less than 10% listen on the NYSE. Stocks with a spread larger than 10% are to be excluded. The variable of interest to us is the VIX intra-day beta over the last month. 
# The VIX intra-day beta of a stock can be found by making use of intra-day stock returns, calculated easily using the open and close price of the stocks, and the change in the VIX index across the trading day, calculated using open and close 
# values of the VIX obtained through the CBOE. We split our investment universe into deciles, ranked based on intraday beta to VIX. We will then go short decile of stocks with the highest intraday beta to VIX over the previous month and go long
# decile of stocks with the lowest intraday beta to VIX over the previous month. Stocks are weighted equally, and the portfolio is rebalanced on a monthly basis.
#
# QC implementation changes:
#   - The investment universe consists of 1000 most liquid stocks traded on NASDAQ selected quarterly.

# region imports
from AlgorithmImports import *
from scipy import stats
from dateutil.relativedelta import relativedelta
# endregion

class IntradayVIXBetasPredictStocksReturns(QCAlgorithm):

    def Initialize(self) -> None:
        self.SetStartDate(2010, 1, 1)
        self.SetCash(100_000)

        self._tickers_to_ignore: List[str] = ['BLVD']

        self.vix: Symbol = self.AddData(CBOE, 'VIX', Resolution.Daily).Symbol
        self.market_symbol: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol

        self.active_universe: List[Symbol] = []

        self.min_return_period: int = 15
        self.quantile: int = 10
        leverage: int = 5
        self.min_share_price: float = 5.

        self.SetWarmup(self.min_return_period, Resolution.Daily)
        
        self.fundamental_count: int = 1000
        self.fundamental_sorting_key = lambda x: x.dollar_volume
        
        self.selection_flag: bool = False
        self.rebalance_flag: bool = False
        self.UniverseSettings.Resolution = Resolution.Daily
        self.UniverseSettings.Leverage = leverage
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.settings.daily_precise_end_time = False
        self.settings.minimum_order_margin_portfolio_percentage = 0.
        self.Schedule.On(self.DateRules.MonthStart(self.market_symbol), self.TimeRules.BeforeMarketClose(self.market_symbol), self.Selection)

    def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
        for security in changes.AddedSecurities:
            security.SetFeeModel(CustomFeeModel())

    def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
        if not self.selection_flag:
            return Universe.Unchanged
        self.selection_flag = False
        
        selected:List[CoarseFundamental] = [
            x for x in fundamental 
            if x.HasFundamentalData 
            and x.Market == 'usa' 
            and x.Price >= self.min_share_price 
            and x.SecurityReference.ExchangeId == "NAS"
            and x.symbol.value not in self._tickers_to_ignore
            and x.price >= 5
            ]

        if len(selected) > self.fundamental_count:
            selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
        
        self.active_universe = list(map(lambda stock: stock.Symbol, selected))
        
        return self.active_universe

    def OnData(self, data: Slice) -> None:
        beta: Dict[Symbol, float] = {}

        if self.IsWarmingUp:
            return

        if not self.rebalance_flag:
            return
        self.rebalance_flag = False

        vix_history = self.history(self.vix, start=self.time - relativedelta(months=1), end=self.time.date()).unstack(level=0)
        if vix_history.empty:
            return
        vix_returns = vix_history.close.pct_change().dropna()

        for symbol in self.active_universe:
            symbol_history = self.history(symbol, start=self.time - relativedelta(months=1), end=self.time.date()).unstack(level=0)
            if symbol_history.empty:
                continue
            symbol_returns = symbol_history.close.pct_change().dropna()
            if len(symbol_returns) >= self.min_return_period:
                vix_intersection = vix_returns.loc[vix_returns.index.intersection(symbol_returns.index)]
                slope, _, _, _, _ = stats.linregress(vix_intersection.values.flatten()[-len(symbol_returns):], symbol_returns.values.flatten())
                beta[symbol] = slope

        long: List[Symbol] = []
        short: List[Symbol] = []

        if len(beta) >= self.quantile:
            # sort by intraday beta
            sorted_by_beta: List[Tuple[Symbol, float]] = sorted(beta.items(), key=lambda x: x[1], reverse=True)
            quantile: int = int(len(beta) / self.quantile)
            long = [x[0] for x in sorted_by_beta[-quantile:]]
            short = [x[0] for x in sorted_by_beta[:quantile]]
        
        # order execution
        targets: List[PortfolioTarget] = []
        for i, portfolio in enumerate([long, short]):
            for symbol in portfolio:
                if symbol in data and data[symbol]:
                    targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio)))
        
        self.SetHoldings(targets, True)

    def Selection(self) -> None:
        # quarterly universe selection
        if self.Time.month % 3 == 0:
            self.selection_flag = True
        
        # monthly rebalance
        self.rebalance_flag = True

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