Overall Statistics
Total Orders
58664
Average Win
0.02%
Average Loss
-0.02%
Compounding Annual Return
13.370%
Drawdown
34.900%
Expectancy
0.343
Start Equity
10000000
End Equity
65337929.36
Net Profit
553.379%
Sharpe Ratio
0.653
Sortino Ratio
0.668
Probabilistic Sharpe Ratio
13.088%
Loss Rate
32%
Win Rate
68%
Profit-Loss Ratio
0.98
Alpha
0.011
Beta
0.806
Annual Standard Deviation
0.126
Annual Variance
0.016
Information Ratio
-0.113
Tracking Error
0.06
Treynor Ratio
0.102
Total Fees
$720026.49
Estimated Strategy Capacity
$72000.00
Lowest Capacity Asset
ESGL YAOMCIXLXR39
Portfolio Turnover
1.32%
# region imports
from AlgorithmImports import *

import itertools
# endregion
class VirtualYellowGreenLlama(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2010, 1, 2)
        self.set_cash(10_000_000)
        self.settings.automatic_indicator_warm_up = True
        self.settings.minimum_order_margin_portfolio_percentage = 0.0001
        self.set_security_initializer(BrokerageModelSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices)))
        self._beta_period = self.get_parameter('beta_period', 252)
        self._assets_per_industry = self.get_parameter('assets_per_industry', 20)
        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.add_universe(self._select_assets)
        self.schedule.on(self.date_rules.month_start(self._spy.symbol, 2), self.time_rules.midnight, self._rebalance)

    def _select_assets(self, fundamentals):
        # Select the most liquid assets of each industry group.
        fundamentals = sorted([f for f in fundamentals if f.asset_classification.morningstar_industry_group_code], key=lambda f: (f.asset_classification.morningstar_industry_group_code, f.dollar_volume))
        selected = []
        for _, industry_group_fundamentals in itertools.groupby(fundamentals, lambda f: f.asset_classification.morningstar_industry_group_code):
            selected.extend(list(industry_group_fundamentals)[-self._assets_per_industry:])  # We already sorted by dollar volume above.
        # Get the absolute beta of each asset.
        self._beta_by_symbol = self._beta([f.symbol for f in selected]).abs()
        # Get the median beta.
        median_beta = np.median(self._beta_by_symbol.values)
        # Select the assets in each industry have that a beta below the median.
        weights_by_industry = {}
        symbols = []
        for industry_code, industry_assets in itertools.groupby(selected, lambda f: f.asset_classification.morningstar_industry_group_code):
            # Get the beta of each asset in the industry.
            industry_beta_by_symbol = self._beta_by_symbol[[f.symbol for f in industry_assets if f.symbol in self._beta_by_symbol]]
            # Select assets with a beta below the median.
            low_betas = industry_beta_by_symbol[industry_beta_by_symbol < median_beta]
            if low_betas.empty:
                continue
            symbols.extend(list(low_betas.index))
            # Create weights for the assets in this industry (Beta-weighted weights).
            beta_ranks = low_betas.sort_values().rank(method='first', ascending=False) # Higher rank = Lower beta => Larger positive position.
            weights_by_industry[industry_code] = beta_ranks / beta_ranks.sum()

        # Create the portfolio targets. Give equal weight to each industry. Liquidate assets we no longer want.
        self._targets = [PortfolioTarget(symbol, 0) for symbol, holding in self.portfolio.items() if holding.invested and symbol not in symbols]
        for industry_assets in weights_by_industry.values():
            self._targets.extend([PortfolioTarget(symbol, weight/len(weights_by_industry)) for symbol, weight in industry_assets.items()])
        return symbols
        
    def _beta(self, symbols): # Source: https://stackoverflow.com/questions/39501277/efficient-python-pandas-stock-beta-calculation-on-many-dataframes
        returns = self.history([self._spy.symbol] + symbols, self._beta_period, Resolution.DAILY, fill_forward=False).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):
        if self._targets:
            # Rebalance the portfolio.
            self.set_holdings(self._targets)
            self._targets = []