Overall Statistics
Total Orders
4359
Average Win
0.28%
Average Loss
-0.27%
Compounding Annual Return
27.894%
Drawdown
21.900%
Expectancy
0.210
Start Equity
100000
End Equity
342332.73
Net Profit
242.333%
Sharpe Ratio
0.898
Sortino Ratio
1.098
Probabilistic Sharpe Ratio
50.731%
Loss Rate
41%
Win Rate
59%
Profit-Loss Ratio
1.04
Alpha
0.136
Beta
0.606
Annual Standard Deviation
0.188
Annual Variance
0.035
Information Ratio
0.647
Tracking Error
0.176
Treynor Ratio
0.279
Total Fees
$5202.75
Estimated Strategy Capacity
$3900000.00
Lowest Capacity Asset
GRMN S0DPIYB0VD5X
Portfolio Turnover
17.25%
Drawdown Recovery
290
from AlgorithmImports import *
from datetime import timedelta


class FractalMomentumCascade(QCAlgorithm):
    """
    Fractal momentum cascade — multi-timeframe momentum coherence.

    COMPLETELY INVENTED SIGNAL:
    Momentum Coherence Score (MCS) — requires momentum to be positive
    across THREE timeframes simultaneously:
      - Short:  ROC(10) — 2-week momentum
      - Medium: ROC(21) — monthly momentum
      - Long:   ROC(63) — quarterly momentum

    MCS = (ROC10 × ROC21 × ROC63) ^ (1/3)

    Gate: all three ROC > 0, Sharpe(126) > 0, price > SMA(200).
    Ranking: MCS / volatility.
    Universe: top-300 liquid $2B+ caps, PE > 0, ROE > 0, net margin > 0.
    Regime: SPY above 200-day SMA.
    Sizing: inverse-vol weighted top-25 at 2x leverage.
    Execution: biweekly rebalance Monday 31 min after open.
    """

    COARSE_SIZE = 800
    FINE_SIZE = 300
    MIN_PRICE = 10.0
    MIN_DOLLAR_VOL = 10e6
    MIN_MARKET_CAP = 2e9

    ROC_SHORT = 10
    ROC_MED = 21
    ROC_LONG = 63
    SMA_PERIOD = 200
    SHARPE_PERIOD = 126
    VOL_PERIOD = 21
    TOP_N = 25
    REGIME_PERIOD = 200

    MAX_GROSS = 2.0
    MAX_SINGLE = 0.20

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

        self.settings.minimum_order_margin_portfolio_percentage = 0
        self.settings.automatic_indicator_warm_up = True
        self.settings.seed_initial_prices = True
        self.set_warm_up(timedelta(days=400))

        self._spy = self.add_equity("SPY")
        self._spy_sma = self.sma(
            self._spy, self.REGIME_PERIOD, Resolution.DAILY
        )

        self._safe_haven = self.add_equity("GLD")

        self.universe_settings.resolution = Resolution.MINUTE
        self.universe_settings.schedule.on(
            self.date_rules.month_start(self._spy)
        )
        self._universe = self.add_universe(self._fundamental_filter)

    def on_warmup_finished(self):
        time_rule = self.time_rules.after_market_open(self._spy, 31)
        self.schedule.on(
            self.date_rules.week_start(self._spy),
            time_rule,
            self._weekly_check,
        )
        if self.live_mode:
            self._weekly_check()
        else:
            self.schedule.on(
                self.date_rules.today, time_rule,
                lambda: self._weekly_check(True),
            )

    # ── universe ─────────────────────────────────────────────

    def _fundamental_filter(self, fundamental):
        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 and
                f.market_cap >= self.MIN_MARKET_CAP and
                f.valuation_ratios.pe_ratio > 0 and
                f.operation_ratios.roe.value > 0 and
                f.operation_ratios.net_margin.value > 0)
        ]
        pool = sorted(pool, key=lambda f: f.dollar_volume)[-self.COARSE_SIZE:]
        pool = sorted(pool, key=lambda f: f.market_cap)[-self.FINE_SIZE:]
        return [f.symbol for f in pool]

    # ── security lifecycle ───────────────────────────────────

    def on_securities_changed(self, changes):
        for sec in changes.added_securities:
            if sec == self._spy:
                continue
            sec.sma = self.sma(sec, self.SMA_PERIOD, Resolution.DAILY)
            sec.sharpe = self.sr(
                sec, self.SHARPE_PERIOD, 0.0, Resolution.DAILY
            )
            sec.roc_s = self.roc(sec, self.ROC_SHORT, Resolution.DAILY)
            sec.roc_m = self.roc(sec, self.ROC_MED, Resolution.DAILY)
            sec.roc_l = self.roc(sec, self.ROC_LONG, Resolution.DAILY)
            roc1 = self.roc(sec, 1, Resolution.DAILY)
            sec.volatility = IndicatorExtensions.of(
                StandardDeviation(self.VOL_PERIOD), roc1
            )
            roc1.reset()
            for bar in self.history[TradeBar](
                sec, self.VOL_PERIOD + 1, Resolution.DAILY
            ):
                roc1.update(bar)

    # ── biweekly rebalance ───────────────────────────────────

    def _weekly_check(self, skip_week_check=False):
        if self.is_warming_up or not self._spy_sma.is_ready:
            return

        if self._spy.price < self._spy_sma.current.value:
            self.set_holdings(self._safe_haven, 1, True, tag="bear_regime")
            return
        if not skip_week_check and self.time.isocalendar()[1] % 2 != 0:
            return

        securities = [self.securities[sym] for sym in self._universe.selected]

        # filter eligible securities — all gates applied here
        eligible = [
            s for s in securities
            if (s != self._spy and
                s.sma.is_ready and s.sharpe.is_ready and
                s.roc_s.is_ready and s.roc_m.is_ready and
                s.roc_l.is_ready and s.volatility.is_ready and
                s.price and s.sma.current.value and
                s.volatility.current.value and
                s.price > s.sma.current.value and
                s.sharpe.current.value > 0 and
                s.roc_s.current.value > 0 and
                s.roc_m.current.value > 0 and
                s.roc_l.current.value > 0)
        ]

        def _mcs_score(s):
            mcs = (s.roc_s.current.value * s.roc_m.current.value
                   * s.roc_l.current.value) ** (1.0 / 3.0)
            return mcs / s.volatility.current.value

        top = sorted(eligible, key=_mcs_score, reverse=True)[:self.TOP_N]

        if not top:
            return

        # inverse-volatility weighting
        inv_vols = {s: 1.0 / s.volatility.current.value for s in top}
        total_inv = sum(inv_vols.values())
        weights = {
            s: min((iv / total_inv) * self.MAX_GROSS, self.MAX_SINGLE)
            for s, iv in inv_vols.items()
        }

        total_w = sum(weights.values())
        if total_w > self.MAX_GROSS:
            scale = self.MAX_GROSS / total_w
            weights = {s: w * scale for s, w in weights.items()}

        targets = [PortfolioTarget(s, w) for s, w in weights.items()]
        self.set_holdings(targets, True)