Overall Statistics
Total Orders
24387
Average Win
0.39%
Average Loss
-0.07%
Compounding Annual Return
40.094%
Drawdown
9.100%
Expectancy
0.123
Start Equity
25000
End Equity
173219.94
Net Profit
592.880%
Sharpe Ratio
1.754
Sortino Ratio
2.108
Probabilistic Sharpe Ratio
97.362%
Loss Rate
83%
Win Rate
17%
Profit-Loss Ratio
5.51
Alpha
0.255
Beta
-0.015
Annual Standard Deviation
0.144
Annual Variance
0.021
Information Ratio
0.546
Tracking Error
0.257
Treynor Ratio
-17.086
Total Fees
$6479.21
Estimated Strategy Capacity
$11000000.00
Lowest Capacity Asset
QQQ RIWIV7K5Z9LX
Portfolio Turnover
2183.01%
Drawdown Recovery
183
"""
VWAP "Holy Grail" Intraday Day-Trading Strategy - Independent QuantConnect / LEAN Reproduction
=============================================================================================

A clean, fully reproducible QuantConnect LEAN implementation of the intraday
VWAP crossover ("VWAP Holy Grail") day-trading strategy from Zarattini & Aziz
(2023), "VWAP: The Holy Grail for Day Trading Systems" (SSRN working paper
4631351).

The original paper reports a Sharpe ratio of ~2.10 and a ~671% total return on
QQQ (the NASDAQ-100 ETF) over 2018-2023 from a one-line idea: go long when the
1-minute close is above the session VWAP, go short when it is below, and flatten
into the close. Several public QuantConnect replications (Seth "Algo_dude"
Lingafeldt and others) reported far lower numbers and concluded "there's no holy
grail here." This algorithm is the config-matched reproduction behind an
independent study that reconciles that gap.

Strategy rules (exactly as published)
-------------------------------------
  * Indicator: session-anchored VWAP, reset every morning at 09:30 ET, using the
    HLC/3 typical price weighted by volume (paper Eq. 1).
  * Long  when the 1-minute bar close is above VWAP.
  * Short when the 1-minute bar close is below VWAP.
  * Reverse on every VWAP crossover; 100% of equity per trade.
  * Intraday only: force-flat one minute before the close, no overnight risk.
  * Costs: $0.0005/share commission, zero slippage, zero bid-ask spread
    (paper §3.4).

Headline findings of the reproduction
-------------------------------------
  * In-sample (2018-2023) the effect is real and replicates across four
    independent backtest engines - the "coding error" narrative does not hold.
  * Out-of-sample (Sep 2023 onward, parameters frozen) the Sharpe ratio decays
    toward ~0.7: a textbook case of post-publication anomaly decay.
  * The edge disappears under realistic retail transaction costs: the break-even
    sits below ~1 bp round-trip, well under real QQQ effective spreads.

The full write-up covers equity curves, multi-timeframe robustness, and
transaction-cost sensitivity, alongside the complete data pipeline.

Keywords: VWAP strategy, VWAP Holy Grail, anchored VWAP, intraday momentum,
day trading algorithm, QQQ, mean reversion, VWAP crossover strategy, Zarattini
Aziz, SSRN 4631351, QuantConnect LEAN algorithm, algorithmic trading, backtest,
Sharpe ratio, out-of-sample, transaction costs.
"""

# region imports
# ruff: noqa: F403, F405
from AlgorithmImports import *
from datetime import time
import math

# endregion


START_DATE = (2018, 1, 2)
END_DATE = (2023, 9, 28)
STARTING_CASH = 25_000
TIME_ZONE = "America/New_York"
SYMBOL = "QQQ"
COMMISSION_PER_SHARE = 0.0005
LEVERAGE = 2.0
SKIP_OPENING_BARS = 1
EOD_FLATTEN_MINUTES_BEFORE_CLOSE = 1
RTH_OPEN = time(9, 30)
RTH_CLOSE = time(16, 0)


class PerShareFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = abs(parameters.Order.AbsoluteQuantity) * COMMISSION_PER_SHARE
        return OrderFee(CashAmount(fee, "USD"))


class NextOpenFillModel(FillModel):
    def __init__(self):
        super().__init__()
        self.fill_on_close = False

    def MarketFill(self, asset, order):
        fill = super().MarketFill(asset, order)
        bar = asset.GetLastData()
        if bar is not None:
            fill.FillPrice = bar.Close if self.fill_on_close else bar.Open
        return fill


