Overall Statistics
Total Orders
540
Average Win
2.02%
Average Loss
-2.24%
Compounding Annual Return
-18.301%
Drawdown
81.600%
Expectancy
-0.075
Start Equity
1000000
End Equity
445280.02
Net Profit
-55.472%
Sharpe Ratio
-0.155
Sortino Ratio
-0.196
Probabilistic Sharpe Ratio
0.388%
Loss Rate
51%
Win Rate
49%
Profit-Loss Ratio
0.90
Alpha
-0.059
Beta
-0.061
Annual Standard Deviation
0.427
Annual Variance
0.183
Information Ratio
-0.4
Tracking Error
0.465
Treynor Ratio
1.095
Total Fees
$13309.16
Estimated Strategy Capacity
$25000.00
Lowest Capacity Asset
MYOS VS3HQVEKIZJ9
Portfolio Turnover
2.60%
#region imports
from AlgorithmImports import *
#endregion
# https://quantpedia.com/Screener/Details/77


class BetaFactorInStocks(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2018, 1, 1)   
        self.set_end_date(2022, 1, 1)         
        self.set_cash(1000000)            

        self.set_security_initializer(BrokerageModelSecurityInitializer(
            self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices)))
        self.universe_settings.resolution = Resolution.DAILY
        self.add_universe(self._coarse_selection_function)

        # Add Wilshire 5000 Total Market Index data from Dropbox 
        self._price5000 = self.add_data(Fred, Fred.Wilshire.PRICE_5000, Resolution.DAILY).symbol

        # Setup a RollingWindow to hold market return
        self._market_return = RollingWindow[float](252)
        # Use a ROC indicator to convert market price index into return, and save it to the RollingWindow
        self._roc = self.roc(self._price5000, 1)
        self._roc.updated += lambda _, updated: self._market_return.add(updated.value)
        # Warm up
        hist = self.history(self._price5000, 253, Resolution.DAILY)
        for t, value in hist.loc[self._price5000]['value'].items():
            self._roc.update(t, value)

        self._data = {}
        self._monthly_rebalance = False
        self._long = None
        self._short = None
        
        spy = Symbol.create("SPY", SecurityType.EQUITY, Market.USA)
        self.schedule.on(self.date_rules.month_start(spy), self.time_rules.after_market_open(spy), self._rebalance)
        self.set_warm_up(timedelta(365))

    def _coarse_selection_function(self, coarse):
        for c in coarse:
            if c.symbol not in self._data:
                self._data[c.symbol] = SymbolData(c.symbol)
            self._data[c.symbol].update(c.end_time, c.adjusted_price)

        if self._monthly_rebalance:
            filtered_data = {symbol: data for symbol, data in self._data.items() if data.last_price > 5 and data.is_ready()}

            if len(filtered_data) > 10:
                # sort the dictionary in ascending order by beta value
                sorted_beta = sorted(filtered_data, key=lambda x: filtered_data[x].beta(self._market_return))
                self._long = sorted_beta[:5]
                self._short = sorted_beta[-5:]
                return self._long + self._short
            else: 
                self._monthly_rebalance = False
        return []

    def _rebalance(self):
        self._monthly_rebalance = True

    def on_data(self, data):
        if not self._monthly_rebalance or self.is_warming_up: 
            return 
        
        # Liquidate symbols not in the universe anymore
        for symbol, security_holding in self.portfolio.items():
            if security_holding.invested and symbol not in self._long + self._short:
                self.liquidate(symbol)

        if self._long is None or self._short is None: 
            return
        
        longs = [symbol for symbol in self._long if self.securities[symbol].price]
        long_scale_factor = 0.4/sum(range(1,len(longs)+1))
        for rank, symbol in enumerate(longs):    
            self.set_holdings(symbol, (len(longs)-rank+1)*long_scale_factor)
        
        shorts = [symbol for symbol in self._short if self.securities[symbol].price]
        short_scale_factor = 0.4/sum(range(1,len(shorts)+1))
        for rank, symbol in enumerate(shorts):
            self.set_holdings(symbol, -(rank+1)*short_scale_factor)
            
        self._monthly_rebalance = False
        self._long = None
        self._short = None


class SymbolData:

    def __init__(self, symbol):
        self.symbol = symbol
        self.last_price = 0
        self._returns = RollingWindow[float](252)
        self._roc = RateOfChange(1)
        self._roc.updated += lambda _, updated: self._returns.add(updated.value)
        
    def update(self, time, price):
        if price != 0:
            self.last_price = price
            self._roc.update(time, price)
    
    def is_ready(self):
        return self._roc.is_ready and self._returns.is_ready
    
    def beta(self, market_ret):
        asset_return = np.array(list(self._returns), dtype=np.float32)
        market_return = np.array(list(market_ret), dtype=np.float32)
        return np.cov(asset_return, market_return)[0][1] / np.var(market_return)