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