// Forward test of the EMA 5/8/13 ribbon on NQ, 15-minute bars, US session (protocol RB-M15-LS-N_nq_forward_protocol.md, RIP Backtest). // Long and short, no stop, no target. Entry at market when an alignment starts on a 15-minute bar closing after 09:30 and no later than 15:45 New York; // exit at market when the alignment breaks, and at the scheduled session close of the exchange calendar (16:00 New York, earlier on early-close days). // Live mode (TestMode = false, the default): trades only in real time on the account it is enabled on (Sim101), never on historical bars; marks the account // equity every minute and stops at the drawdown limit. Test mode (TestMode = true): trades on historical bars too, for the check against our own simulation in // Strategy Analyzer; never used for the forward record. Writes every event to a CSV journal. using System; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.IO; using NinjaTrader.Cbi; using NinjaTrader.Data; using NinjaTrader.NinjaScript; using NinjaTrader.NinjaScript.Indicators; namespace NinjaTrader.NinjaScript.Strategies { public class RibbonForwardNQ : Strategy { private EMA e5, e8, e13; private ATR atr; private TimeZoneInfo nyZone, chartZone; private int prevState; private double lastR, peakEquity = double.MinValue; private bool stopped; private string journalPath; private static readonly CultureInfo IC = CultureInfo.InvariantCulture; protected override void OnStateChange() { if (State == State.SetDefaults) { Name = "RibbonForwardNQ"; Description = "EMA 5/8/13 ribbon, NQ 15-minute, US session, long and short, no stop (RIP Backtest forward test RB-M15-LS-N)."; Calculate = Calculate.OnBarClose; EntriesPerDirection = 1; EntryHandling = EntryHandling.AllEntries; IsExitOnSessionCloseStrategy = false; BarsRequiredToTrade = 20; StartBehavior = StartBehavior.WaitUntilFlat; JournalFolder = @"C:\Users\\Documents\NinjaTrader 8\RibbonForward"; DrawdownLimit = 74054; TestMode = false; } else if (State == State.Configure) { AddDataSeries(BarsPeriodType.Minute, 1); // BarsInProgress 1: the session-close exit and the equity marks } else if (State == State.DataLoaded) { e5 = EMA(Closes[0], 5); e8 = EMA(Closes[0], 8); e13 = EMA(Closes[0], 13); atr = ATR(BarsArray[0], 14); nyZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"); chartZone = NinjaTrader.Core.Globals.GeneralOptions.TimeZoneInfo; Directory.CreateDirectory(JournalFolder); journalPath = Path.Combine(JournalFolder, TestMode ? "rb_test_journal.csv" : "rb_forward_journal.csv"); if (!File.Exists(journalPath)) File.WriteAllText(journalPath, "kind,time_ny,bar_close_ny,close,ema5,ema8,ema13,atr14,R,state,action,order,price,quantity,bid,ask,commission,note\r\n"); } else if (State == State.Realtime) { Log("start", ClockNy(), "", string.Format(IC, ",,,,,,,,,,,,,,,,account={0} instrument={1} testmode={2} position={3}", Account != null ? Account.Name : "?", Instrument.FullName, TestMode, Position.MarketPosition)); } else if (State == State.Terminated && journalPath != null) { Log("stop", ClockNy(), "", ",,,,,,,,,,,,,,,,strategy terminated"); } } // Exchange calendar of the US session close (New York), the same rules as rb_lib.js: 13:00 on MLK, Presidents, Memorial, Juneteenth (from 2022), // Independence Day (observed), Labor Day and Thanksgiving; 13:15 on the day after Thanksgiving, on 24 December and on 3 July when they are not the // observed holiday; otherwise 16:00. (NinjaTrader's last-bar-of-session flag did not mark the 13:00 early closes in the historical check.) private static int NthWeekday(int y, int m, DayOfWeek wd, int k) { int c = 0; for (int d = 1; d <= DateTime.DaysInMonth(y, m); d++) if (new DateTime(y, m, d).DayOfWeek == wd && ++c == k) return d; return -1; } private static int LastWeekday(int y, int m, DayOfWeek wd) { int r = -1; for (int d = 1; d <= DateTime.DaysInMonth(y, m); d++) if (new DateTime(y, m, d).DayOfWeek == wd) r = d; return r; } private static DateTime Observed(int y, int m, int d) { DateTime x = new DateTime(y, m, d); return x.DayOfWeek == DayOfWeek.Saturday ? x.AddDays(-1) : x.DayOfWeek == DayOfWeek.Sunday ? x.AddDays(1) : x; } public static TimeSpan SessionClose(DateTime day) { int y = day.Year; DateTime d = day.Date; TimeSpan h13 = new TimeSpan(13, 0, 0), h1315 = new TimeSpan(13, 15, 0); if (d == new DateTime(y, 1, NthWeekday(y, 1, DayOfWeek.Monday, 3)) || d == new DateTime(y, 2, NthWeekday(y, 2, DayOfWeek.Monday, 3)) || d == new DateTime(y, 5, LastWeekday(y, 5, DayOfWeek.Monday)) || d == new DateTime(y, 9, NthWeekday(y, 9, DayOfWeek.Monday, 1))) return h13; DateTime tg = new DateTime(y, 11, NthWeekday(y, 11, DayOfWeek.Thursday, 4)); if (d == tg) return h13; if (d == tg.AddDays(1)) return h1315; if (y >= 2022 && d == Observed(y, 6, 19)) return h13; DateTime j4 = Observed(y, 7, 4); if (d == j4) return h13; if (j4.Day == 4 && d == new DateTime(y, 7, 3) && d.DayOfWeek != DayOfWeek.Saturday && d.DayOfWeek != DayOfWeek.Sunday) return h1315; DateTime x24 = new DateTime(y, 12, 24); if (d == x24 && x24.DayOfWeek != DayOfWeek.Saturday && x24.DayOfWeek != DayOfWeek.Sunday && new DateTime(y, 12, 25).DayOfWeek != DayOfWeek.Saturday) return h1315; return new TimeSpan(16, 0, 0); } private bool Live { get { return TestMode || State == State.Realtime; } } // bar and execution times are in the chart time zone (Kind cleared before converting); the computer clock is local time private DateTime Ny(DateTime t) { return TimeZoneInfo.ConvertTime(DateTime.SpecifyKind(t, DateTimeKind.Unspecified), chartZone, nyZone); } private DateTime ClockNy() { return TimeZoneInfo.ConvertTime(DateTime.Now, TimeZoneInfo.Local, nyZone); } private DateTime NowNy() { return TestMode ? Ny(Times[BarsInProgress][0]) : ClockNy(); } private void Log(string kind, DateTime timeNy, string barClose, string rest) { File.AppendAllText(journalPath, string.Format(IC, "{0},{1:yyyy-MM-dd HH:mm:ss},{2}{3}\r\n", kind, timeNy, barClose, rest)); } private int Alignment() { const double eps = 1e-10; if (e5[0] - e8[0] > eps && e8[0] - e13[0] > eps) return 1; if (e8[0] - e5[0] > eps && e13[0] - e8[0] > eps) return -1; return 0; } private void Flatten(string why) { if (Position.MarketPosition == MarketPosition.Long) ExitLong(1, Position.Quantity, why, ""); else if (Position.MarketPosition == MarketPosition.Short) ExitShort(1, Position.Quantity, why, ""); } protected override void OnConnectionStatusUpdate(ConnectionStatusEventArgs connectionStatusUpdate) { if (journalPath != null) Log("connection", ClockNy(), "", string.Format(IC, ",,,,,,,,,,,,,,,,status={0} price_feed={1}", connectionStatusUpdate.Status, connectionStatusUpdate.PriceStatus)); } protected override void OnBarUpdate() { if (BarsInProgress == 1) { if (!Live) return; DateTime ny = Ny(Times[1][0]); // equity marks and the drawdown limit (live mode only): account net liquidation = cash after commissions + open profit or loss if (!TestMode && Account != null && !stopped) { double eq = Account.Get(AccountItem.NetLiquidation, Currency.UsDollar); if (eq > peakEquity) peakEquity = eq; if (peakEquity - eq >= DrawdownLimit) { stopped = true; Log("drawdown_stop", NowNy(), ny.ToString("yyyy-MM-dd HH:mm", IC), string.Format(IC, ",,,,,,,,,,,,{0:F2},,,,peak={1:F2} limit={2}", eq, peakEquity, DrawdownLimit)); Flatten("drawdown_stop"); } } // the scheduled session close from the exchange calendar: the exit is sent at the close of the minute before it (15:59, 12:59 or 13:14), so that it fills // while the market is still open (on an early-close day the market stops at the close and an order sent then would only fill at the reopening); // a data hole over that time: the first bar after it triggers the exit if (Position.MarketPosition != MarketPosition.Flat && ny.TimeOfDay >= SessionClose(ny) - new TimeSpan(0, 1, 0)) { Log("exit_signal", NowNy(), ny.ToString("yyyy-MM-dd HH:mm", IC), string.Format(IC, ",,,,,,,,session_end,,,,{0},{1},,", GetCurrentBid(1), GetCurrentAsk(1))); Flatten("session"); } return; } if (CurrentBars[0] < BarsRequiredToTrade || CurrentBars[1] < 1) return; int state = Alignment(); int start = state != 0 && state != prevState ? state : 0; prevState = state; if (!Live) return; // live mode: the averages are warmed up on history; no trade is taken on historical bars DateTime barNy = Ny(Times[0][0]); double R = 1.5 * atr[0]; string common = string.Format(IC, ",{0},{1:F2},{2:F4},{3:F4},{4:F4},{5:F4},{6:F4},{7}", barNy.ToString("yyyy-MM-dd HH:mm", IC), Closes[0][0], e5[0], e8[0], e13[0], atr[0], R, state); TimeSpan tod = barNy.TimeOfDay; bool inWindow = tod > new TimeSpan(9, 30, 0) && tod <= new TimeSpan(15, 45, 0) && tod < SessionClose(barNy); // no entry at or after an early close bool reverse = start != 0 && inWindow && !stopped; // exit on a broken alignment (a flip to the opposite alignment inside the window is done by the entry below, which reverses the position in one order) if (Position.MarketPosition == MarketPosition.Long && state != 1 && !(reverse && start < 0)) { Log("signal", NowNy(), "", common.Substring(1) + ",exit_long_alignment,,,,,,,"); ExitLong(1, Position.Quantity, "alignment", ""); } if (Position.MarketPosition == MarketPosition.Short && state != -1 && !(reverse && start > 0)) { Log("signal", NowNy(), "", common.Substring(1) + ",exit_short_alignment,,,,,,,"); ExitShort(1, Position.Quantity, "alignment", ""); } if (start == 0) return; if (stopped) { Log("signal", NowNy(), "", common.Substring(1) + ",no_entry_drawdown_stop,,,,,,,"); return; } if (!inWindow) { Log("signal", NowNy(), "", common.Substring(1) + string.Format(IC, ",{0}_start_outside_window,,,,,,,", start > 0 ? "bull" : "bear")); return; } lastR = R; Log("signal", NowNy(), "", common.Substring(1) + string.Format(IC, ",{0},{1},,1,{2},{3},,", start > 0 ? "enter_long" : "enter_short", start > 0 ? "long" : "short", GetCurrentBid(0), GetCurrentAsk(0))); // orders go to the 1-minute series (index 1): live they fill at the next price; on history at the next 1-minute open, as in rb_lib.js if (start > 0) EnterLong(1, 1, "long"); else EnterShort(1, 1, "short"); } protected override void OnExecutionUpdate(Execution execution, string executionId, double price, int quantity, MarketPosition marketPosition, string orderId, DateTime time) { if (!Live) return; Log("fill", Ny(time), "", string.Format(IC, ",,,,,,,{0:F4},,{1},{2},{3},{4},,,{5},{6}", lastR, marketPosition, execution.Order != null ? execution.Order.Name : "", price, quantity, execution.Commission, execution.Order != null ? execution.Order.OrderAction.ToString() : "")); } [NinjaScriptProperty] [Display(Name = "Journal folder", GroupName = "Forward test", Order = 1)] public string JournalFolder { get; set; } [NinjaScriptProperty] [Range(1, double.MaxValue)] [Display(Name = "Drawdown limit ($)", GroupName = "Forward test", Order = 2)] public double DrawdownLimit { get; set; } [NinjaScriptProperty] [Display(Name = "Test mode (historical check only)", GroupName = "Forward test", Order = 3)] public bool TestMode { get; set; } } }