// Copyright QUANTOWER LLC. © 2017-2023. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using TradingPlatform.BusinessLayer; namespace NQProgressionStrategy { public class NQMartingale5M : Strategy, ICurrentAccount, ICurrentSymbol { [InputParameter("Symbol", 0)] public Symbol CurrentSymbol { get; set; } [InputParameter("Account", 1)] public Account CurrentAccount { get; set; } [InputParameter("Contracts", 2, 1, 100, 1, 0)] public int Contracts { get; set; } = 1; private const double EntryOffsetPoints = 37.25; // Changed from 0.50 to 37.25 private const double StopLossPoints = 1.00; private const double TakeProfitDollars = 220.0; // TP remains at $220 private const double DollarsPerPoint = 20.0; private const int MaxLevels = 15; // Asian session: 20:00 to 00:00 UTC-5 (8:00 PM to 12:00 AM EST) private const int AsianStartHour = 20; private const int AsianEndHour = 24; private HistoricalData _hdm; private string _marketOrderTypeId = string.Empty; private double takeProfitPoints; private double sessionOpen; private double upperEntry; private double lowerEntry; private DateTime currentSessionTime = DateTime.MinValue; private bool isFirstSession = true; private bool sessionActive = false; private int currentLevel = 1; private int currentLotSize = 1; private bool wonThisSession = false; private bool hadPosition = false; private int lastClosedPositionCount = 0; private DateTime lastEntryTime = DateTime.MinValue; public override string[] MonitoringConnectionsIds => new[] { this.CurrentSymbol?.ConnectionId, this.CurrentAccount?.ConnectionId }; public NQMartingale5M() { this.Name = "NQ Martingale - Asian Session"; this.Description = "Trades only during Asian session (8PM-12AM EST) with 37.25pt entry"; } protected override void OnRun() { if (this.CurrentSymbol != null && this.CurrentSymbol.State == BusinessObjectState.Fake) this.CurrentSymbol = Core.Instance.GetSymbol(this.CurrentSymbol.CreateInfo()); if (this.CurrentAccount != null && this.CurrentAccount.State == BusinessObjectState.Fake) this.CurrentAccount = Core.Instance.GetAccount(this.CurrentAccount.CreateInfo()); if (this.CurrentSymbol == null) { Log("Symbol null", StrategyLoggingLevel.Error); return; } if (this.CurrentAccount == null) { Log("Account null", StrategyLoggingLevel.Error); return; } if (this.CurrentSymbol.ConnectionId != this.CurrentAccount.ConnectionId) { Log("Different connections", StrategyLoggingLevel.Error); return; } var ot = Core.OrderTypes.FirstOrDefault(x => x.ConnectionId == this.CurrentSymbol.ConnectionId && x.Behavior == OrderTypeBehavior.Market); if (ot == null || string.IsNullOrEmpty(ot.Id)) { Log("No market orders", StrategyLoggingLevel.Error); return; } _marketOrderTypeId = ot.Id; takeProfitPoints = TakeProfitDollars / DollarsPerPoint; currentLevel = 1; currentLotSize = Contracts; isFirstSession = true; hadPosition = false; sessionActive = false; var start = Core.TimeUtils.DateTimeUtcNow.AddDays(-5); _hdm = this.CurrentSymbol.GetHistory(Period.MIN5, this.CurrentSymbol.HistoryType, start); _hdm.HistoryItemUpdated += HdmOnHistoryItemUpdated; lastClosedPositionCount = Core.Instance.ClosedPositions.Count(cp => cp.Account == this.CurrentAccount && cp.Symbol == this.CurrentSymbol); Log("Strategy started - Asian Session Only (8PM-12AM EST)", StrategyLoggingLevel.Trading); Log($"Entry Offset: ±{EntryOffsetPoints} points | TP: ${TakeProfitDollars} (11pts) | SL: {StopLossPoints} points", StrategyLoggingLevel.Trading); } protected override void OnStop() { if (_hdm != null) { _hdm.HistoryItemUpdated -= HdmOnHistoryItemUpdated; _hdm.Dispose(); _hdm = null; } base.OnStop(); } private void HdmOnHistoryItemUpdated(object sender, HistoryEventArgs e) { OnUpdate(); } private bool IsAsianSession(DateTime time) { // Convert to EST (UTC-5) TimeZoneInfo estZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"); DateTime estTime = TimeZoneInfo.ConvertTimeFromUtc(time, estZone); int hour = estTime.Hour; // Asian session: 20:00 to 23:59 (8PM to 11:59PM EST) return hour >= AsianStartHour && hour < AsianEndHour; } private void OnUpdate() { if (_hdm == null || _hdm.Count < 2) return; var currentTime = _hdm[0].TimeLeft; bool isAsianNow = IsAsianSession(currentTime); // Detect session start if (isAsianNow && !sessionActive) { sessionActive = true; currentSessionTime = currentTime; if (isFirstSession) { isFirstSession = false; Log("⏳ SKIPPING first Asian session", StrategyLoggingLevel.Trading); wonThisSession = true; return; } // NEW SESSION START sessionOpen = _hdm[0][PriceType.Open]; upperEntry = sessionOpen + EntryOffsetPoints; lowerEntry = sessionOpen - EntryOffsetPoints; wonThisSession = false; currentLevel = 1; currentLotSize = Contracts; hadPosition = false; Log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", StrategyLoggingLevel.Trading); Log($"🌏 ASIAN SESSION START | Open={sessionOpen:F2}", StrategyLoggingLevel.Trading); Log($" BUY Entry: {upperEntry:F2} (+{EntryOffsetPoints}pts)", StrategyLoggingLevel.Trading); Log($" SELL Entry: {lowerEntry:F2} (-{EntryOffsetPoints}pts)", StrategyLoggingLevel.Trading); Log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", StrategyLoggingLevel.Trading); } // Detect session end if (!isAsianNow && sessionActive) { sessionActive = false; Log("🌏 ASIAN SESSION ENDED - Waiting for next session...", StrategyLoggingLevel.Trading); // Close any open positions at session end if (HasOpenPosition()) { CancelAllOrders(); // You could add logic here to close positions if needed } return; } // Only trade during Asian session if (!sessionActive) return; // Check position status bool hasPos = HasOpenPosition(); // Detect position close and immediately re-enter on opposite side if stop loss if (hadPosition && !hasPos) { int currentClosedCount = Core.Instance.ClosedPositions.Count(cp => cp.Account == this.CurrentAccount && cp.Symbol == this.CurrentSymbol); if (currentClosedCount > lastClosedPositionCount) { lastClosedPositionCount = currentClosedCount; var closedPositions = Core.Instance.ClosedPositions.Where(cp => cp.Account == this.CurrentAccount && cp.Symbol == this.CurrentSymbol).ToArray(); if (closedPositions.Length > 0) { var lastTrade = closedPositions[closedPositions.Length - 1]; double pnl = lastTrade.GrossPnL != null ? lastTrade.GrossPnL.Value : 0; CancelAllOrders(); if (pnl > 0) { wonThisSession = true; Log($"🎯 TP HIT! PnL=${pnl:F2} - LOCKED for this session", StrategyLoggingLevel.Trading); } else { // Stop Loss hit - increase level and lot size currentLevel++; currentLotSize++; if (currentLevel > MaxLevels) { wonThisSession = true; Log($"❌ SL PnL=${pnl:F2} - MAX LEVELS REACHED ({MaxLevels})", StrategyLoggingLevel.Trading); } else { Log($"❌ SL HIT PnL=${pnl:F2} → Lvl{currentLevel} Size{currentLotSize}", StrategyLoggingLevel.Trading); System.Threading.Thread.Sleep(500); } } } } } hadPosition = hasPos; // If we just closed and haven't won this session and haven't hit max levels, check for re-entry if (!hasPos && !wonThisSession && currentLevel > 1) { var timeSinceLastEntry = Core.TimeUtils.DateTimeUtcNow - lastEntryTime; if (timeSinceLastEntry.TotalSeconds >= 1) { double currentPrice = _hdm[0][PriceType.Close]; // Check which side price is currently on if (currentPrice >= upperEntry) { Log($"⚡ RE-ENTRY BUY {currentLotSize} contracts | Lvl{currentLevel} | Price above {upperEntry:F2}", StrategyLoggingLevel.Trading); PlaceEntry(Side.Buy); return; } else if (currentPrice <= lowerEntry) { Log($"⚡ RE-ENTRY SELL {currentLotSize} contracts | Lvl{currentLevel} | Price below {lowerEntry:F2}", StrategyLoggingLevel.Trading); PlaceEntry(Side.Sell); return; } } return; } if (hasPos || wonThisSession) return; // Initial entry check (level 1 only - checks BOTH directions) if (currentLevel == 1) { double high0 = _hdm[0][PriceType.High]; double low0 = _hdm[0][PriceType.Low]; double prevClose = _hdm[1][PriceType.Close]; // Check BUY signal bool buyCross = prevClose < upperEntry && high0 >= upperEntry; if (buyCross) { Log($"🔔 BUY SIGNAL | {currentLotSize} contract | Lvl{currentLevel} | Entry: {upperEntry:F2}", StrategyLoggingLevel.Trading); PlaceEntry(Side.Buy); return; } // Check SELL signal bool sellCross = prevClose > lowerEntry && low0 <= lowerEntry; if (sellCross) { Log($"🔔 SELL SIGNAL | {currentLotSize} contract | Lvl{currentLevel} | Entry: {lowerEntry:F2}", StrategyLoggingLevel.Trading); PlaceEntry(Side.Sell); return; } } } private void CancelAllOrders() { var orders = Core.Instance.Orders.Where(o => o.Account == this.CurrentAccount && o.Symbol == this.CurrentSymbol).ToArray(); foreach (var order in orders) { Core.Instance.CancelOrder((IOrder)order); } } private void PlaceEntry(Side side) { try { lastEntryTime = Core.TimeUtils.DateTimeUtcNow; var req = new PlaceOrderRequestParameters { Account = this.CurrentAccount, Symbol = this.CurrentSymbol, OrderTypeId = _marketOrderTypeId, Quantity = this.currentLotSize, Side = side }; var res = Core.Instance.PlaceOrder(req); if (res.Status == TradingOperationResultStatus.Success) { Log($"✅ FILLED {side} {currentLotSize} contracts", StrategyLoggingLevel.Trading); PlaceTPandSL(side); } else { Log($"❌ ORDER FAILED: {res.Message}", StrategyLoggingLevel.Error); } } catch (Exception ex) { Log($"❌ EXCEPTION: {ex.Message}", StrategyLoggingLevel.Error); } } private void PlaceTPandSL(Side entrySide) { System.Threading.Thread.Sleep(300); var position = Core.Instance.Positions.FirstOrDefault(p => p.Account == this.CurrentAccount && p.Symbol == this.CurrentSymbol); if (position == null) return; double entry = position.OpenPrice; double tp = entrySide == Side.Buy ? entry + takeProfitPoints : entry - takeProfitPoints; double sl = entrySide == Side.Buy ? entry - StopLossPoints : entry + StopLossPoints; // Place Take Profit Core.Instance.PlaceOrder(new PlaceOrderRequestParameters() { Account = this.CurrentAccount, Symbol = this.CurrentSymbol, Side = entrySide == Side.Buy ? Side.Sell : Side.Buy, OrderTypeId = OrderType.Limit, Price = tp, Quantity = currentLotSize, TimeInForce = TimeInForce.GTC }); // Place Stop Loss Core.Instance.PlaceOrder(new PlaceOrderRequestParameters() { Account = this.CurrentAccount, Symbol = this.CurrentSymbol, Side = entrySide == Side.Buy ? Side.Sell : Side.Buy, OrderTypeId = OrderType.Stop, TriggerPrice = sl, Quantity = currentLotSize, TimeInForce = TimeInForce.GTC }); Log($" TP/SL SET → Entry={entry:F2} | SL={sl:F2} | TP={tp:F2}", StrategyLoggingLevel.Trading); } private bool HasOpenPosition() { return Core.Instance.Positions.Any(p => p.Symbol == this.CurrentSymbol && p.Account == this.CurrentAccount); } } }