Overall Statistics
Total Orders
12436
Average Win
0.11%
Average Loss
-0.17%
Compounding Annual Return
34.083%
Drawdown
30.500%
Expectancy
0.138
Start Equity
200000
End Equity
866067.35
Net Profit
333.034%
Sharpe Ratio
0.816
Sortino Ratio
0.929
Probabilistic Sharpe Ratio
33.065%
Loss Rate
30%
Win Rate
70%
Profit-Loss Ratio
0.63
Alpha
0.161
Beta
1.295
Annual Standard Deviation
0.295
Annual Variance
0.087
Information Ratio
0.759
Tracking Error
0.236
Treynor Ratio
0.186
Total Fees
$15570.54
Estimated Strategy Capacity
$93000000.00
Lowest Capacity Asset
NVS RULY784EQ6AT
Portfolio Turnover
25.48%
Drawdown Recovery
559
from AlgorithmImports import *
from datetime import timedelta
from typing import List
import numpy as np


class EliteDipBuyer_MOC(QCAlgorithm):
    """
    Long-only momentum strategy selecting high-Sharpe, large-cap equities
    above their 200-SMA, rebalanced at 8 AM (orders fill at open).
    """

    MAX_POSITIONS = 10
    TARGET_LEVERAGE = 1.5
    SHARPE_LOOKBACK = 252
    SHARPE_FLOOR = 0.6
    COARSE_SIZE = 300
    FINE_SIZE = 200
    MIN_PRICE = 50
    MIN_DOLLAR_VOL = 25e6
    MIN_MARKET_CAP = 10e9
    STOP_ATR_MULT = 4.0

    def initialize(self):
        self.set_start_date(self.end_date - timedelta(5 * 365))
        self.set_cash(200_000)

        self.settings.free_portfolio_value_percentage = 0.02
        self.settings.minimum_order_margin_portfolio_percentage = 0
        self.settings.seed_initial_prices = True
        self.settings.automatic_indicator_warm_up = True

        self.universe_settings.resolution = Resolution.DAILY
        self.universe_settings.leverage = 2.0

        self._universe = self.add_universe(self._fundamental_filter)

        self.schedule.on(
            self.date_rules.every_day("SPY"),
            self.time_rules.at(8, 0),
            self._rebalance,
        )

    def _fundamental_filter(self, fundamental: List[Fundamental]) -> List[Symbol]:
        pool = [
            f for f in fundamental
            if f.has_fundamental_data
            and f.price > self.MIN_PRICE
            and f.dollar_volume > self.MIN_DOLLAR_VOL
        ]
        pool = sorted(pool, key=lambda f: f.dollar_volume, reverse=True)[:self.COARSE_SIZE]

        pool = [f for f in pool if f.market_cap >= self.MIN_MARKET_CAP]
        pool = sorted(pool, key=lambda f: f.market_cap, reverse=True)[:self.FINE_SIZE]
        return [f.symbol for f in pool]

    def on_securities_changed(self, changes: SecurityChanges) -> None:
        for security in changes.added_securities:
            security.ichimoku = self.ichimoku(security, 9, 26, 17, 52, 26, 26)
            security.atr = self.atr(security, 50, MovingAverageType.WILDERS)
            security.sma = self.sma(security, 200)

            base_sharpe = self.sr(security, self.SHARPE_LOOKBACK, 0.0)
            security.sharpe_ratio = IndicatorExtensions.times(base_sharpe, float(np.sqrt(252)))

            security.stop_flag = False

    def on_data(self, data: Slice) -> None:
        if not data.bars:
            return
        for symbol in self._universe.selected:
            security = self.securities[symbol]
            if not security.ichimoku.is_ready or not security.atr.is_ready:
                continue

            atr_val = security.atr.current.value
            if atr_val <= 0:
                continue

            kijun = security.ichimoku.kijun.current.value
            if security.price < kijun - self.STOP_ATR_MULT * atr_val:
                security.stop_flag = True

    def _select_elite(self) -> List[Security]:
        selected = []
        for symbol in self._universe.selected:
            security = self.securities[symbol]
            if (not security.sma.is_ready or 
                not security.sharpe_ratio.is_ready or
                security.price < security.sma.current.value or
                security.sharpe_ratio.current.value < self.SHARPE_FLOOR):
                continue
            selected.append(security)
        return sorted(selected, key=lambda security: security.sharpe_ratio)[-self.MAX_POSITIONS:]
        
    def _rebalance(self) -> None:
        elite = self._select_elite()
        stopped = set()
        for symbol in self._universe.selected:
            security = self.securities[symbol]
            if security.stop_flag and security.invested:
                security.stop_flag = False
                stopped.add(security)

        elite = [s for s in elite if s not in stopped]
        if not elite:
            self.liquidate()
            return

        target_weight = self.TARGET_LEVERAGE / len(elite)
        targets = [PortfolioTarget(security, target_weight) for security in elite]
        self.set_holdings(targets, True)