Overall Statistics |
Total Orders 5108 Average Win 0.08% Average Loss -0.08% Compounding Annual Return -11.335% Drawdown 65.100% Expectancy -0.422 Start Equity 100000 End Equity 38511.73 Net Profit -61.488% Sharpe Ratio -0.841 Sortino Ratio -1.017 Probabilistic Sharpe Ratio 0.000% Loss Rate 72% Win Rate 28% Profit-Loss Ratio 1.09 Alpha -0.042 Beta -0.603 Annual Standard Deviation 0.116 Annual Variance 0.014 Information Ratio -0.745 Tracking Error 0.255 Treynor Ratio 0.162 Total Fees $158.50 Estimated Strategy Capacity $99000000.00 Lowest Capacity Asset UBS VVYBS1ZDBD5X Portfolio Turnover 1.53% |
# https://quantpedia.com/strategies/enhanced-betting-against-beta-strategy-in-equities/ # # The investment universe consists of high market capitalization CRSP stocks listed primarily in NYSE and NASDAQ – stocks which have # been among the top thousand market capitalization stocks in the previous year. Further, leave out stocks where market betas are # estimated to be above two or below 0,3. # Firstly, divide the stocks into quintiles based on the past 48 months value-weighted market returns. Then use only the quintile # with the highest returns and apply the Betting against beta strategy (https://quantpedia.com/Screener/Details/77 in our database # – The beta for each stock is calculated with respect to the benchmark using a 1-year rolling window. Stocks are then ranked in # ascending order on the basis of their estimated beta. The ranked stocks are assigned to one of two portfolios: low beta and high # beta. Securities are weighted by the ranked betas and portfolios are rebalanced every calendar month. Both portfolios are rescaled # to have a beta of one at portfolio formation. The “Betting-Against-Beta” is the zero-cost zero-beta portfolio that is long on the # low-beta portfolio and short on the high-beta portfolio.). # The strategy is rebalanced monthly as the reason for high transaction costs and frequent turnovers connected with the daily strategy. # # QC implementation changes: # - Universe consists of 1000 most liquid stocks traded on NYSE or NASDAQ. import numpy as np from AlgorithmImports import * import pandas as pd from typing import List, Dict class EnhancedBettingAgainstBetaStrategyEquities(QCAlgorithm): def Initialize(self): # self.SetStartDate(2000, 1, 1) self.SetStartDate(2017, 1, 1) self.SetCash(100000) self.SetSecurityInitializer(lambda x: x.SetMarketPrice(self.GetLastKnownPrice(x))) # Daily price data. self.data:Dict[Symbol, SymbolData] = {} self.period:int = 4*12*21 self.beta_period:int = 12*21 self.leverage:int = 10 self.quantile:int = 5 self.beta_thresholds:List[float] = [0.3, 2.] self.exchange_codes:List[str] = ['NYS', 'NAS'] self.market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol # Warmup market data. self.data[self.market] = SymbolData(self.period) history:DataFrame = self.History(self.market, self.period, Resolution.Daily) if not history.empty: closes:Series = history.loc[self.market].close for time, close in closes.items(): self.data[self.market].update(close) self.weight:Dict[Symbol, float] = {} self.fundamental_count:int = 1000 self.fundamental_sorting_key = lambda x: x.DollarVolume self.selection_flag:bool = True self.Settings.MinimumOrderMarginPortfolioPercentage = 0. self.UniverseSettings.Resolution = Resolution.Daily self.AddUniverse(self.FundamentalSelectionFunction) self.Schedule.On(self.DateRules.MonthEnd(self.market), self.TimeRules.AfterMarketOpen(self.market), self.Selection) self.settings.daily_precise_end_time = False def OnSecuritiesChanged(self, changes: SecurityChanges) -> None: for security in changes.AddedSecurities: security.SetFeeModel(CustomFeeModel()) security.SetLeverage(self.leverage) def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]: # Update the rolling window every day. for stock in fundamental: symbol:Symbol = stock.Symbol # Store monthly price. if symbol in self.data: self.data[symbol].update(stock.AdjustedPrice) if not self.selection_flag: return Universe.Unchanged selected:List[Fundamental] = [x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and x.MarketCap != 0 and x.SecurityReference.ExchangeId in self.exchange_codes] if len(selected) > self.fundamental_count: selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]] # Warmup price rolling windows. for stock in selected: symbol:Symbol = stock.Symbol if symbol in self.data: continue self.data[symbol] = SymbolData(self.period) history:DataFrame = self.History(symbol, self.period, Resolution.Daily) if history.empty: self.Log(f"Not enough data for {symbol} yet.") continue closes:Series = history.loc[symbol].close for time, close in closes.items(): self.data[symbol].update(close) stock_data:Dict[Symbol, StockData] = {} market_closes:np.ndarray = np.array([x for x in self.data[self.market]._price][:self.beta_period]) market_returns:np.ndarray = (market_closes[1:] - market_closes[:-1]) / market_closes[:-1] if len(market_returns) != 0: for stock in selected: symbol:Symbol = stock.Symbol if not self.data[symbol].is_ready(): continue # Data is ready. stock_closes:np.ndarray = np.array([x for x in self.data[symbol]._price][:self.beta_period]) stock_returns:np.ndarray = (stock_closes[1:] - stock_closes[:-1]) / stock_closes[:-1] # Manual beta calc. cov:np.ndarray = np.cov(market_returns, stock_returns)[0][1] market_variance:float = np.std(market_returns) ** 2 beta:float = cov / market_variance if beta >= self.beta_thresholds[0] and beta <= self.beta_thresholds[1]: # Return calc. ret = self.data[symbol].performance() stock_data[symbol] = StockData(beta, ret, stock.MarketCap) if len(stock_data) >= self.quantile: # Value weighted return sorting. total_market_cap:float = sum([x[1]._market_cap for x in stock_data.items()]) sorted_by_return:[List[Tuple[Symbol, StockData]]] = sorted(stock_data.items(), key = lambda x: x[1]._performance * (x[1]._market_cap / total_market_cap), reverse = True) quintile:int = int(len(sorted_by_return) / self.quantile) top_by_ret:List[StockData] = [x for x in sorted_by_return[:quintile]] sorted_by_beta:[List[Tuple[Symbol, StockData]]] = sorted(top_by_ret, key = lambda x: x[1]._beta, reverse = True) beta_median:float = np.median([x[1]._beta for x in sorted_by_beta]) low_beta_stocks:List[Tuple[StockData, float]] = [(x, abs(beta_median - x[1]._beta)) for x in sorted_by_beta if x[1]._beta < beta_median] high_beta_stocks:List[Tuple[StockData, float]] = [(x, abs(beta_median - x[1]._beta)) for x in sorted_by_beta if x[1]._beta > beta_median] # Beta diff weighting. # for i, portfolio in enumerate([low_beta_stocks, high_beta_stocks]): for i, portfolio in enumerate([high_beta_stocks, low_beta_stocks]): total_diff:float = sum(list(map(lambda x: x[1], portfolio))) for symbol_data, diff in portfolio: self.weight[symbol_data[0]] = ((-1)**i) * (diff / total_diff) return [x[0] for x in self.weight.items()] def OnData(self, data: Slice) -> None: if not self.selection_flag: return self.selection_flag = False # portfolio:List[PortfolioTarget] = [PortfolioTarget(symbol, w) for symbol, w in self.weight.items() if symbol in data and data[symbol]] portfolio:List[PortfolioTarget] = [PortfolioTarget(symbol, w) for symbol, w in self.weight.items() if w < 0 and symbol in data and data[symbol]] # portfolio:List[PortfolioTarget] = [PortfolioTarget(symbol, w) for symbol, w in self.weight.items() if w > 0 and symbol in data and data[symbol]] self.SetHoldings(portfolio, True) self.weight.clear() def Selection(self) -> None: self.selection_flag = True class StockData(): def __init__(self, beta:float, performance:float, market_cap:float): self._beta:float = beta self._performance:float = performance self._market_cap:float = market_cap class SymbolData(): def __init__(self, period:int): self._price:RollingWindow = RollingWindow[float](period) def update(self, value:float) -> None: self._price.Add(value) def is_ready(self) -> bool: return self._price.IsReady def performance(self) -> float: return (self._price[0] / self._price[self._price.Count - 1] - 1) # Custom fee model. class CustomFeeModel(FeeModel): def GetOrderFee(self, parameters): fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 return OrderFee(CashAmount(fee, "USD"))