Overall Statistics |
Total Orders 3666 Average Win 0.33% Average Loss -0.25% Compounding Annual Return 6.991% Drawdown 45.900% Expectancy 0.159 Start Equity 10000000 End Equity 27472634.93 Net Profit 174.726% Sharpe Ratio 0.284 Sortino Ratio 0.294 Probabilistic Sharpe Ratio 0.230% Loss Rate 50% Win Rate 50% Profit-Loss Ratio 1.31 Alpha -0.017 Beta 0.674 Annual Standard Deviation 0.151 Annual Variance 0.023 Information Ratio -0.368 Tracking Error 0.126 Treynor Ratio 0.064 Total Fees $368251.58 Estimated Strategy Capacity $14000000.00 Lowest Capacity Asset ALIT X314VB47TMXX Portfolio Turnover 2.25% |
# region imports from AlgorithmImports import * # endregion class VirtualYellowGreenLlama(QCAlgorithm): def initialize(self): self.set_start_date(2010, 1, 1) self.set_cash(10_000_000) self.settings.automatic_indicator_warm_up = True self._beta_period = self.get_parameter('beta_period', 252) self._spy = self.add_equity('SPY', Resolution.DAILY) self.universe_settings.resolution = Resolution.DAILY self.universe_settings.schedule.on(self.date_rules.month_start(self._spy.symbol)) self._universe = self.add_universe(self._select_assets) self.schedule.on(self.date_rules.month_start(self._spy.symbol), self.time_rules.at(0, 1), self._rebalance) def _select_assets(self, fundamentals): fundamentals = sorted(fundamentals, key=lambda f: f.dollar_volume)[-2_500:] return [f.symbol for f in fundamentals if f.asset_classification.morningstar_industry_code == MorningstarIndustryCode.SOFTWARE_APPLICATION] def _beta(self, securities): # Source: https://stackoverflow.com/questions/39501277/efficient-python-pandas-stock-beta-calculation-on-many-dataframes symbols = [s.symbol for s in securities] returns = self.history([self._spy.symbol] + symbols, self._beta_period, Resolution.DAILY).close.unstack(0).dropna(axis=1).pct_change().dropna() symbols = [s for s in symbols if s in returns.columns] df = returns[[self._spy.symbol] + symbols] # first column is the market X = df.values[:, [0]] # prepend a column of ones for the intercept X = np.concatenate([np.ones_like(X), X], axis=1) # matrix algebra b = np.linalg.pinv(X.T.dot(X)).dot(X.T).dot(df.values[:, 1:]) return pd.Series(b[1], df.columns[1:], name='Beta') def _rebalance(self): securities = [self.securities[symbol] for symbol in self._universe.selected] self.plot('Securities', 'Original Size', len(securities)) securities = [s for s in securities if s.price] self.plot('Securities', 'Tradable Size', len(securities)) if not securities: return # Get the beta of each asset. beta_by_symbol = self._beta(securities) # Calculate the weights of the portfolio. # Step 1: Weigh the assets by their beta ranks, relative to the universe median. # - Lower-beta security have larger weight in the low-beta portfolio and higher-beta securities have larger weights in the high beta portfolio. beta_rank_by_symbol = beta_by_symbol.sort_values().rank(method='first', ascending=False) # Higher rank number = Lower Beta (so more positive portfolio weight). rank_delta_from_mean = beta_rank_by_symbol - beta_rank_by_symbol.mean() normalizing_constant = rank_delta_from_mean.abs().sum() / 2 # Make the weights in the long and short sides of the portfolio sum to 1/-1. weight_by_symbol = rank_delta_from_mean / normalizing_constant # Step 2: Scale the weights of each side of the portfolio to a -1 beta, making the portfolio market-netural. try: self._scale_weights_to_negative_1_beta(beta_by_symbol, weight_by_symbol, lambda x: x > 0) self._scale_weights_to_negative_1_beta(beta_by_symbol, weight_by_symbol, lambda x: x < 0) except: # If the portfolio Beta for either side of the portfolio is >= 0, don't rebalance. return long_portfolio_weights = weight_by_symbol[weight_by_symbol > 0] long_portfolio_beta = beta_by_symbol[long_portfolio_weights.index].dot(long_portfolio_weights) short_portfolio_weights = weight_by_symbol[weight_by_symbol < 0] short_portfolio_beta = beta_by_symbol[short_portfolio_weights.index].dot(short_portfolio_weights) self.plot('Portfolio Beta', 'Long', long_portfolio_beta) self.plot('Portfolio Beta', 'Short', short_portfolio_beta) # Step 3: Scale all the weights to keep gross leverage at 1. weight_sum_by_bias = { 'long' : weight_by_symbol[weight_by_symbol > 0].sum(), 'short': weight_by_symbol[weight_by_symbol < 0].sum() } self.plot('Weight Sum', 'Long (Before)', weight_sum_by_bias['long']) self.plot('Weight Sum', 'Short (Before)', weight_sum_by_bias['short']) scaling_factor = 1 / sum([abs(weight) for weight in weight_sum_by_bias.values()]) weight_by_symbol *= scaling_factor self.plot('Weight Sum', 'Long (After)', weight_by_symbol[weight_by_symbol > 0].sum()) self.plot('Weight Sum', 'Short (After)', weight_by_symbol[weight_by_symbol < 0].sum()) # Rebalance the portfolio. self.set_holdings([PortfolioTarget(symbol, weight) for symbol, weight in weight_by_symbol.items()], True) def _scale_weights_to_negative_1_beta(self, beta_by_symbol, weight_by_symbol, selection_function): portfolio_weights = weight_by_symbol[selection_function(weight_by_symbol)] portfolio_beta = beta_by_symbol[portfolio_weights.index].dot(portfolio_weights) if portfolio_beta >= 0: # Beta should be negative since we're betting against beta self.log(f'{self.time} Portfolio beta >= 0: {portfolio_beta}') raise Exception(f"Portfolio beta >= 0: {portfolio_beta}") weight_by_symbol[selection_function(weight_by_symbol)] /= -portfolio_beta # We don't want to flip the sign of the weights, so divide by the negative value.