Overall Statistics
from AlgorithmImports import *
from datetime import time, timedelta

class MNQScalper(QCAlgorithm):
    """
    Aggressive but risk‑controlled scalping algorithm for the Micro E‑mini Nasdaq‑100 (MNQ).

    Strategy Highlights
    -------------------
    ▸ 15‑second bars built from tick data (lowest stable resolution in LEAN)
    ▸ Momentum filter: EMA‑9 / EMA‑21 crossover and VWAP regime check
    â–¸ One position at a time; bracket (OCO) orders sized by fixed $ risk
    â–¸ Auto‑scales position: +1 MNQ for every fresh $5 k of equity (cap 10)
    â–¸ Daily guardrails: halt after +$500 or –$300 realised P&L
    """

    # === Parameters you will tune === #
    FAST_PERIOD      = 9
    SLOW_PERIOD      = 21
    VWAP_WINDOW_MIN  = 60          # minutes
    ATR_LEN          = 14
    PROFIT_MULT      = 2           # × ATR for target
    STOP_MULT        = 1           # × ATR for stop
    RISK_PER_TRADE   = 150         # $ risk reference (still used for lot calc safety)
    DAILY_TARGET     = 500
    DAILY_MAX_DD     = 300
    MAX_CONTRACTS    = 10          # liquidity/margin ceiling

    def Initialize(self):
        self.SetStartDate(2023, 1, 1)
        self.SetCash(25_000)
        self.SetTimeZone("America/New_York")

        # --- Add the Micro E‑mini Nasdaq‑100 root ("MNQ") ---
        future = self.AddFuture("MNQ", Resolution.Tick, Market.CME,
                                dataNormalizationMode=DataNormalizationMode.BackwardsRatio)
        future.SetFilter(0, 90)
        self.future_symbol = future.Symbol  # canonical root symbol

        # --- Consolidate ticks to 15‑second bars ---
        self.consolidator = TickConsolidator(timedelta(seconds=15))
        self.consolidator.DataConsolidated += self.OnBar
        self.SubscriptionManager.AddConsolidator(self.future_symbol, self.consolidator)

        # --- Indicators ---
        self.fast = ExponentialMovingAverage(self.FAST_PERIOD)
        self.slow = ExponentialMovingAverage(self.SLOW_PERIOD)
        self.atr  = AverageTrueRange(self.ATR_LEN)
        self.vwap = VolumeWeightedAveragePriceIndicator(int(self.VWAP_WINDOW_MIN * 60 / 15))

        self.RegisterIndicator(self.future_symbol, self.fast, self.consolidator)
        self.RegisterIndicator(self.future_symbol, self.slow, self.consolidator)
        self.RegisterIndicator(self.future_symbol, self.atr,  self.consolidator)
        self.RegisterIndicator(self.future_symbol, self.vwap, self.consolidator)

        # --- State ---
        self.daily_pl     = 0
        self.last_session = None
        self.brackets     = ()

    # -----------------------------------------------------
    # Consolidated 15‑sec bar handler
    # -----------------------------------------------------
    def OnBar(self, sender, bar: TradeBar):
        if not (self.fast.IsReady and self.slow.IsReady and self.atr.IsReady and self.vwap.IsReady):
            return

        # Session reset 00:00 ET (Lean deals in algorithm timezone)
        if self.last_session != self.Time.date():
            self.daily_pl     = 0
            self.last_session = self.Time.date()

        # Halt trading once daily thresholds are breached
        if self.daily_pl >= self.DAILY_TARGET or abs(self.daily_pl) >= self.DAILY_MAX_DD:
            return

        # Regular session filter (avoid globex chop/illiquidity)
        t = self.Time.time()
        if t < time(9, 35) or t > time(15, 45):
            return

        # Get current front‑month contract
        chain = self.CurrentSlice.FuturesChains.get(self.future_symbol)
        if not chain:  # no data yet
            return
        front = sorted(chain, key=lambda x: x.Expiry)[0]
        price = bar.Close

        # Momentum & VWAP regime
        long_ok  = self.fast > self.slow and price > self.vwap.Current.Value
        short_ok = self.fast < self.slow and price < self.vwap.Current.Value

        if self.Portfolio[front.Symbol].Invested:
            return  # one position max

        if long_ok or short_ok:
            direction = 1 if long_ok else -1
            qty = self._scale_qty()
            if qty == 0:
                self.Debug("Qty calc returned 0; skip trade")
                return

            ticket = self.MarketOrder(front.Symbol, direction * qty, False, "Entry")
            if ticket.Status != OrderStatus.Filled:
                return
            entry_price = ticket.AverageFillPrice

            atr_ticks = int(self.atr.Current.Value / 0.25)
            pt_price  = entry_price + direction * self.PROFIT_MULT * atr_ticks * 0.25
            sl_price  = entry_price - direction * self.STOP_MULT   * atr_ticks * 0.25

            pt = self.LimitOrder(front.Symbol, -direction * qty, pt_price, "PT", tag="Bracket")
            sl = self.StopMarketOrder(front.Symbol, -direction * qty, sl_price, "SL", tag="Bracket")
            self.brackets = (pt.OrderId, sl.OrderId)

    # -----------------------------------------------------
    # Order event to track P&L and bracket logic
    # -----------------------------------------------------
    def OnOrderEvent(self, oe: OrderEvent):
        if oe.Status != OrderStatus.Filled:
            return
        order = self.Transactions.GetOrderById(oe.OrderId)
        # Cancel sibling when one leg of bracket fills
        if order.Tag == "Bracket":
            for oid in self.brackets:
                if oid != oe.OrderId:
                    self.Transactions.CancelOrder(oid)
            self.brackets = ()
        # Update realised P&L
        self.daily_pl = self.Portfolio.TotalUnrealisedProfit + self.Portfolio.TotalProfit

    # -----------------------------------------------------
    # Helper: position sizing by equity staircase
    # -----------------------------------------------------
    def _scale_qty(self):
        equity   = self.Portfolio.TotalPortfolioValue
        allowed  = int(equity // 5000)
        qty      = max(1, min(allowed, self.MAX_CONTRACTS))
        return qty