Overall Statistics
Total Orders
383
Average Win
0.08%
Average Loss
-0.09%
Compounding Annual Return
8.134%
Drawdown
5.200%
Expectancy
0.540
Start Equity
1000000
End Equity
1146902
Net Profit
14.690%
Sharpe Ratio
0.049
Sortino Ratio
0.059
Probabilistic Sharpe Ratio
50.942%
Loss Rate
21%
Win Rate
79%
Profit-Loss Ratio
0.95
Alpha
-0.014
Beta
0.147
Annual Standard Deviation
0.055
Annual Variance
0.003
Information Ratio
-1.123
Tracking Error
0.101
Treynor Ratio
0.018
Total Fees
$446.63
Estimated Strategy Capacity
$82000000.00
Lowest Capacity Asset
QQQ RIWIV7K5Z9LX
Portfolio Turnover
1.10%
from AlgorithmImports import *
from collections import defaultdict
from datetime import timedelta, time

class VwmaStrategy(QCAlgorithm):

    def Initialize(self):
        # ---------------------------------------------------------
        #   Set basic parameters and framework
        # ---------------------------------------------------------
        self.SetStartDate(2023, 4, 1)
        self.SetEndDate(2024, 12, 30)
        self.SetCash(1000000)
        self.SetSecurityInitializer(lambda x: x.SetDataNormalizationMode(DataNormalizationMode.Raw))

        self.trade_weight       = 0.4      # e.g. 0.8 => 80% of portfolio equity
        self.minimum_threshold  = 0.002
        self.vwma_days          = 27
        self.days_rate_of_change= 6

        self.total_minutes = self.vwma_days * 960  

        # ---------------------------------------------------------
        #   Symbol configuration
        # ---------------------------------------------------------
        self.AddEquity("SPY", Resolution.Daily)
        self.symbol = "QQQ"
        self.AddEquity(self.symbol, Resolution.Minute)

        # ---------------------------------------------------------
        #   Store daily VWMA values
        # ---------------------------------------------------------
        self.vwma_history = []

        # ---------------------------------------------------------
        #   Schedule a daily event at market close
        # ---------------------------------------------------------
        self.Schedule.On(
            self.DateRules.EveryDay("SPY"),
            self.TimeRules.At(16, 0, 0),
            self.DayStart
        )


    def DayStart(self):
        """
        This gets called once per day at 16:00 (market close).
        We calculate the current VWMA, then decide whether to go
        long, short, or stay out based on the (N-day) rate of
        change of the VWMA, where N = days_rate_of_change.
        """
        today_vwma = self.CalculateVWMA(self.symbol, self.total_minutes)
        if today_vwma is None:
            return
        
        self.Debug(f"Date: {self.Time}, VWMA: {today_vwma}")
        
        # Store the latest VWMA in our history
        self.vwma_history.append(today_vwma)

        # Keep the vwma_history from growing too large; 30 is arbitrary
        if len(self.vwma_history) > 30:
            self.vwma_history.pop(0)

        # We need at least N+1 daily VWMA points to compute an N-day rate of change
        if len(self.vwma_history) < self.days_rate_of_change + 1:
            return

        # ---------------------------------------------------------
        #   Compute the N-day rate of change:
        #   roc = (VWMA_current - VWMA_n_days_ago) / VWMA_n_days_ago
        # ---------------------------------------------------------
        past_vwma    = self.vwma_history[-(self.days_rate_of_change + 1)]
        current_vwma = self.vwma_history[-1]

        if abs(past_vwma) < 1e-12:
            return

        roc = (current_vwma - past_vwma) / past_vwma

        # ---------------------------------------------------------
        #   Trading logic (using portfolio-weighted sizing)
        # ---------------------------------------------------------
        current_position = self.Portfolio[self.symbol].Quantity
        price            = self.Securities[self.symbol].Price

        # 1) If ROC > minimum_threshold => Long
        if roc > self.minimum_threshold:
            # How many shares correspond to `trade_weight` * total_portfolio_value
            desired_value_long = self.Portfolio.TotalPortfolioValue * self.trade_weight
            target_shares_long = int(desired_value_long / price)

            # Calculate how many shares we need to buy/sell to get from current_position to target_shares_long
            delta_shares = target_shares_long - current_position

            # If delta_shares > 0, we’re buying; if delta_shares < 0, we’re selling
            if delta_shares != 0:
                self.MarketOrder(self.symbol, delta_shares)
            return

        # 2) If ROC < -minimum_threshold => Short
        if roc < -self.minimum_threshold:
            # How many shares correspond to `trade_weight` * total_portfolio_value
            desired_value_short = self.Portfolio.TotalPortfolioValue * self.trade_weight
            # Convert that to shares, but for a short position we aim for negative holdings
            target_shares_short = int(desired_value_short / price)
            new_target_shares   = -target_shares_short  # Make it negative for short

            delta_shares = new_target_shares - current_position
            if delta_shares != 0:
                self.MarketOrder(self.symbol, delta_shares)
            return

        # 3) If |ROC| <= minimum_threshold => Liquidate
        if current_position != 0:
            self.Liquidate(self.symbol)


    def CalculateVWMA(self, symbol, bars_count):
        """
        Calculate a Volume-Weighted Moving Average (VWMA) over the last
        `bars_count` minute-bars (excluding today's data up to the current time).
        """
        history = self.history[TradeBar](symbol, bars_count, Resolution.Minute, extended_market_hours=True)
        if not history:
            return None

        # Exclude today's bars
        today = self.Time.date()
        history = [bar for bar in history if bar.Time.date() != today]
        if not history:
            return None

        vwma_sum = 0
        volume_sum = 0
        for bar in history:
            vwma_sum += bar.Close * bar.Volume
            volume_sum += bar.Volume

        if volume_sum == 0:
            return None

        return vwma_sum / volume_sum