Overall Statistics
Total Orders
21439
Average Win
0.04%
Average Loss
-0.03%
Compounding Annual Return
11.027%
Drawdown
25.900%
Expectancy
0.166
Start Equity
10000000
End Equity
16880516.06
Net Profit
68.805%
Sharpe Ratio
0.269
Sortino Ratio
0.325
Probabilistic Sharpe Ratio
2.150%
Loss Rate
50%
Win Rate
50%
Profit-Loss Ratio
1.31
Alpha
-0.006
Beta
1.035
Annual Standard Deviation
0.178
Annual Variance
0.032
Information Ratio
-0.042
Tracking Error
0.1
Treynor Ratio
0.046
Total Fees
$95613.80
Estimated Strategy Capacity
$2100000.00
Lowest Capacity Asset
WBST R735QTJ8XC9X
Portfolio Turnover
3.46%
Drawdown Recovery
828
# region imports
from AlgorithmImports import *
# endregion


class NoBuzzMomentumAlgorithm(QCAlgorithm):

    def initialize(self) -> None:
        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._slow_period = 252
        self._fast_period = 21
        self._fast_dollar_vol_filter_size = 1_500
        self._max_volume_surge_ratio = 1.5
        self._universe_size = 50
        # Add an indicator universe of US Equities.
        self._symbol_data_by_symbol = {}
        self.universe_settings.resolution = Resolution.DAILY
        self._universe = self.add_universe(self._select_assets)
        # Add a warm up period to prime the indicators of all stocks in the universe.
        self.set_warm_up(timedelta(400))
        # Add a Scheduled Event to rebalance the portfolio each month.
        self.schedule.on(self.date_rules.month_start('SPY'), self.time_rules.at(8, 0), self._rebalance)

    def _select_assets(self, fundamentals: list[Fundamental]) -> list[Symbol]:
        # Update the indicators of all stocks.
        candidates = []
        for f in fundamentals:
            if f.symbol not in self._symbol_data_by_symbol:
                self._symbol_data_by_symbol[f.symbol] = SymbolData(f.symbol, self._slow_period, self._fast_period)
            symbol_data = self._symbol_data_by_symbol[f.symbol]
            if symbol_data.update(f.end_time, f.adjusted_price, f.dollar_volume):
                candidates.append(f)
        # During warm-up, do nothing.
        if self.is_warming_up:
            return []
        # Apply the price and market-cap filters.
        filtered = [self._symbol_data_by_symbol[f.symbol] for f in candidates if f.price > 5.0 and f.market_cap > 500_000_000]
        # Select the stocks with the greatest mean dollar volume over the last month.
        filtered = sorted(filtered, key=lambda sd: sd.fast_dollar_vol)[-self._fast_dollar_vol_filter_size:]
        
        
        # Get surge ratios of remaining stocks: 
        values = [sd.volume_surge_ratio for sd in filtered]
        values = np.array([sd.volume_surge_ratio for sd in filtered])
        mean = values.mean()
        std = values.std()
        threshold = mean + 2 * std

        filtered = [sd for sd in filtered if sd.roc.current.value > 0 and mean <= sd.volume_surge_ratio <= threshold]
        self.plot('Universe', 'Size', len(filtered))
        return [sd.symbol for sd in filtered]

    def _rebalance(self) -> None:
        # Form an equal-weighted portfolio.
        securities = [self.securities[symbol] for symbol in self._universe.selected]
        securities = [s for s in securities if s.price]
        self.set_holdings([PortfolioTarget(s, 1/len(securities)) for s in securities], True)


class SymbolData(object):

    def __init__(self, symbol, slow_period, fast_period):
        self.symbol = symbol
        self.roc = RateOfChange(slow_period)
        self.fast_dollar_vol = SimpleMovingAverage(fast_period)
        self.slow_dollar_vol = SimpleMovingAverage(slow_period-fast_period)
        self.delayed_slow_dollar_vol = IndicatorExtensions.of(Delay(fast_period), self.slow_dollar_vol)
        self.volume_surge_ratio = None

    def update(self, time, close, dollar_volume):
        self.fast_dollar_vol.update(time, dollar_volume)
        self.slow_dollar_vol.update(time, dollar_volume)
        if self.roc.update(time, close):
            self.volume_surge_ratio = self.fast_dollar_vol.current.value / self.delayed_slow_dollar_vol.current.value
            return True
        return False