Overall Statistics
Total Orders
2891
Average Win
0.09%
Average Loss
-0.08%
Compounding Annual Return
14.946%
Drawdown
3.000%
Expectancy
0.102
Start Equity
1000000
End Equity
1106841.84
Net Profit
10.684%
Sharpe Ratio
0.707
Sortino Ratio
0.774
Probabilistic Sharpe Ratio
66.549%
Loss Rate
49%
Win Rate
51%
Profit-Loss Ratio
1.15
Alpha
0.032
Beta
0.315
Annual Standard Deviation
0.069
Annual Variance
0.005
Information Ratio
-0.042
Tracking Error
0.098
Treynor Ratio
0.155
Total Fees
$3855.15
Estimated Strategy Capacity
$1300000000.00
Lowest Capacity Asset
DISCA TAHT8L1LVDR9
Portfolio Turnover
23.06%
Drawdown Recovery
89
# region imports
from AlgorithmImports import *
# endregion


class SpectralPeriodicityPremiumAlgorithm(QCAlgorithm):
    """Long high / short low intraday-volume periodicity (Track 1 volume_variance_explained), with long
    weights tilted toward names that persistently host execution programs (Track 2). Reproduces the
    SSRN 4230610 section 6.3 Periodic-minus-Smooth premium, refined so both tracks contribute each
    rebalance. Because same-day Track 2 signatures are sparse (~3% of ticker-days), the exec score is
    smoothed into a persistent execution-intensity proxy rather than used as a same-day hard filter."""

    def initialize(self):
        # Bound the run to the dataset coverage window (explicit end date is required here).
        self.set_start_date(2025, 10, 1)
        self.set_end_date(2026, 6, 23)
        self.set_cash(1_000_000)
        self.set_benchmark("SPY")
        self.settings.seed_initial_prices = True
        self.universe_settings.resolution = Resolution.DAILY
        self._spy = self.add_equity("SPY", Resolution.DAILY)
        self._rank_fraction = 0.2
        self._min_per_side = 5
        self._gross_exposure = 1.0
        # The freshest signal at a Monday 8 AM rebalance is Friday's, already 3 calendar days old
        # (4 after a Monday holiday), so the max signal age must be at least 4 to ever qualify.
        self._max_signal_age_days = 4
        self._tilt_by_exec = True
        # Smooth the sparse Track 2 score into a persistent execution-intensity proxy.
        self._exec_decay = 0.94
        self._tilt_strength = 1.0
        self._securities = []
        self._security_by_dataset_symbol = {}
        # Liquid large-cap S&P proxy matching the dataset coverage; broad enough for quintile sorts.
        self.add_universe(lambda fundamental: [
            f.symbol for f in sorted(
                [f for f in fundamental if f.has_fundamental_data and f.market_cap > 5e9],
                key=lambda f: f.dollar_volume)[-150:]])
        # Rebalance weekly at 8 AM, the daily-data cadence; the premium is a next-session hold (section 6.3).
        self.schedule.on(
            self.date_rules.week_start(self._spy), self.time_rules.at(8, 0), 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:
            if security == self._spy or security.symbol.security_type != SecurityType.EQUITY:
                continue
            if security in self._securities:
                continue
            self._securities.append(security)
            security.signal = None
            security.exec_intensity = 0.0
            security.dataset_symbol = self.add_data(
                QuantConnectSpectralTickFlowSignal, security, Resolution.DAILY).symbol
            self._security_by_dataset_symbol[security.dataset_symbol] = security
            # Replay recent history so the first weekly rebalance already has a signal value.
            history = self.history[QuantConnectSpectralTickFlowSignal](
                security.dataset_symbol, timedelta(days=10), Resolution.DAILY)
            for point in history:
                security.signal = point
                self._update_intensity(security, point.execution_score)
        for security in changes.removed_securities:
            if security not in self._securities:
                continue
            self._securities.remove(security)
            self._security_by_dataset_symbol.pop(security.dataset_symbol, None)
            self.remove_security(security.dataset_symbol)
            if security.invested:
                self.liquidate(security)

    def _update_intensity(self, security, execution_score):
        # Exponentially smooth the sparse exec score into a persistent execution-intensity proxy.
        security.exec_intensity = (self._exec_decay * security.exec_intensity
                                    + (1.0 - self._exec_decay) * execution_score)

    def on_data(self, data):
        # Ingest the day's signal rows and refresh each name's execution-intensity proxy.
        points = data.get(QuantConnectSpectralTickFlowSignal)
        if points:
            for point in points.values():
                security = self._security_by_dataset_symbol.get(point.symbol)
                if security is None:
                    continue
                security.signal = point
                self._update_intensity(security, point.execution_score)

    def _rebalance(self):
        # Rank fresh names by Track 1 periodicity; long the top, short the bottom, dollar-neutral.
        if self.is_warming_up:
            return
        # Keep only securities whose latest signal day is recent enough to act on.
        today = self.time.date()
        eligible = []
        for security in self._securities:
            if security.signal is None:
                continue
            if (today - security.signal.time.date()).days > self._max_signal_age_days:
                continue
            eligible.append(security)
        if len(eligible) < 2 * self._min_per_side:
            self.set_holdings([], True)
            self._plot_selection(0, 0, 0.0, 0.0, 0.0)
            return
        ranked = sorted(eligible, key=lambda security: security.signal.volume_variance_explained)
        count = max(self._min_per_side, int(len(ranked) * self._rank_fraction))
        count = min(count, len(ranked) // 2)
        short_securities = ranked[:count]
        long_securities = ranked[-count:]
        # Equal weight, optionally tilted toward names with stronger persistent Track 2 execution.
        leg_budget = self._gross_exposure / 2.0
        peak = max((security.exec_intensity for security in long_securities), default=0.0)
        if not self._tilt_by_exec or peak <= 0.0:
            weight_by_security = {security: leg_budget / len(long_securities) for security in long_securities}
        else:
            raw_by_security = {security: 1.0 + self._tilt_strength * security.exec_intensity / peak
                                for security in long_securities}
            total = sum(raw_by_security.values())
            weight_by_security = {security: leg_budget * raw / total for security, raw in raw_by_security.items()}
        short_weight = (self._gross_exposure / 2.0) / len(short_securities)
        targets = [PortfolioTarget(security, weight) for security, weight in weight_by_security.items()]
        targets += [PortfolioTarget(security, -short_weight) for security in short_securities]
        self.set_holdings(targets, True)
        self._plot_selection(
            len(long_securities), len(short_securities),
            self._mean_periodicity(long_securities), self._mean_periodicity(short_securities),
            sum(1 for security in long_securities if security.exec_intensity > 0.0) / len(long_securities)
            if long_securities else 0.0)

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

    def _plot_selection(self, long_count, short_count, long_mean_periodicity, short_mean_periodicity,
                         exec_active_long_fraction):
        # Chart leg sizes and the periodicity spread that drives the premium.
        self.plot("Spectral Selection", "Long Count", long_count)
        self.plot("Spectral Selection", "Short Count", short_count)
        self.plot("Spectral Signal", "Long Mean Periodicity", long_mean_periodicity)
        self.plot("Spectral Signal", "Short Mean Periodicity", short_mean_periodicity)
        self.plot("Spectral Signal", "Exec-Active Long Fraction", exec_active_long_fraction)