// ============================================================ // NSQ_ATRChannelScalper.cs // Night Shift Quant · nightshiftquant.com // ============================================================ // // STRATEGY OVERVIEW // ----------------- // Mean-reversion scalper on ES Futures (or any liquid instrument). // Uses an EMA midline with ATR-dynamic channel bands: // · Upper band = EMA + (ATR × ChannelMultiple) // · Lower band = EMA - (ATR × ChannelMultiple) // // LOGIC // ----- // · Enter LONG when price tags the lower ATR band (oversold) // · Enter SHORT when price tags the upper ATR band (overbought) // · Target: EMA midline (price tends to revert to mean) // · Stop: ATR × StopMultiple beyond the band entry // · Session: 04:00 → 09:15 ET (pre-market, before NY open) // · One trade per direction per session // // WHY THIS EDGE EXISTS // -------------------- // Pre-market ES moves are often momentum-driven overreactions // to overnight futures positioning and thin liquidity. The market // tends to snap back toward the EMA once NY participants arrive. // This strategy exploits that snap-back statistically. // // PHILOSOPHY (nightshiftquant.com Issue 05) // ------------------------------------------ // ✓ 4 parameters only → walk-forward compliant // ✓ ATR-based stops → adapts to volatility automatically // ✓ Real-world logic → mean reversion in thin pre-market // ✓ Time exit → hard session close at 09:15 ET, no carry // // SETUP // ----- // 1. Copy to: Documents\NinjaTrader 8\bin\Custom\Strategies\ // 2. Compile with F5 in NinjaScript Editor // 3. Apply to ES or MES 5-min chart, session: "CME US Index Futures ETH" // 4. Run Walk-Forward in Strategy Analyzer before live trading // 5. Start with MES (Micro ES) — $1.25/tick vs $12.50 on full ES // // DEFAULT PARAMETERS (from backtest validation) // --------------------------------------------- // EMAPeriod : 20 (midline — price tends to revert here) // ChannelMultiple : 1.8 (ATR × 1.8 above/below EMA = band) // StopATRMultiple : 0.8 (stop placed 0.8 ATR beyond the band) // ATRPeriod : 14 (standard volatility lookback) // // DISCLAIMER // ---------- // Educational use only. Past performance does not guarantee // future results. Always use proper risk management. // nightshiftquant.com · hello@nightshiftquant.com // ============================================================ #region Using declarations using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Windows.Media; using NinjaTrader.Cbi; using NinjaTrader.Data; using NinjaTrader.NinjaScript; using NinjaTrader.NinjaScript.DrawingTools; #endregion namespace NinjaTrader.NinjaScript.Strategies { public class NSQ_ATRChannelScalper : Strategy { // ── Session State ────────────────────────────────────────── private bool longFired; // One long per session private bool shortFired; // One short per session private DateTime sessionDate; // Track current day // ── Indicators ──────────────────────────────────────────── private EMA ema; private ATR atr; // ── Time Gates (HHMMSS format) ───────────────────────────── // Pre-market window: 04:00 → 09:15 ET // Thin liquidity creates overextended moves → clean mean reversion private const int T_SESSION_START = 40000; // 04:00 ET private const int T_SESSION_END = 91500; // 09:15 ET // ───────────────────────────────────────────────────────── protected override void OnStateChange() { if (State == State.SetDefaults) { Description = "NSQ ATR Channel Scalper · nightshiftquant.com"; Name = "NSQ_ATRChannelScalper"; Calculate = Calculate.OnBarClose; EntriesPerDirection = 1; EntryHandling = EntryHandling.AllEntries; IsExitOnSessionCloseStrategy = true; ExitOnSessionCloseSeconds = 30; BarsRequiredToTrade = 25; IsUnmanaged = false; TraceOrders = false; // ── 4 Parameters Only ── (walk-forward rule) EMAPeriod = 20; ChannelMultiple = 1.8; StopATRMultiple = 0.8; ATRPeriod = 14; } else if (State == State.Configure) { ema = EMA(EMAPeriod); atr = ATR(ATRPeriod); } else if (State == State.DataLoaded) { ResetSession(); } } // ───────────────────────────────────────────────────────── protected override void OnBarUpdate() { if (CurrentBar < BarsRequiredToTrade) return; int barTime = ToTime(Time[0]); DateTime barDate = Time[0].Date; // ── STEP 1: New day → reset flags ───────────────────── if (barDate != sessionDate) { ResetSession(); sessionDate = barDate; } // ── STEP 2: Only trade pre-market window ────────────── if (barTime < T_SESSION_START || barTime >= T_SESSION_END) { // Force close at 09:15 ET — no carry into NY open if (barTime >= T_SESSION_END) { if (Position.MarketPosition == MarketPosition.Long) { ExitLong("TimeExit", "NSQ_ATR_Long"); Draw.Diamond(this, "ExitL_" + CurrentBar, false, 0, High[0] + (3 * TickSize), Brushes.Yellow); } else if (Position.MarketPosition == MarketPosition.Short) { ExitShort("TimeExit", "NSQ_ATR_Short"); Draw.Diamond(this, "ExitS_" + CurrentBar, false, 0, Low[0] - (3 * TickSize), Brushes.Yellow); } } return; } // ── STEP 3: Calculate channel bands ─────────────────── double midline = ema[0]; double atrValue = atr[0]; if (atrValue <= 0 || double.IsNaN(atrValue) || midline <= 0 || double.IsNaN(midline)) return; double upperBand = midline + (atrValue * ChannelMultiple); double lowerBand = midline - (atrValue * ChannelMultiple); // Visualize channel on chart string tag = barDate.ToString("MMdd"); Draw.HorizontalLine(this, "Upper_" + tag, false, upperBand, Brushes.OrangeRed, DashStyleHelper.Dash, 1); Draw.HorizontalLine(this, "Mid_" + tag, false, midline, Brushes.DimGray, DashStyleHelper.Dot, 1); Draw.HorizontalLine(this, "Lower_" + tag, false, lowerBand, Brushes.DodgerBlue, DashStyleHelper.Dash, 1); // ── STEP 4: Entry logic (mean reversion) ────────────── if (Position.MarketPosition == MarketPosition.Flat) { double stopDist = atrValue * StopATRMultiple; int stopTicks = (int)Math.Max(1, Math.Round(stopDist / TickSize)); // LONG: price tags lower band — expect reversion to midline if (!longFired && Low[0] <= lowerBand) { int tgtTicks = (int)Math.Round((Close[0] - midline) / TickSize); if (tgtTicks < 4) tgtTicks = 4; // minimum 1 point target SetStopLoss(CalculationMode.Ticks, stopTicks); SetProfitTarget(CalculationMode.Price, midline); EnterLong("NSQ_ATR_Long"); longFired = true; Draw.ArrowUp(this, "EntryL_" + CurrentBar, false, 0, Low[0] - (3 * TickSize), Brushes.LimeGreen); } // SHORT: price tags upper band — expect reversion to midline else if (!shortFired && High[0] >= upperBand) { int tgtTicks = (int)Math.Round((midline - Close[0]) / TickSize); if (tgtTicks < 4) tgtTicks = 4; SetStopLoss(CalculationMode.Ticks, stopTicks); SetProfitTarget(CalculationMode.Price, midline); EnterShort("NSQ_ATR_Short"); shortFired = true; Draw.ArrowDown(this, "EntryS_" + CurrentBar, false, 0, High[0] + (3 * TickSize), Brushes.OrangeRed); } } } // ───────────────────────────────────────────────────────── private void ResetSession() { longFired = false; shortFired = false; sessionDate = DateTime.MinValue; } // ───────────────────────────────────────────────────────── #region Properties [NinjaScriptProperty] [Range(5, 50)] [Display( Name = "EMA Period", Description = "Midline EMA period. Price reverts toward this after channel tags. Default: 20.", Order = 1, GroupName = "NSQ Parameters")] public int EMAPeriod { get; set; } [NinjaScriptProperty] [Range(0.5, 5.0)] [Display( Name = "Channel Multiple (ATR)", Description = "Bands = EMA ± (ATR × this value). Higher = fewer but cleaner signals. Default: 1.8.", Order = 2, GroupName = "NSQ Parameters")] public double ChannelMultiple { get; set; } [NinjaScriptProperty] [Range(0.3, 3.0)] [Display( Name = "Stop Loss — ATR Multiple", Description = "Stop placed ATR × this value beyond the band. Default: 0.8.", Order = 3, GroupName = "NSQ Parameters")] public double StopATRMultiple { get; set; } [NinjaScriptProperty] [Range(7, 30)] [Display( Name = "ATR Period", Description = "Lookback for ATR volatility measure. Default: 14.", Order = 4, GroupName = "NSQ Parameters")] public int ATRPeriod { get; set; } #endregion } }