class VWAPHolyGrail(QCAlgorithm):
    """Intraday VWAP crossover day-trading strategy (Zarattini-Aziz 2023) on QQQ:
    long above the session VWAP, short below, reversed on each cross and
    flattened before the close. See the module docstring for full reproduction
    notes and out-of-sample results."""

    def initialize(self):
        self.set_start_date(*START_DATE)
        self.set_end_date(*END_DATE)
        self.set_cash(STARTING_CASH)
        self.set_time_zone(TIME_ZONE)

        equity = self.add_equity(
            SYMBOL,
            Resolution.MINUTE,
            data_normalization_mode=DataNormalizationMode.RAW,
        )
        self.symbol = equity.symbol
        self.set_benchmark(self.symbol)

        self.set_brokerage_model(
            BrokerageName.INTERACTIVE_BROKERS_BROKERAGE,
            AccountType.MARGIN,
        )
        equity.set_fee_model(PerShareFeeModel())
        equity.set_slippage_model(ConstantSlippageModel(0.0))
        self.fill_model = NextOpenFillModel()
        equity.set_fill_model(self.fill_model)
        equity.set_buying_power_model(BuyingPowerModel(LEVERAGE))
        self.settings.free_portfolio_value_percentage = 0.0

        self.cum_pv = 0.0
        self.cum_volume = 0.0
        self.session_date = None
        self.last_signal = 0
        self.eod_locked = False

        self.schedule.on(
            self.date_rules.every_day(self.symbol),
            self.time_rules.before_market_close(
                self.symbol,
                EOD_FLATTEN_MINUTES_BEFORE_CLOSE,
            ),
            self.eod_flat,
        )

    def eod_flat(self):
        self.last_signal = 0
        if self.portfolio.invested:
            try:
                self.fill_model.fill_on_close = True
                self.liquidate(self.symbol)
            finally:
                self.fill_model.fill_on_close = False
        self.eod_locked = True

    def equity_at(self, price):
        cash = float(self.portfolio.cash_book["USD"].amount)
        quantity = float(self.portfolio[self.symbol].quantity)
        return cash + quantity * price

    def target_quantity(self, direction, price):
        equity = self.equity_at(price)
        current_quantity = abs(float(self.portfolio[self.symbol].quantity))
        available = equity - current_quantity * COMMISSION_PER_SHARE
        if price <= 0 or available <= 0:
            return 0
        return direction * math.floor(available / (price + COMMISSION_PER_SHARE))

    def trade_next_open(self, direction, price):
        current_quantity = float(self.portfolio[self.symbol].quantity)
        if current_quantity * direction > 0:
            return

        target_quantity = self.target_quantity(direction, price)
        delta = target_quantity - current_quantity
        if delta != 0:
            self.market_order(self.symbol, delta)

    def minutes_since_open(self, bar):
        return int(
            (bar.end_time - bar.end_time.replace(
                hour=RTH_OPEN.hour,
                minute=RTH_OPEN.minute,
                second=0,
                microsecond=0,
            )).total_seconds()
            // 60
        )

    def reset_session_if_needed(self, bar):
        session_date = bar.end_time.date()
        if self.session_date == session_date:
            return

        self.cum_pv = 0.0
        self.cum_volume = 0.0
        self.session_date = session_date
        self.last_signal = 0
        self.eod_locked = False

    def on_data(self, data: Slice):
        if self.symbol not in data.bars:
            return

        bar = data.bars[self.symbol]
        bar_time = bar.end_time.time()
        if not (RTH_OPEN < bar_time <= RTH_CLOSE):
            return

        self.reset_session_if_needed(bar)

        if self.last_signal != 0 and not self.eod_locked:
            self.trade_next_open(self.last_signal, bar.open)
            self.last_signal = 0

        hlc = (bar.high + bar.low + bar.close) / 3.0
        self.cum_pv += hlc * bar.volume
        self.cum_volume += bar.volume

        if self.cum_volume <= 0:
            self.last_signal = 0
            return

        if self.minutes_since_open(bar) <= SKIP_OPENING_BARS or self.eod_locked:
            self.last_signal = 0
            return

        vwap = self.cum_pv / self.cum_volume
        current_direction = 0
        if self.portfolio.invested:
            current_direction = 1 if self.portfolio[self.symbol].is_long else -1

        if bar.close > vwap and current_direction <= 0:
            self.last_signal = 1
        elif bar.close < vwap and current_direction >= 0:
            self.last_signal = -1
        else:
            self.last_signal = 0