Overall Statistics
Total Orders
658
Average Win
0.42%
Average Loss
-0.40%
Compounding Annual Return
0.637%
Drawdown
10.300%
Expectancy
0.006
Start Equity
100000
End Equity
100422.8
Net Profit
0.423%
Sharpe Ratio
-0.568
Sortino Ratio
-0.647
Probabilistic Sharpe Ratio
18.822%
Loss Rate
51%
Win Rate
49%
Profit-Loss Ratio
1.06
Alpha
-0.055
Beta
0.054
Annual Standard Deviation
0.083
Annual Variance
0.007
Information Ratio
-1.559
Tracking Error
0.129
Treynor Ratio
-0.874
Total Fees
$1414.70
Estimated Strategy Capacity
$860000000.00
Lowest Capacity Asset
ES YLZ9Z50BJE2P
Portfolio Turnover
741.48%
from AlgorithmImports import *
from math import *

class ESFuturesVWAP9EMACrossAlgorithm(QCAlgorithm):
    def Initialize(self):
        # Set start and end date for backtesting
        self.SetStartDate(2024, 1, 1)
        #self.SetEndDate(2024, 1, 1)
        
        # Set cash allocation
        self.SetCash(100000)

        resolution = Resolution.Second
        
        # Add ES futures contract (S&P 500 e-mini)
        self.es_future = self.AddFuture(Futures.Indices.SP500EMini, resolution, Market.CME)
        self.es_future.SetFilter(timedelta(0), timedelta(180))  # Set contract expiry filter
        
        # Variables to keep track of positions and entry state
        self.current_position = None
        self.position_entry_time = None  # Track entry time for position
        
        self.contract = {self.es_future.Symbol: None}
        
        # Schedule to liquidate positions outside regular market hours
        self.Schedule.On(
            self.DateRules.EveryDay(self.es_future.Symbol),
            self.TimeRules.BeforeMarketClose(self.es_future.Symbol, 1),
            self.ExitPositions
        )

        # Create indicators
        self.vwap = self.VWAP(self.es_future.Symbol, 14, resolution)
        self.ema9 = self.EMA(self.es_future.Symbol, 9, resolution)
        
        # Rolling windows to keep track of EMA, VWAP values, and recent bars
        self.ema_window = RollingWindow[float](2)    # Store the last 2 EMA values
        self.vwap_window = RollingWindow[float](2)    # Store the last 2 VWAP values
        self.price_window = RollingWindow[TradeBar](2)    # Store the last 2 bars
        
        # Request consolidated minute data
        self.consolidator = TradeBarConsolidator(timedelta(minutes=10))
        self.consolidator.DataConsolidated += self.OnDataConsolidated
        
        # Register the consolidator
        self.SubscriptionManager.AddConsolidator(self.es_future.Symbol, self.consolidator)

    def OnData(self, slice):
        for kvp in slice.FutureChains:
            symbol = kvp.Key
            chain = kvp.Value
            
            # Find the most liquid contract
            if symbol in self.contract:
                most_liquid_contract = sorted(chain, key=lambda contract: contract.OpenInterest, reverse=True)[0]
                self.contract[symbol] = most_liquid_contract

        # Check profit and stop loss targets for active positions
        if self.current_position is not None:
            contract = self.contract[self.es_future.Symbol]
            if contract is None: 
                return
            symbol = contract.Symbol

            # Retrieve current position details
            current_holdings = self.Portfolio[symbol]
            entry_price = current_holdings.AveragePrice if current_holdings.Invested else None

            if entry_price is not None:
                # Define profit and stop loss targets
                profit_target_points = 10
                stop_loss_points = 10

                if self.current_position == "long":
                    # Profit target and stop loss for long position
                    stop_loss_price = entry_price - stop_loss_points
                    profit_target_price = entry_price + profit_target_points

                    if slice[symbol].Close <= stop_loss_price:
                        self.Liquidate(symbol)
                        self.current_position = None
                        self.Debug(f"Long Stop Loss Hit: {self.Time}, Price: {slice[symbol].Close}")

                    elif slice[symbol].Close >= profit_target_price:
                        self.Liquidate(symbol)
                        self.current_position = None
                        self.Debug(f"Long Profit Target Hit: {self.Time}, Price: {slice[symbol].Close}")

                elif self.current_position == "short":
                    # Profit target and stop loss for short position
                    stop_loss_price = entry_price + stop_loss_points
                    profit_target_price = entry_price - profit_target_points

                    if slice[symbol].Close >= stop_loss_price:
                        self.Liquidate(symbol)
                        self.current_position = None
                        self.Debug(f"Short Stop Loss Hit: {self.Time}, Price: {slice[symbol].Close}")

                    elif slice[symbol].Close <= profit_target_price:
                        self.Liquidate(symbol)
                        self.current_position = None
                        self.Debug(f"Short Profit Target Hit: {self.Time}, Price: {slice[symbol].Close}")

    def OnDataConsolidated(self, sender, bar):
        contract = self.contract[self.es_future.Symbol]
        if contract is None: 
            return
        symbol = contract.Symbol

        # Add the latest bar to the rolling window
        self.price_window.Add(bar)

        # Ensure indicators are ready and rolling windows have enough data
        if not self.vwap.IsReady or not self.ema9.IsReady or self.price_window.Count < 2:
            return

        # Add the latest EMA and VWAP values to their respective rolling windows
        self.ema_window.Add(self.ema9.Current.Value)
        self.vwap_window.Add(self.vwap.Current.Value)

        # Ensure rolling windows have enough data
        if self.ema_window.Count < 2 or self.vwap_window.Count < 2:
            return

        # Get the current and previous values from rolling windows
        current_ema = self.ema_window[0]
        previous_ema = self.ema_window[1]
        current_vwap = self.vwap_window[0]
        previous_vwap = self.vwap_window[1]

        # Get the current and previous bars
        current_bar = self.price_window[0]
        previous_bar = self.price_window[1]

        # Long entry condition: 9EMA crosses above VWAP and the last 2 bars close above VWAP
        if previous_ema <= previous_vwap and current_ema > current_vwap and \
           current_bar.Close > previous_bar.Close and previous_bar.Close > previous_vwap:
            if self.current_position != "long" or not self.Portfolio.Invested:
                #self.Liquidate()
                limit_price = ceil(max(current_bar.High, previous_bar.High) + 1)  # Set limit price slightly above the high
                #self.LimitOrder(symbol, 1, limitPrice=limit_price)  # Use limit order for long entry
                self.MarketOrder(symbol, 1)
                self.current_position = "long"
                self.Debug(f"Entered Long: {self.Time}, EMA: {current_ema}, VWAP: {current_vwap}")

        # Short entry condition: 9EMA crosses below VWAP and the last 2 bars close below VWAP
        elif previous_ema >= previous_vwap and current_ema < current_vwap and \
             current_bar.Close < previous_bar.Close and previous_bar.Close < previous_vwap:
            if self.current_position != "short" or not self.Portfolio.Invested:
                #self.Liquidate()
                limit_price = ceil(min(current_bar.Low, previous_bar.Low) - 1)  # Set limit price slightly below the low
                #self.LimitOrder(symbol, -1, limitPrice=limit_price)  # Use limit order for short entry
                self.MarketOrder(symbol, -1)
                self.current_position = "short"
                self.Debug(f"Entered Short: {self.Time}, EMA: {current_ema}, VWAP: {current_vwap}")

    def ExitPositions(self):
        # Liquidate all positions if they exist
        if self.current_position is not None:
            self.Liquidate()
            self.current_position = None
            self.Debug(f"Position liquidated after the next candle: {self.Time}")