Overall Statistics
Total Orders
3501
Average Win
0.29%
Average Loss
-0.22%
Compounding Annual Return
48.509%
Drawdown
22.200%
Expectancy
0.254
Start Equity
1000000
End Equity
2688091.64
Net Profit
168.809%
Sharpe Ratio
1.423
Sortino Ratio
1.636
Probabilistic Sharpe Ratio
81.302%
Loss Rate
46%
Win Rate
54%
Profit-Loss Ratio
1.31
Alpha
0.158
Beta
1.317
Annual Standard Deviation
0.201
Annual Variance
0.04
Information Ratio
1.739
Tracking Error
0.109
Treynor Ratio
0.217
Total Fees
$12141.90
Estimated Strategy Capacity
$410000000.00
Lowest Capacity Asset
CTSH RBOKUIZLS38L
Portfolio Turnover
21.06%
Drawdown Recovery
112
# region imports
from AlgorithmImports import *
from plot import SpectralPlotter
# endregion


class SpectralPeriodicityPremiumAlgorithm(QCAlgorithm):

    def initialize(self):
        # Bound the run to the dataset coverage window (explicit end date is required here).
        self.set_start_date(2024, 1, 1)
        self.set_end_date(2026, 7, 1)
        self.set_cash(1_000_000)
        self.settings.seed_initial_prices = True
        self.universe_settings.resolution = Resolution.DAILY
        # Smooth the sparse score into a persistent execution-intensity proxy.
        self._exec_decay = 0.94
        # Long the top fifth of ranked names by periodicity strength.
        self._rank_fraction = 0.2
        self._date_rule = self.date_rules.week_start("SPY")
        self.universe_settings.schedule.on(self._date_rule)
        self._universe = self.add_universe(lambda fundamental:
            [f.symbol for f in sorted([f for f in fundamental if f.has_fundamental_data], key=lambda f: f.dollar_volume)[-100:]]
        )
        self._plotter = SpectralPlotter(self)
        self.set_warm_up(timedelta(14))

    def on_warmup_finished(self) -> None:
        # Rebalance weekly at 8 AM to match the daily-data cadence; the premium is a next-session hold.
        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):
        # Subscribe each added equity to its signal feed; drop feeds and holdings for removed names.
        for security in changes.added_securities:
            security.signal = None
            security.exec_intensity = 0.0
            security.dataset_symbol = self.add_data(QuantConnectSpectralTickFlowSignal, security, Resolution.DAILY).symbol
        for security in changes.removed_securities:
            self.remove_security(security.dataset_symbol)

    def _rebalance(self):
        # Rank the selected names by volume-periodicity strength, long the top slice, and tilt by execution intensity.
        if self.is_warming_up or not self._universe.selected:
            return
        eligible = []
        for symbol in self._universe.selected:
            security = self.securities[symbol]
            history = self.history[QuantConnectSpectralTickFlowSignal](security.dataset_symbol, timedelta(90), Resolution.DAILY)
            for point in history:
                security.signal = point
                # Fold this point's execution score into the persistent execution-intensity EMA.
                security.exec_intensity = self._exec_decay * security.exec_intensity + (1.0 - self._exec_decay) * point.execution_score
            # Drop names with no signal in the lookback window.
            if security.signal is None:
                continue
            eligible.append(security)
        # Sort ascending so the strongest periodicity lands at the tail.
        ranked = sorted(eligible, key=lambda security: security.signal.volume_variance_explained)
        # Take the top rank fraction, but never fewer than five names and never more than are available.
        count = max(5, int(len(ranked) * self._rank_fraction))
        count = min(count, len(ranked))
        long_securities = ranked[-count:]
        # Weight the longs by persistent execution intensity.
        peak = max((security.exec_intensity for security in long_securities))
        # Give each long a raw weight of one plus its execution intensity relative to the peak.
        raw_by_security = {security: 1.0 + security.exec_intensity / peak for security in long_securities}
        # Normalize each raw weight into a portfolio fraction that sums to one.
        weight_by_security = {security: raw / sum(raw_by_security.values()) for security, raw in raw_by_security.items()}
        # Build a portfolio target from each name's normalized weight.
        targets = [PortfolioTarget(security, weight) for security, weight in weight_by_security.items()]
        # Rebalance to the targets, liquidating any holdings not in the target list.
        self.set_holdings(targets, True)
        # Plot this rebalance's long selection for diagnostics.
        self._plotter.plot_selection(long_securities)
from AlgorithmImports import *


class SpectralPlotter:

    def __init__(self, algorithm):
        self._algorithm = algorithm

    def _plot(self, chart_name, series):
        for name, value in series:
            self._algorithm.plot(chart_name, name, value)

    def _mean_periodicity(self, securities):
        # Average intraday-volume periodicity strength across the leg, for diagnostics.
        if not securities:
            return 0.0
        return sum(security.signal.volume_variance_explained for security in securities) / len(securities)

    def _mean_exec_intensity(self, securities):
        # Average persistent execution-intensity proxy across the leg -- the execution tilt strength.
        if not securities:
            return 0.0
        return sum(security.exec_intensity for security in securities) / len(securities)

    def plot_selection(self, long_securities):
        # Chart the intraday-volume periodicity strength driving the premium.
        self._plot("Spectral Periodicity", [("Long Mean Periodicity", self._mean_periodicity(long_securities))])
        # Chart the execution-intensity strength tilting the long weights.
        self._plot("Spectral Exec Intensity", [("Long Mean Exec Intensity", self._mean_exec_intensity(long_securities))])
        # Chart how many names clear the periodicity ranking each rebalance.
        self._plot("Spectral Selection", [("Long Count", len(long_securities))])