Overall Statistics
Total Orders
2541
Average Win
0.12%
Average Loss
-0.12%
Compounding Annual Return
1.244%
Drawdown
18.400%
Expectancy
-0.013
Start Equity
1000000
End Equity
1063812.61
Net Profit
6.381%
Sharpe Ratio
-0.426
Sortino Ratio
-0.503
Probabilistic Sharpe Ratio
1.119%
Loss Rate
50%
Win Rate
50%
Profit-Loss Ratio
0.97
Alpha
-0.025
Beta
-0.045
Annual Standard Deviation
0.066
Annual Variance
0.004
Information Ratio
-0.604
Tracking Error
0.162
Treynor Ratio
0.62
Total Fees
$2891.04
Estimated Strategy Capacity
$2700000000.00
Lowest Capacity Asset
SAP S5MRCWJFK7Z9
Portfolio Turnover
1.53%
Drawdown Recovery
371
# region imports
from AlgorithmImports import *
from universe import LargeLiquidUSEquities
# endregion


class ValueAndMomentumEverywhere(QCAlgorithm):
    # Value Indicator: Book-to-market ratio
    # Momentum Indicator: 12month returns, excluding the most recent month
    
    def initialize(self):
        self.set_start_date(self.end_date - timedelta(5*365))
        self.set_cash(1000000)
        self.settings.seed_initial_prices = True
        # Define some parameters.
        self._momentum_lookback = 12*22  # 12 months
        self._momentum_delay = 22  # 1 month
        # Add a universe of US Equities.
        self._date_rule = self.date_rules.month_start('SPY')
        self.universe_settings.schedule.on(self._date_rule)
        self.universe_settings.resolution = Resolution.DAILY
        self._universe = []
        self.add_universe_selection(LargeLiquidUSEquities())
        # Add a warm-up period so the algorithm trades right away
        # instead of waiting for the next month.
        self.set_warm_up(timedelta(45))
    
    def on_warmup_finished(self):
        # Add a Scheduled Event to rebalance the portfolio monthly.        
        time_rule = self.time_rules.at(8, 0)
        self.schedule.on(self._date_rule, time_rule, self._rebalance)  
        # Rebalance today too.
        if self.live_mode:
            self._rebalance()
        else:
            self.schedule.on(self.date_rules.today, time_rule, self._rebalance)

    def on_securities_changed(self, changes):
        # As assets enter the universe, add an indicator to track their momentum.
        for security in changes.added_securities:
            # Setup consolidator and indicators
            security.consolidator = self.resolve_consolidator(security, Resolution.DAILY)
            mom = RateOfChange(self._momentum_lookback - self._momentum_delay)
            security.momentum_indicator = IndicatorExtensions.of(Delay(self._momentum_delay), mom)
            self.register_indicator(security, mom, security.consolidator)
            # Warm up the indicator.
            for bar in self.history[TradeBar](security, self._momentum_lookback + self._momentum_delay):
                mom.update(bar.end_time, bar.close)
            self._universe.append(security)
        # As assets leave the universe, liquidate them and stop updating their indicators.
        for security in changes.removed_securities:
            self.liquidate(security.symbol)
            self.subscription_manager.remove_consolidator(security, security.consolidator)
            self._universe.remove(security)
    
    def _rebalance(self): 
        # Gather value and momentum factor values for the assets in the universe.
        df = pd.DataFrame()
        for security in self._universe: 
            if (not security.momentum_indicator.is_ready or 
                security.fundamentals.valuation_ratios.pb_ratio in [None, 0]):
                continue
            df.loc[security, 'Value'] = 1 / security.fundamentals.valuation_ratios.pb_ratio
            df.loc[security, 'Momentum'] = security.momentum_indicator.current.value
        # Ensure some constituents are ready.
        if df.empty: 
            self.debug(f'{self.time} Consitituents not warm yet')
            self.liquidate()
            return
        # Rank the securities on their factor values; Make dollar-neutral.
        weight_by_security = df.rank() - df.rank().mean()
        weight_by_security = weight_by_security.mean(axis=1)
        # Scale weights down so the portfolio stays within leverage constraints.
        weight_by_security /= abs(weight_by_security).sum()
        # Place trades to rebalance the portfolio.
        for security, weight in weight_by_security.items():
            self.set_holdings(security, float(weight))
            
# region imports
from AlgorithmImports import *
# endregion


class LargeLiquidUSEquities(FundamentalUniverseSelectionModel):

    def __init__(self, min_price=1):
        self.min_price = min_price

    def select(self, algorithm, fundamentals):
        # Select securities with fundamental data (exclude penny stocks).
        selected = [f for f in fundamentals if f.has_fundamental_data and f.price >= self.min_price]
        sorted_by_dollar_volume = sorted(selected, key=lambda x: x.dollar_volume, reverse=True)
        filtered = [c for c in sorted_by_dollar_volume[:500]]
        # Restrict to common stock; Remove ADRs, REITs, and Financials.
        selected = [
            f for f in filtered 
            if (f.security_reference.security_type == 'ST00000001' and
                not f.security_reference.is_depositary_receipt and
                not f.company_reference.is_reit and
                f.asset_classification.morningstar_sector_code != MorningstarSectorCode.FINANCIAL_SERVICES)
        ]
        # Select the largest stocks.
        top_market_cap = sorted(selected, key=lambda x: x.market_cap, reverse=True)
        return [f.symbol for f in top_market_cap[:50]]