Overall Statistics
Total Orders
17314
Average Win
0.08%
Average Loss
-0.13%
Compounding Annual Return
22.969%
Drawdown
49.200%
Expectancy
0.095
Start Equity
10000000
End Equity
28149719.20
Net Profit
181.497%
Sharpe Ratio
0.525
Sortino Ratio
0.62
Probabilistic Sharpe Ratio
7.370%
Loss Rate
33%
Win Rate
67%
Profit-Loss Ratio
0.63
Alpha
0.101
Beta
1.58
Annual Standard Deviation
0.35
Annual Variance
0.123
Information Ratio
0.469
Tracking Error
0.281
Treynor Ratio
0.116
Total Fees
$236509.24
Estimated Strategy Capacity
$350000000.00
Lowest Capacity Asset
FDX R735QTJ8XC9X
Portfolio Turnover
12.66%
Drawdown Recovery
1093
# region imports
from AlgorithmImports import *
# endregion


class MomentumUniverseTrackingAlgorithm(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2021, 9, 1)
        self.set_end_date(2026, 9, 1)
        self.set_cash(10_000_000)
        self.settings.seed_initial_prices = True
        # Define some parameters
        self._period = self.get_parameter('lookback_months', 12) * 21
        self._winsor_percentile = self.get_parameter('winsor_percentile', 5.0)
        self._std_multiplier = 2
        # Add an universe of US Equities based on indicators.
        self._selection_data_by_symbol = {}
        self.universe_settings.resolution = Resolution.DAILY
        self._universe = self.add_universe(self._select_assets)
        self.set_warm_up(self._period, Resolution.DAILY)
        # Add a Scheduled Event to rebalance the portfolio each morning.
        self.schedule.on(self.date_rules.every_day('SPY'), self.time_rules.at(8, 0), self._rebalance)

    def _select_assets(self, fundamentals: List[Fundamental]):
        # Update the indicators of all stocks.
        candidates = []
        for f in fundamentals:
            if not f.has_fundamental_data:
                continue
            if f.symbol not in self._selection_data_by_symbol:
                self._selection_data_by_symbol[f.symbol] = SelectionData(self._period)
            if self._selection_data_by_symbol[f.symbol].update(f):
                candidates.append(f.symbol)
        # During warm-up, keep the universe empty so the algorithm runs quickly.
        if self.is_warming_up:
            return []
        # Apply the liquidity filter: Select the stocks with the greatest mean liquidity.
        candidates = self._outliers(candidates, lambda s: self._selection_data_by_symbol[s].mean_dollar_volume.current.value)
        self.plot('Universe', 'Filter 1', len(candidates))
        # Apply the momentum filter: Select the subset of stocks with the greatest momentum.
        candidates = self._outliers(candidates, lambda s: self._selection_data_by_symbol[s].momentum.current.value)
        self.plot('Universe', 'Filter 2', len(candidates))
        return candidates

    def _outliers(self, candidates, factor):
        values = np.array([factor(s) for s in candidates])
        # Winsorize the values so extreme outliers don't skew the standard deviation calculation.
        lower, upper = np.percentile(values, [self._winsor_percentile, 100 - self._winsor_percentile])
        clipped = np.clip(values, lower, upper)
        # Calculate the threshold based on standard deviations.
        threshold = clipped.mean() + self._std_multiplier * clipped.std() 
        # Select the stocks that exceed the threshold.
        return [candidates[i] for i in np.where(values > threshold)[0]]

    def _rebalance(self):
        # During warm-up, do nothing.
        if self.is_warming_up:
            return
        # Form an equal-weighted portfolio.
        weight = 1.0 / len(self._universe.selected)
        self.set_holdings([PortfolioTarget(s, weight) for s in self._universe.selected ], liquidate_existing_holdings=True)


class SelectionData:

    def __init__(self, period):
        self.momentum = MomentumPercent(period)
        self.mean_dollar_volume = SimpleMovingAverage(period)

    def update(self, f):
        return (
            self.momentum.update(f.end_time, f.adjusted_price) &
            self.mean_dollar_volume.update(f.end_time, f.dollar_volume)
        )