Overall Statistics
Total Orders
1895
Average Win
0.14%
Average Loss
-0.13%
Compounding Annual Return
19.834%
Drawdown
3.100%
Expectancy
0.111
Start Equity
1000000
End Equity
1140947.79
Net Profit
14.095%
Sharpe Ratio
0.979
Sortino Ratio
1.178
Probabilistic Sharpe Ratio
70.425%
Loss Rate
47%
Win Rate
53%
Profit-Loss Ratio
1.11
Alpha
0.064
Beta
0.355
Annual Standard Deviation
0.084
Annual Variance
0.007
Information Ratio
0.284
Tracking Error
0.104
Treynor Ratio
0.232
Total Fees
$2922.54
Estimated Strategy Capacity
$890000000.00
Lowest Capacity Asset
PGR R735QTJ8XC9X
Portfolio Turnover
22.89%
Drawdown Recovery
22
# 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
        # Long enough that a fresh EMA replay each rebalance converges to the same steady state on_data would reach.
        self._intensity_lookback_days = 90
        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], key=lambda f: f.dollar_volume)[-100:]]
        )
        # 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 or 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
        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 _rebalance(self):
        # Rank fresh names by Track 1 periodicity; long the top, short the bottom, dollar-neutral.
        if self.is_warming_up:
            return
        for security in self._securities:
            security.signal = None
            security.exec_intensity = 0.0
        dataset_symbols = [security.dataset_symbol for security in self._securities]
        if dataset_symbols:
            history = self.history[QuantConnectSpectralTickFlowSignal](dataset_symbols, timedelta(days=self._intensity_lookback_days), Resolution.DAILY)
            for data_dictionary in history:
                for point in data_dictionary.values():
                    security = self._security_by_dataset_symbol.get(point.symbol)
                    if security is None: 
                        continue
                    security.signal = point
                    security.exec_intensity = self._exec_decay * security.exec_intensity + (1.0 - self._exec_decay) * point.execution_score
        # Keep only securities whose latest signal day is recent enough to act on.
        eligible = []
        for security in self._securities:
            if (security.signal is None or
                    (self.time.date() - 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)