From 29428b415bcc5c3c4ca6bf64b84756254111d4e1 Mon Sep 17 00:00:00 2001 From: gavindiaz Date: Mon, 6 Jul 2026 21:58:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=90=8E=E7=BC=80=E6=98=A0?= =?UTF-8?q?=E5=B0=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- NANDR_ORB_OB_EA.ex5 | Bin 0 -> 101004 bytes NANDR_ORB_OB_EA.mq5 | 2523 +++++++++++++++++ README.md | 87 +- config/ea_configs.yaml | 36 + .../MACD_MA_XAUUSD_5M_XAUUSD_H1_default.ini | Bin 0 -> 2673 bytes .../MACD_MA_XAUUSD_5M_XAUUSDc_H1_default.ini | Bin 0 -> 2675 bytes gui.py | 160 +- logs/smoke_alias.log | 28 + scripts/ini_generator.py | 16 +- scripts/mt5_auto_runner.py | 9 +- scripts/optimizer/ea_batch_optimizer.py | 3 +- scripts/symbol_resolver.py | 28 + tests/smoke_alias_backtest.py | 143 + tests/test_ini_generator.py | 96 + tests/test_symbol_resolver.py | 60 + tests/verify_reports.py | 16 + 16 files changed, 3157 insertions(+), 48 deletions(-) create mode 100644 NANDR_ORB_OB_EA.ex5 create mode 100644 NANDR_ORB_OB_EA.mq5 create mode 100644 config/generated_smoke/no_alias/MACD_MA_XAUUSD_5M_XAUUSD_H1_default.ini create mode 100644 config/generated_smoke/with_alias/MACD_MA_XAUUSD_5M_XAUUSDc_H1_default.ini create mode 100644 logs/smoke_alias.log create mode 100644 scripts/symbol_resolver.py create mode 100644 tests/smoke_alias_backtest.py create mode 100644 tests/test_ini_generator.py create mode 100644 tests/test_symbol_resolver.py create mode 100644 tests/verify_reports.py diff --git a/NANDR_ORB_OB_EA.ex5 b/NANDR_ORB_OB_EA.ex5 new file mode 100644 index 0000000000000000000000000000000000000000..2e91e24a90321d0759a458bd8d811554e3d23994 GIT binary patch literal 101004 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 z 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 A*Z=?k literal 0 HcmV?d00001 diff --git a/NANDR_ORB_OB_EA.mq5 b/NANDR_ORB_OB_EA.mq5 new file mode 100644 index 0000000..9185b94 --- /dev/null +++ b/NANDR_ORB_OB_EA.mq5 @@ -0,0 +1,2523 @@ +//+------------------------------------------------------------------+ +//| NANDR_ORB_OB_EA.mq5 | +//| ORB + Order Block Scalping EA for XAUUSD | +//| Supports M1 / M5 / M15 (default M15). ORB = 15 real minutes. | +//| Based on: ORB-All-Sessions.pine + LuxAlgo Order Block Detector | +//+------------------------------------------------------------------+ +#property copyright "NANDR" +#property version "1.40" +#property strict + +#include +#include +#include + +//+------------------------------------------------------------------+ +//| Enumerations | +//+------------------------------------------------------------------+ +enum ENUM_ORB_TF +{ + ORB_TF_M1 = 1, // M1 (15 bars = 15min ORB) + ORB_TF_M5 = 5, // M5 (3 bars = 15min ORB) + ORB_TF_M15 = 15 // M15 (1 bar = 15min ORB) [Default] +}; + +enum ENUM_SL_MODE +{ + SL_OB_BOUNDARY, // OB Boundary + SL_FIXED_PIPS, // Fixed Pips + SL_ATR_BASED, // ATR-Based + SL_ORB_OPPOSITE, // ORB Opposite Boundary (buy=ORL, sell=ORH) + SL_ORB_MID // ORB Range Midpoint +}; + +enum ENUM_TP_MODE +{ + TP_FIXED_PIPS, // Fixed Pips + TP_RR_RATIO // Risk:Reward Ratio +}; + +enum ENUM_LOT_MODE +{ + LOT_FIXED, // Fixed Lot Size + LOT_RISK_PERCENT // Risk % of Balance +}; + +enum ENUM_ENTRY_MODE +{ + ENTRY_MARKET, // Market Order (on retest bar close) + ENTRY_LIMIT // Limit Order (at ORB level) +}; + +enum ENUM_OB_MITIGATION +{ + MITIG_WICK, // Wick (high/low penetrates zone) + MITIG_CLOSE // Close (close penetrates zone) +}; + +enum ENUM_LADDER_SL_MODE +{ + LADDER_SL_FIXED_PIPS, // Use InpSLPips + LADDER_SL_LEVEL_BUFFER // Buffer beyond source level +}; + +// Coupled ORB risk methods (per ORB strategy spec). Each option fixes BOTH the +// stop-loss placement and the reward:risk ratio together. +enum ENUM_ORB_RISK_METHOD +{ + ORB_RISK_LEGACY, // Use InpSLMode / InpTPMode / InpRRRatio (manual) + ORB_RISK_BOUNDARY_1_2, // SL at broken range boundary → 1:2 RR + ORB_RISK_MID_1_1_5, // SL at range midpoint (50%) → 1:1.5 RR + ORB_RISK_OPPOSITE_1_1 // SL at opposite range boundary→ 1:1 RR +}; + +// Daily directional bias control. +enum ENUM_BIAS_MODE +{ + BIAS_AUTO, // Auto: first ORB breakout of the day sets the tradeable direction + BIAS_OFF, // Flat: take no trades today (news / high-uncertainty days) + BIAS_LONG_ONLY, // Manual: only long entries allowed + BIAS_SHORT_ONLY // Manual: only short entries allowed +}; + +//+------------------------------------------------------------------+ +//| Input Parameters | +//+------------------------------------------------------------------+ + +// --- Symbol --- +input group "═══ Symbol Settings ═══" +input string InpSymbolName = "XAUUSD"; // Symbol Name (auto-detected) + +// --- Timeframe --- +input group "═══ Timeframe Settings ═══" +input ENUM_ORB_TF InpOrbTimeframe = ORB_TF_M15; // ORB Timeframe (M1/M5/M15) +input int InpBreakoutConfBars = 0; // Breakout Confirm Bars (0=auto) + +// --- Sessions --- +input group "═══ Session Settings ═══" +input bool InpUseDailyOpen = true; // Daily Open Session (22:00 UTC, 01:00 Kuwait) +input bool InpUseTokyoSession = true; // Tokyo Session (00:00 UTC) +input bool InpUseLondonSession = true; // London Session (08:00 UTC = 8AM GMT) +input bool InpUseNYSession = true; // NY Session (12:00 UTC) +input bool InpUseNYOrbSession = true; // NY ORB Session (13:30 UTC = 9:30 EDT) +// Session start times are UTC-based (converted to broker server time internally). +input int InpDailyOpenHour = 22; // Daily Open Start Hour (UTC) +input int InpDailyOpenMin = 0; // Daily Open Start Minute +input int InpTokyoHour = 0; // Tokyo Start Hour (UTC) +input int InpTokyoMin = 0; // Tokyo Start Minute +input int InpLondonHour = 8; // London Start Hour (UTC, 8=8AM GMT) +input int InpLondonMin = 0; // London Start Minute +input int InpNYHour = 12; // NY Start Hour (UTC) +input int InpNYMin = 0; // NY Start Minute +input int InpNYOrbHour = 13; // NY ORB Start Hour (UTC) +input int InpNYOrbMin = 30; // NY ORB Start Minute + +// --- Order Block Settings --- +input group "═══ Order Block Settings ═══" +input int InpOBPivotLength = 0; // OB Pivot Length (0=auto by TF) +input int InpOBMaxCount = 5; // Max Active OBs per direction +input ENUM_OB_MITIGATION InpOBMitig = MITIG_WICK; // Mitigation Method +input bool InpOBRequireConf = false; // Require OB Confirmation +input double InpOBProximityPips = 50.0; // OB Proximity (pips) + +// --- Entry Settings --- +input group "═══ Entry Settings ═══" +input ENUM_ENTRY_MODE InpEntryMode = ENTRY_MARKET; // Entry Mode +input bool InpUseStrictFilter = false; // Use Strict Breakout Filter (false=simple close-cross) +input bool InpWaitForRetest = true; // Wait for ORB Retest Before Entry +input int InpBreakoutExpireBars = 20; // Bars Before Breakout Expires (0=never) +input bool InpUseOBRetestEntry = false; // OB Retest Entry (optional add-on, default OFF) +input int InpMaxRetestsPerSession = 3; // Max Retest Entries Per Session (per breakout) +input ENUM_BIAS_MODE InpDailyBiasMode = BIAS_AUTO; // Daily Bias: AUTO / OFF(no trades) / LONG-only / SHORT-only +input bool InpUseBreakoutVolFilter = false; // Require Volume Spike on Breakout Bar (fakeout filter) +input double InpBreakoutVolMult = 1.5; // Breakout Volume >= X * Average Volume +input int InpBreakoutVolAvgBars = 20; // Bars to Average Volume Over + +// --- Session Level Ladder --- +input group "═══ Session Level Ladder ═══" +input bool InpUseSessionLevelLadder = false; // Enable Session Level Ladder Entries +input bool InpLadderOnDailyOpen = true; // Ladder on Daily Open session +input bool InpLadderOnTokyo = true; // Ladder on Tokyo session +input bool InpLadderOnLondon = true; // Ladder on London session +input bool InpLadderOnNY = true; // Ladder on NY session +input bool InpLadderOnNYORB = true; // Ladder on NY ORB session +input bool InpUseLadderATRVolCeiling = false; // Use ATR Volatility Ceiling for Ladder Entries +input double InpLadderATRMaxPips = 200.0; // Max ATR (pips) allowed for Ladder Entries +input bool InpUseLadderSessionCaps = false; // Use Per-Session Ladder Trade Caps +input int InpLadderCapDailyOpen = 2; // Daily Open Ladder Cap (0=unlimited) +input int InpLadderCapTokyo = 2; // Tokyo Ladder Cap (0=unlimited) +input int InpLadderCapLondon = 2; // London Ladder Cap (0=unlimited) +input int InpLadderCapNY = 2; // NY Ladder Cap (0=unlimited) +input int InpLadderCapNYORB = 2; // NY ORB Ladder Cap (0=unlimited) +input ENUM_LADDER_SL_MODE InpLadderSLMode = LADDER_SL_FIXED_PIPS; // Ladder SL Mode +input double InpLadderSLBufferPips = 5.0; // Ladder SL Buffer Pips (when level-buffer mode) +input int InpLadderMaxTradesPerSession = 2; // Max Ladder Trades Per Session (0=unlimited) + +// --- Lot Size & Risk --- +input group "═══ Lot Size & Risk ═══" +input ENUM_LOT_MODE InpLotMode = LOT_RISK_PERCENT; // Lot Mode +input double InpFixedLotSize = 0.05; // Fixed Lot Size +input double InpRiskPercent = 0.05; // Risk % per Trade + +// --- Stop Loss --- +input group "═══ Stop Loss ═══" +input ENUM_ORB_RISK_METHOD InpOrbRiskMethod = ORB_RISK_BOUNDARY_1_2; // ORB Risk Method (sets SL + RR together) +input ENUM_SL_MODE InpSLMode = SL_OB_BOUNDARY; // SL Mode (used only when Risk Method = LEGACY) +input double InpSLPips = 100.0; // SL Fixed Pips +input double InpATRMultiplier = 1.5; // ATR Multiplier (for SL_ATR) +input int InpATRPeriod = 14; // ATR Period + +// --- Take Profit --- +input group "═══ Take Profit ═══" +input ENUM_TP_MODE InpTPMode = TP_RR_RATIO; // TP Mode (used only when Risk Method = LEGACY) +input double InpTPPips = 100.0; // TP Fixed Pips +input double InpRRRatio = 2.0; // Risk:Reward Ratio (LEGACY mode) + +// --- Trade Management --- +input group "═══ Trade Management ═══" +input double InpBreakevenTrigPips = 50.0; // Breakeven Trigger (pips) +input double InpBreakevenOffPips = 2.0; // Breakeven Offset (pips) +input double InpPartialCloseRatio = 0.5; // Partial Close Ratio at BE (0=disabled, 0.5=half) +input double InpTrailingStopPips = 0.0; // Trailing Stop Pips (0=off) +input int InpMaxTradesPerDay = 8; // Max Trades per Day +input int InpMaxPosPerSession = 2; // Max Positions per Session + +// --- Risk Management --- +input group "═══ Risk Management ═══" +input double InpMaxDailyLossUSD = 150.0; // Max Daily Loss USD (0=off) +input double InpMaxDailyLossPct = 5.0; // Max Daily Loss % (0=off) + +// --- Display --- +input group "═══ Display Settings ═══" +input bool InpShowDashboard = true; // Show Dashboard +input bool InpShowORBLines = true; // Show ORB Lines +input bool InpShowOBZones = true; // Show OB Zones +input bool InpShowTradeLabels = true; // Show Trade Labels + +// --- Magic --- +input int InpMagicNumber = 202601; // Magic Number + +//+------------------------------------------------------------------+ +//| Session structure | +//+------------------------------------------------------------------+ +struct SSession +{ + string name; + int startHour; + int startMin; + bool enabled; + + double orbHigh; + double orbLow; + int orbBarCount; + bool orbComplete; + datetime orbStartTime; + + int breakoutDir; // 0=none, 1=bull, -1=bear + bool inBreakout; + int breakoutBarsAgo; + bool breakoutEntryTaken; // one entry max per breakout confirmation leg + + bool ladderBullLowDone; // Bull ladder: low -> mid done + bool ladderBullMidDone; // Bull ladder: mid -> high done + bool ladderBearHighDone; // Bear ladder: high -> mid done + bool ladderBearMidDone; // Bear ladder: mid -> low done + + int ladderTradesThisSession; + + int tradesThisSession; + bool retestFiredThisBar; // blocks duplicate entries on consecutive ticks within same bar + + void Reset() + { + orbHigh = 0; + orbLow = DBL_MAX; + orbBarCount = 0; + orbComplete = false; + orbStartTime = 0; + breakoutDir = 0; + inBreakout = false; + breakoutBarsAgo = 0; + breakoutEntryTaken = false; + + ladderBullLowDone = false; + ladderBullMidDone = false; + ladderBearHighDone = false; + ladderBearMidDone = false; + ladderTradesThisSession = 0; + + tradesThisSession = 0; + retestFiredThisBar = false; + } +}; + +//+------------------------------------------------------------------+ +//| Order Block structure | +//+------------------------------------------------------------------+ +struct SOrderBlock +{ + double top; + double bottom; + double mid; + datetime time; + bool active; + bool traded; // Prevents re-entering the same OB zone twice in one day + string objName; +}; + +//+------------------------------------------------------------------+ +//| Global variables | +//+------------------------------------------------------------------+ +CTrade g_Trade; +CPositionInfo g_Position; +COrderInfo g_Order; + +string g_Symbol; +int g_OrbBarsNeeded; +int g_BreakoutConfBars; +int g_OBPivotLength; +double g_PipSize; +double g_PointSize; + +// Sessions array (max 5) +SSession g_Sessions[5]; +int g_SessionCount = 0; + +// Order blocks +SOrderBlock g_BullOBs[]; +SOrderBlock g_BearOBs[]; +int g_BullOBCount = 0; +int g_BearOBCount = 0; + +// Day bias: in AUTO mode this holds the direction of the first confirmed ORB +// breakout of the day (0=none, 1=bull, -1=bear). In manual bias modes it is unused. +// Breakout DETECTION is never suppressed (levels/state keep updating); entries are +// gated by IsDirectionAllowed() so the bias only filters trade execution. +int g_GlobalBiasDir = 0; + +// Tracks tickets that have already had partial close executed so it only fires once per position +#define MAX_PARTIAL_TRACKED 20 +ulong g_PartialClosedTickets[MAX_PARTIAL_TRACKED]; +int g_PartialClosedCount = 0; + +bool IsPartialClosed(ulong ticket) +{ + for(int i = 0; i < g_PartialClosedCount; i++) + if(g_PartialClosedTickets[i] == ticket) return true; + return false; +} + +void MarkPartialClosed(ulong ticket) +{ + if(g_PartialClosedCount < MAX_PARTIAL_TRACKED) + { + g_PartialClosedTickets[g_PartialClosedCount] = ticket; + g_PartialClosedCount++; + } +} + +// Daily stats +datetime g_LastDayReset = 0; +int g_TodayTrades = 0; +int g_TodayWins = 0; +int g_TodayLosses = 0; +double g_TodayPnL = 0; +double g_DayStartEquity = 0; +bool g_TradingHalted = false; +double g_TotalPnL = 0; + +// Bar tracking +datetime g_LastBarTime = 0; +bool g_IsNewBar = false; +int g_ServerUtcOffsetSec = 0; + +// ATR handle +int g_ATRHandle = INVALID_HANDLE; + +//+------------------------------------------------------------------+ +//| Utility: resolve actual symbol | +//+------------------------------------------------------------------+ +string ResolveSymbol(const string base) +{ + // Exact match first + if(SymbolInfoDouble(base, SYMBOL_BID) > 0) + return base; + + // Scan all symbols for XAUUSD variants + int total = SymbolsTotal(false); + for(int i = 0; i < total; i++) + { + string sym = SymbolName(i, false); + if(StringFind(sym, "XAUUSD") >= 0 || StringFind(sym, "GOLD") >= 0 || StringFind(sym, "XAU") >= 0) + { + if(SymbolInfoDouble(sym, SYMBOL_BID) > 0) + { + PrintFormat("NANDR EA: Symbol '%s' not found. Using '%s' instead.", base, sym); + return sym; + } + } + } + Print("NANDR EA: WARNING — could not resolve symbol '", base, "'. Using as-is."); + return base; +} + +//+------------------------------------------------------------------+ +//| Utility: pip value | +//+------------------------------------------------------------------+ +double PipsToPrice(double pips) +{ + return pips * g_PipSize; +} + +double PriceToPips(double price) +{ + return price / g_PipSize; +} + +//+------------------------------------------------------------------+ +//| Utility: normalize lot size | +//+------------------------------------------------------------------+ +double NormalizeLot(double lots) +{ + double step = SymbolInfoDouble(g_Symbol, SYMBOL_VOLUME_STEP); + double minL = SymbolInfoDouble(g_Symbol, SYMBOL_VOLUME_MIN); + double maxL = SymbolInfoDouble(g_Symbol, SYMBOL_VOLUME_MAX); + lots = MathFloor(lots / step) * step; + lots = MathMax(lots, minL); + lots = MathMin(lots, maxL); + return NormalizeDouble(lots, 2); +} + +//+------------------------------------------------------------------+ +//| Calculate lot size from risk | +//+------------------------------------------------------------------+ +double CalcLotSize(double slPips) +{ + if(InpLotMode == LOT_FIXED) + return NormalizeLot(InpFixedLotSize); + + double balance = AccountInfoDouble(ACCOUNT_BALANCE); + double riskUSD = balance * InpRiskPercent / 100.0; + double tickVal = SymbolInfoDouble(g_Symbol, SYMBOL_TRADE_TICK_VALUE); + double tickSize = SymbolInfoDouble(g_Symbol, SYMBOL_TRADE_TICK_SIZE); + double pipValue = tickVal * (g_PipSize / tickSize); + + if(pipValue <= 0 || slPips <= 0) + return NormalizeLot(InpFixedLotSize); + + double lots = riskUSD / (slPips * pipValue); + + // Safety cap: never risk more than 5% of balance per trade regardless of settings. + // This prevents runaway lot sizes caused by an incorrect SL distance (e.g. pip size bug, + // misconfigured pips, or SL that is too tight). + double maxRiskUSD = balance * 5.0 / 100.0; + double maxSafeLots = maxRiskUSD / (slPips * pipValue); + if(lots > maxSafeLots) + { + PrintFormat("NANDR EA: WARNING — Computed lots %.2f exceeds 5%% risk cap. " + "Clamped to %.2f. Check SL pips (%.1f) and pip size (%.5f).", + lots, maxSafeLots, slPips, g_PipSize); + lots = maxSafeLots; + } + + return NormalizeLot(lots); +} + +//+------------------------------------------------------------------+ +//| Calculate SL price | +//+------------------------------------------------------------------+ +double CalcSL(int dir, double entry, double obBottom, double obTop, double orbHigh, double orbLow) +{ + double sl = 0; + double fixedFallback = (dir > 0) ? entry - PipsToPrice(InpSLPips) + : entry + PipsToPrice(InpSLPips); + + if(InpSLMode == SL_FIXED_PIPS) + { + sl = fixedFallback; + } + else if(InpSLMode == SL_OB_BOUNDARY) + { + if(dir > 0 && obBottom > 0) + sl = obBottom - PipsToPrice(2.0); // 2 pip buffer below OB bottom + else if(dir < 0 && obTop > 0) + sl = obTop + PipsToPrice(2.0); // 2 pip buffer above OB top + else + sl = fixedFallback; + } + else if(InpSLMode == SL_ORB_OPPOSITE) + { + // Buy → SL at ORB Low; Sell → SL at ORB High (2 pip buffer beyond). + double raw = (dir > 0) ? orbLow - PipsToPrice(2.0) + : orbHigh + PipsToPrice(2.0); + bool valid = (dir > 0) ? (raw < entry) : (raw > entry); + sl = valid ? raw : fixedFallback; + } + else if(InpSLMode == SL_ORB_MID) + { + // SL at the midpoint of the opening range. + double mid = (orbHigh + orbLow) / 2.0; + bool valid = (dir > 0) ? (mid < entry) : (mid > entry); + sl = valid ? mid : fixedFallback; + } + else // SL_ATR_BASED + { + double atr[1]; + if(CopyBuffer(g_ATRHandle, 0, 0, 1, atr) > 0) + sl = (dir > 0) ? entry - atr[0] * InpATRMultiplier + : entry + atr[0] * InpATRMultiplier; + else + sl = fixedFallback; + } + return NormalizeDouble(sl, (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS)); +} + +//+------------------------------------------------------------------+ +//| Calculate TP price | +//+------------------------------------------------------------------+ +double CalcTP(int dir, double entry, double sl) +{ + double tp = 0; + double slDist = MathAbs(entry - sl); + if(InpTPMode == TP_FIXED_PIPS) + { + tp = (dir > 0) ? entry + PipsToPrice(InpTPPips) + : entry - PipsToPrice(InpTPPips); + } + else // RR ratio + { + tp = (dir > 0) ? entry + slDist * InpRRRatio + : entry - slDist * InpRRRatio; + } + return NormalizeDouble(tp, (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS)); +} + +//+------------------------------------------------------------------+ +//| Coupled ORB risk: derive SL and TP together from the opening | +//| range, per the selected risk method. | +//| BOUNDARY → SL at broken boundary (long=ORH, short=ORL), 1:2 | +//| MID → SL at range midpoint, 1:1.5 | +//| OPPOSITE → SL at opposite boundary (long=ORL, short=ORH), 1:1 | +//| Returns false when the method is LEGACY or the geometry is | +//| invalid (caller then falls back to CalcSL/CalcTP). | +//+------------------------------------------------------------------+ +bool CalcOrbRisk(int dir, double entry, double orbHigh, double orbLow, + double &slOut, double &tpOut) +{ + if(InpOrbRiskMethod == ORB_RISK_LEGACY) return false; + if(orbHigh <= 0 || orbLow >= DBL_MAX || orbHigh <= orbLow) return false; + + double buf = PipsToPrice(2.0); + double mid = (orbHigh + orbLow) / 2.0; + double slLevel = 0.0; + double rr = 0.0; + + switch(InpOrbRiskMethod) + { + case ORB_RISK_BOUNDARY_1_2: + slLevel = (dir > 0) ? orbHigh - buf : orbLow + buf; + rr = 2.0; + break; + case ORB_RISK_MID_1_1_5: + slLevel = mid; + rr = 1.5; + break; + case ORB_RISK_OPPOSITE_1_1: + slLevel = (dir > 0) ? orbLow - buf : orbHigh + buf; + rr = 1.0; + break; + default: + return false; + } + + // SL must sit on the correct side of entry. + if((dir > 0 && slLevel >= entry) || (dir < 0 && slLevel <= entry)) + return false; + + double slDist = MathAbs(entry - slLevel); + if(slDist <= 0) return false; + + double tpLevel = (dir > 0) ? entry + slDist * rr : entry - slDist * rr; + + int digits = (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS); + slOut = NormalizeDouble(slLevel, digits); + tpOut = NormalizeDouble(tpLevel, digits); + return true; +} + +//+------------------------------------------------------------------+ +//| Daily bias gate: is a trade in this direction allowed today? | +//+------------------------------------------------------------------+ +bool IsDirectionAllowed(int dir) +{ + switch(InpDailyBiasMode) + { + case BIAS_OFF: return false; // skip the day entirely + case BIAS_LONG_ONLY: return (dir > 0); + case BIAS_SHORT_ONLY: return (dir < 0); + case BIAS_AUTO: + default: + // First breakout of the day establishes the direction; afterwards only + // trades aligned with that direction are permitted. + if(g_GlobalBiasDir == 0) return true; + return (dir == g_GlobalBiasDir); + } +} + +//+------------------------------------------------------------------+ +//| Initialize sessions | +//+------------------------------------------------------------------+ +void InitSessions() +{ + g_SessionCount = 0; + + // Daily Open + if(InpUseDailyOpen) + { + g_Sessions[g_SessionCount].name = "DailyOpen"; + g_Sessions[g_SessionCount].startHour = InpDailyOpenHour; + g_Sessions[g_SessionCount].startMin = InpDailyOpenMin; + g_Sessions[g_SessionCount].enabled = true; + g_Sessions[g_SessionCount].Reset(); + g_SessionCount++; + } + // Tokyo + if(InpUseTokyoSession) + { + g_Sessions[g_SessionCount].name = "Tokyo"; + g_Sessions[g_SessionCount].startHour = InpTokyoHour; + g_Sessions[g_SessionCount].startMin = InpTokyoMin; + g_Sessions[g_SessionCount].enabled = true; + g_Sessions[g_SessionCount].Reset(); + g_SessionCount++; + } + // London + if(InpUseLondonSession) + { + g_Sessions[g_SessionCount].name = "London"; + g_Sessions[g_SessionCount].startHour = InpLondonHour; + g_Sessions[g_SessionCount].startMin = InpLondonMin; + g_Sessions[g_SessionCount].enabled = true; + g_Sessions[g_SessionCount].Reset(); + g_SessionCount++; + } + // NY + if(InpUseNYSession) + { + g_Sessions[g_SessionCount].name = "NY"; + g_Sessions[g_SessionCount].startHour = InpNYHour; + g_Sessions[g_SessionCount].startMin = InpNYMin; + g_Sessions[g_SessionCount].enabled = true; + g_Sessions[g_SessionCount].Reset(); + g_SessionCount++; + } + // NY ORB (stock market open) + if(InpUseNYOrbSession) + { + g_Sessions[g_SessionCount].name = "NYORB"; + g_Sessions[g_SessionCount].startHour = InpNYOrbHour; + g_Sessions[g_SessionCount].startMin = InpNYOrbMin; + g_Sessions[g_SessionCount].enabled = true; + g_Sessions[g_SessionCount].Reset(); + g_SessionCount++; + } +} + +//+------------------------------------------------------------------+ +//| Check if current bar is the session open bar | +//+------------------------------------------------------------------+ +bool IsSessionOpenBar(const SSession &sess, datetime barTime) +{ + // Session inputs are UTC-based; convert server bar time to UTC before matching. + datetime utcBarTime = barTime - g_ServerUtcOffsetSec; + MqlDateTime dt; + TimeToStruct(utcBarTime, dt); + return (dt.hour == sess.startHour && dt.min == sess.startMin); +} + +//+------------------------------------------------------------------+ +//| Check if current bar is within session ORB accumulation window | +//+------------------------------------------------------------------+ +bool IsWithinOrbWindow(const SSession &sess, datetime barTime) +{ + if(sess.orbStartTime == 0) return false; + int elapsed = (int)(barTime - sess.orbStartTime); + return elapsed < g_OrbBarsNeeded * (int)InpOrbTimeframe * 60; +} + +//+------------------------------------------------------------------+ +//| Update ORB for all sessions on new bar | +//| IMPORTANT: always reads bar[1] (the just-CLOSED candle) so the | +//| true high/low is captured. bar[0] at new-bar time = open only. | +//+------------------------------------------------------------------+ +void UpdateORBSessions() +{ + datetime bar1Time = iTime(g_Symbol, PERIOD_CURRENT, 1); + double bar1High = iHigh(g_Symbol, PERIOD_CURRENT, 1); + double bar1Low = iLow(g_Symbol, PERIOD_CURRENT, 1); + + // New bar: clear per-bar retest flag so the next wick can trigger a fresh entry + for(int s = 0; s < g_SessionCount; s++) + g_Sessions[s].retestFiredThisBar = false; + + for(int s = 0; s < g_SessionCount; s++) + { + if(!g_Sessions[s].enabled) continue; + + // Session open bar just closed: bar[1] time matches session start + if(IsSessionOpenBar(g_Sessions[s], bar1Time)) + { + g_Sessions[s].orbHigh = bar1High; + g_Sessions[s].orbLow = bar1Low; + g_Sessions[s].orbBarCount = 1; + g_Sessions[s].orbStartTime = bar1Time; + g_Sessions[s].orbComplete = (g_OrbBarsNeeded == 1); + g_Sessions[s].breakoutDir = 0; + g_Sessions[s].inBreakout = false; + g_Sessions[s].breakoutBarsAgo = 0; + g_Sessions[s].tradesThisSession= 0; + ResetLadderProgress(s); + + if(g_Sessions[s].orbComplete) + PrintFormat("NANDR EA: [%s] ORB complete. High=%.2f Low=%.2f", + g_Sessions[s].name, bar1High, bar1Low); + if(InpShowORBLines) + DrawORBLines(s); + continue; + } + + // Still accumulating (M5 or M1): append bar[1] closed data to range + if(!g_Sessions[s].orbComplete && g_Sessions[s].orbBarCount > 0 + && IsWithinOrbWindow(g_Sessions[s], bar1Time)) + { + g_Sessions[s].orbHigh = MathMax(g_Sessions[s].orbHigh, bar1High); + g_Sessions[s].orbLow = MathMin(g_Sessions[s].orbLow, bar1Low); + g_Sessions[s].orbBarCount++; + if(g_Sessions[s].orbBarCount >= g_OrbBarsNeeded) + { + g_Sessions[s].orbComplete = true; + PrintFormat("NANDR EA: [%s] ORB complete. High=%.2f Low=%.2f", + g_Sessions[s].name, g_Sessions[s].orbHigh, g_Sessions[s].orbLow); + } + if(InpShowORBLines) + DrawORBLines(s); + } + + // Track breakout bar age (only while breakout is active) + // NOTE: incremented here before DetectBreakouts, so newly-detected + // breakouts start at 0 here but will be seen as 0 in CheckEntrySignals + // because DetectBreakouts resets it to 0 AFTER this runs. + // The entry guard uses <=1 to handle both cases safely. + if(g_Sessions[s].inBreakout) + g_Sessions[s].breakoutBarsAgo++; + } +} + +//+------------------------------------------------------------------+ +//| Strict breakout filter (mirrors Pine filteredHighCrossBO/Low) | +//+------------------------------------------------------------------+ +bool CheckStrictBreakout(int dir, double orbLevel) +{ + int N = g_BreakoutConfBars; + if(N < 2) N = 2; + + // Need at least N+3 closed bars (all indices shifted +1 vs simple version) + int bars = Bars(g_Symbol, PERIOD_CURRENT); + if(bars < N + 3) return false; + + if(dir > 0) // bullish + { + if(iLow(g_Symbol, PERIOD_CURRENT, N + 2) >= orbLevel) return false; + for(int i = 2; i <= N + 1; i++) + { + if(iLow(g_Symbol, PERIOD_CURRENT, i) <= orbLevel) return false; + if(iClose(g_Symbol, PERIOD_CURRENT, i) <= orbLevel) return false; + } + if(iClose(g_Symbol, PERIOD_CURRENT, 1) <= iLow(g_Symbol, PERIOD_CURRENT, 2)) return false; + if(iLow(g_Symbol, PERIOD_CURRENT, 1) <= orbLevel) return false; + return true; + } + else // bearish + { + if(iHigh(g_Symbol, PERIOD_CURRENT, N + 2) <= orbLevel) return false; + for(int i = 2; i <= N + 1; i++) + { + if(iHigh(g_Symbol, PERIOD_CURRENT, i) >= orbLevel) return false; + if(iClose(g_Symbol, PERIOD_CURRENT, i) >= orbLevel) return false; + } + if(iClose(g_Symbol, PERIOD_CURRENT, 1) >= iHigh(g_Symbol, PERIOD_CURRENT, 2)) return false; + if(iHigh(g_Symbol, PERIOD_CURRENT, 1) >= orbLevel) return false; + return true; + } +} + +//+------------------------------------------------------------------+ +//| Simple breakout: uses bar[1] (just-closed) vs bar[2] | +//| Matches Pine Script bar-close evaluation — never checks an | +//| in-progress candle so no premature signals at bar open. | +//+------------------------------------------------------------------+ +bool CheckSimpleBreakout(int dir, double orbLevel) +{ + double c0 = iClose(g_Symbol, PERIOD_CURRENT, 1); // just-closed bar + double c1 = iClose(g_Symbol, PERIOD_CURRENT, 2); // bar before it + if(dir > 0) return (c1 <= orbLevel && c0 > orbLevel); + else return (c1 >= orbLevel && c0 < orbLevel); +} + +//+------------------------------------------------------------------+ +//| Volume-spike confirmation for the breakout bar (bar[1]) | +//| Optional fakeout filter: requires the just-closed breakout bar's | +//| tick volume to exceed a multiple of the recent average volume. | +//| Returns true (pass) when the filter is disabled or data is thin. | +//+------------------------------------------------------------------+ +bool IsBreakoutVolumeConfirmed() +{ + if(!InpUseBreakoutVolFilter) return true; + + int n = InpBreakoutVolAvgBars; + if(n < 1) n = 1; + + int bars = Bars(g_Symbol, PERIOD_CURRENT); + if(bars < n + 2) return true; // not enough history — don't block + + double sum = 0; + for(int i = 2; i <= n + 1; i++) + sum += (double)iVolume(g_Symbol, PERIOD_CURRENT, i); + + double avg = sum / n; + if(avg <= 0) return true; + + double breakoutVol = (double)iVolume(g_Symbol, PERIOD_CURRENT, 1); + return (breakoutVol >= avg * InpBreakoutVolMult); +} + +//+------------------------------------------------------------------+ +//| Detect breakout for all sessions | +//+------------------------------------------------------------------+ +void DetectBreakouts() +{ + for(int s = 0; s < g_SessionCount; s++) + { + if(!g_Sessions[s].enabled || !g_Sessions[s].orbComplete) continue; + if(g_Sessions[s].orbHigh <= 0 || g_Sessions[s].orbLow >= DBL_MAX) continue; + if(g_Sessions[s].inBreakout) continue; + + double c0 = iClose(g_Symbol, PERIOD_CURRENT, 1); // just-closed bar + bool outsideBull = (c0 > g_Sessions[s].orbHigh); + bool outsideBear = (c0 < g_Sessions[s].orbLow); + + bool bullBO = InpUseStrictFilter + ? CheckStrictBreakout( 1, g_Sessions[s].orbHigh) + : CheckSimpleBreakout( 1, g_Sessions[s].orbHigh); + bool bearBO = InpUseStrictFilter + ? CheckStrictBreakout(-1, g_Sessions[s].orbLow) + : CheckSimpleBreakout(-1, g_Sessions[s].orbLow); + + // Fallback arming for simple mode: if the exact close-cross event was missed, + // still arm breakout while price is already closed beyond the ORB boundary. + if(!InpUseStrictFilter) + { + if(!bullBO && outsideBull) + { + bullBO = true; + PrintFormat("NANDR EA: [%s] Bullish breakout armed by outside-range fallback. Close=%.2f ORBHigh=%.2f", + g_Sessions[s].name, c0, g_Sessions[s].orbHigh); + } + if(!bearBO && outsideBear) + { + bearBO = true; + PrintFormat("NANDR EA: [%s] Bearish breakout armed by outside-range fallback. Close=%.2f ORBLow=%.2f", + g_Sessions[s].name, c0, g_Sessions[s].orbLow); + } + } + + // Optional volume-spike confirmation (fakeout filter). Applies to whichever + // direction is breaking out; a weak-volume breakout bar is rejected. + if((bullBO || bearBO) && !IsBreakoutVolumeConfirmed()) + { + PrintFormat("NANDR EA: [%s] Breakout rejected — volume on breakout bar below %.2fx average.", + g_Sessions[s].name, InpBreakoutVolMult); + bullBO = false; + bearBO = false; + } + + if(bullBO) + { + g_Sessions[s].breakoutDir = 1; + g_Sessions[s].inBreakout = true; + g_Sessions[s].breakoutBarsAgo= 0; + g_Sessions[s].breakoutEntryTaken = false; + ResetLadderDirection(s, 1); + // In AUTO bias mode, the first breakout of the day sets the tradeable direction. + if(InpDailyBiasMode == BIAS_AUTO && g_GlobalBiasDir == 0) g_GlobalBiasDir = 1; + PrintFormat("NANDR EA: [%s] Bullish ORB breakout at %.2f [DayBias=%d]", + g_Sessions[s].name, g_Sessions[s].orbHigh, g_GlobalBiasDir); + } + else if(bearBO) + { + g_Sessions[s].breakoutDir = -1; + g_Sessions[s].inBreakout = true; + g_Sessions[s].breakoutBarsAgo= 0; + g_Sessions[s].breakoutEntryTaken = false; + ResetLadderDirection(s, -1); + // In AUTO bias mode, the first breakout of the day sets the tradeable direction. + if(InpDailyBiasMode == BIAS_AUTO && g_GlobalBiasDir == 0) g_GlobalBiasDir = -1; + PrintFormat("NANDR EA: [%s] Bearish ORB breakout at %.2f [DayBias=%d]", + g_Sessions[s].name, g_Sessions[s].orbLow, g_GlobalBiasDir); + } + } +} + +//+------------------------------------------------------------------+ +//| Update retest state on bar close: sweep reversals + invalidation | +//| Called once per new bar. Does NOT fire any trades. | +//+------------------------------------------------------------------+ +void UpdateRetestState(int sessIdx) +{ + if(!g_Sessions[sessIdx].inBreakout) return; + + // Use bar[1] (just-closed) and bar[2] for bar-close confirmation. + double c0 = iClose(g_Symbol, PERIOD_CURRENT, 1); + double c1 = iClose(g_Symbol, PERIOD_CURRENT, 2); + double h0 = iHigh(g_Symbol, PERIOD_CURRENT, 1); + double l0 = iLow(g_Symbol, PERIOD_CURRENT, 1); + + double orbHigh = g_Sessions[sessIdx].orbHigh; + double orbLow = g_Sessions[sessIdx].orbLow; + double orbMid = (orbHigh + orbLow) / 2.0; + + if(g_Sessions[sessIdx].breakoutDir == 1) + { + // Sweep reversal: bullish breakout failed — bar closed below orbHigh → flip to bearish + bool sweepRev = (c1 > orbHigh) && (l0 < orbHigh) && (c0 < orbHigh) && (c0 >= orbMid); + if(sweepRev) + { + g_Sessions[sessIdx].breakoutDir = -1; + g_Sessions[sessIdx].breakoutEntryTaken = false; + ResetLadderDirection(sessIdx, -1); + if(InpDailyBiasMode == BIAS_AUTO) g_GlobalBiasDir = -1; // swept & closed below → bias bearish + PrintFormat("NANDR EA: [%s] Bullish sweep reversal at orbHigh %.2f — flipping to BEARISH [DayBias=%d]", + g_Sessions[sessIdx].name, orbHigh, g_GlobalBiasDir); + } + // Full invalidation: closed below mid + else if(c0 < orbMid && c1 > orbMid) + { + PrintFormat("NANDR EA: [%s] Failed bullish retest (closed below mid %.2f)", + g_Sessions[sessIdx].name, orbMid); + g_Sessions[sessIdx].inBreakout = false; + g_Sessions[sessIdx].breakoutDir = 0; + g_Sessions[sessIdx].breakoutEntryTaken = false; + ResetLadderProgress(sessIdx); + } + } + else if(g_Sessions[sessIdx].breakoutDir == -1) + { + // Sweep reversal: bearish breakout failed — bar closed above orbLow → flip to bullish + bool sweepRev = (c1 < orbLow) && (h0 > orbLow) && (c0 > orbLow) && (c0 < orbHigh); + if(sweepRev) + { + g_Sessions[sessIdx].breakoutDir = 1; + g_Sessions[sessIdx].breakoutEntryTaken = false; + ResetLadderDirection(sessIdx, 1); + if(InpDailyBiasMode == BIAS_AUTO) g_GlobalBiasDir = 1; // swept & closed above → bias bullish + PrintFormat("NANDR EA: [%s] Bearish sweep reversal at orbLow %.2f — flipping to BULLISH [DayBias=%d]", + g_Sessions[sessIdx].name, orbLow, g_GlobalBiasDir); + } + // Full invalidation: closed above mid + else if(c0 > orbMid && c1 < orbMid) + { + PrintFormat("NANDR EA: [%s] Failed bearish retest (closed above mid %.2f)", + g_Sessions[sessIdx].name, orbMid); + g_Sessions[sessIdx].inBreakout = false; + g_Sessions[sessIdx].breakoutDir = 0; + g_Sessions[sessIdx].breakoutEntryTaken = false; + ResetLadderProgress(sessIdx); + } + } +} + +//+------------------------------------------------------------------+ +//| Order Block: scan for new OBs on new bar | +//+------------------------------------------------------------------+ +void ScanOrderBlocks() +{ + int length = g_OBPivotLength; + int barsNeeded = length * 2 + 2; + if(Bars(g_Symbol, PERIOD_CURRENT) < barsNeeded) return; + + // Check for volume pivot at bar[length] + // Volume at bar[length] must be highest in [0..2*length] + long pivotVol = iVolume(g_Symbol, PERIOD_CURRENT, length); + bool isVolPivot = true; + for(int i = 0; i < length * 2 + 1; i++) + { + if(i == length) continue; + if(iVolume(g_Symbol, PERIOD_CURRENT, i) >= pivotVol) + { + isVolPivot = false; + break; + } + } + if(!isVolPivot) return; + + // Market structure: os + // os=1 (bullish): low[length] is lowest low in the window + // os=0 (bearish): high[length] is highest high in the window + double pivotHigh = iHigh(g_Symbol, PERIOD_CURRENT, length); + double pivotLow = iLow(g_Symbol, PERIOD_CURRENT, length); + + double highestHigh = pivotHigh; + double lowestLow = pivotLow; + for(int i = 0; i < length * 2 + 1; i++) + { + highestHigh = MathMax(highestHigh, iHigh(g_Symbol, PERIOD_CURRENT, i)); + lowestLow = MathMin(lowestLow, iLow(g_Symbol, PERIOD_CURRENT, i)); + } + + int os = -1; // unknown + if(pivotHigh >= highestHigh) os = 0; // bearish structure + else if(pivotLow <= lowestLow) os = 1; // bullish structure + + if(os < 0) return; + + datetime obTime = iTime(g_Symbol, PERIOD_CURRENT, length); + double hl2 = (pivotHigh + pivotLow) / 2.0; + + if(os == 1) // Bullish OB: zone [low[length], hl2] + { + if(g_BullOBCount < InpOBMaxCount) + { + // Check not duplicate + for(int i = 0; i < g_BullOBCount; i++) + if(g_BullOBs[i].time == obTime) return; + + g_BullOBs[g_BullOBCount].top = hl2; + g_BullOBs[g_BullOBCount].bottom = pivotLow; + g_BullOBs[g_BullOBCount].mid = (hl2 + pivotLow) / 2.0; + g_BullOBs[g_BullOBCount].time = obTime; + g_BullOBs[g_BullOBCount].active = true; + g_BullOBs[g_BullOBCount].traded = false; + g_BullOBs[g_BullOBCount].objName= "NANDR_BullOB_" + IntegerToString(obTime); + g_BullOBCount++; + if(InpShowOBZones) DrawOBZone(g_BullOBCount - 1, true); + PrintFormat("NANDR EA: Bullish OB detected. Zone [%.2f - %.2f]", pivotLow, hl2); + } + } + else // Bearish OB: zone [hl2, high[length]] + { + if(g_BearOBCount < InpOBMaxCount) + { + for(int i = 0; i < g_BearOBCount; i++) + if(g_BearOBs[i].time == obTime) return; + + g_BearOBs[g_BearOBCount].top = pivotHigh; + g_BearOBs[g_BearOBCount].bottom = hl2; + g_BearOBs[g_BearOBCount].mid = (pivotHigh + hl2) / 2.0; + g_BearOBs[g_BearOBCount].time = obTime; + g_BearOBs[g_BearOBCount].active = true; + g_BearOBs[g_BearOBCount].traded = false; + g_BearOBs[g_BearOBCount].objName= "NANDR_BearOB_" + IntegerToString(obTime); + g_BearOBCount++; + if(InpShowOBZones) DrawOBZone(g_BearOBCount - 1, false); + PrintFormat("NANDR EA: Bearish OB detected. Zone [%.2f - %.2f]", hl2, pivotHigh); + } + } +} + +//+------------------------------------------------------------------+ +//| Remove mitigated OBs (Wick or Close method) | +//+------------------------------------------------------------------+ +void MitigateOrderBlocks() +{ + double c0 = iClose(g_Symbol, PERIOD_CURRENT, 0); + double l0 = iLow(g_Symbol, PERIOD_CURRENT, 0); + double h0 = iHigh(g_Symbol, PERIOD_CURRENT, 0); + + // Bullish OBs: mitigated if price goes below bottom + for(int i = g_BullOBCount - 1; i >= 0; i--) + { + if(!g_BullOBs[i].active) continue; + double target = (InpOBMitig == MITIG_WICK) ? l0 : c0; + if(target < g_BullOBs[i].bottom) + { + g_BullOBs[i].active = false; + if(InpShowOBZones) RemoveOBZone(g_BullOBs[i].objName); + // Shift array left + for(int j = i; j < g_BullOBCount - 1; j++) + g_BullOBs[j] = g_BullOBs[j+1]; + g_BullOBCount--; + } + } + + // Bearish OBs: mitigated if price goes above top + for(int i = g_BearOBCount - 1; i >= 0; i--) + { + if(!g_BearOBs[i].active) continue; + double target = (InpOBMitig == MITIG_WICK) ? h0 : c0; + if(target > g_BearOBs[i].top) + { + g_BearOBs[i].active = false; + if(InpShowOBZones) RemoveOBZone(g_BearOBs[i].objName); + for(int j = i; j < g_BearOBCount - 1; j++) + g_BearOBs[j] = g_BearOBs[j+1]; + g_BearOBCount--; + } + } +} + +//+------------------------------------------------------------------+ +//| Find nearest OB to a given price and direction | +//+------------------------------------------------------------------+ +bool IsOBNearLevel(double price, int dir, double &obTop, double &obBottom) +{ + double proxPrice = PipsToPrice(InpOBProximityPips); + + if(dir > 0) // Look for bullish OB near or below entry + { + double nearest = DBL_MAX; + int bestIdx = -1; + for(int i = 0; i < g_BullOBCount; i++) + { + if(!g_BullOBs[i].active) continue; + double dist = MathAbs(price - g_BullOBs[i].top); + if(dist <= proxPrice && dist < nearest) + { + nearest = dist; + bestIdx = i; + } + } + if(bestIdx >= 0) + { + obTop = g_BullOBs[bestIdx].top; + obBottom = g_BullOBs[bestIdx].bottom; + return true; + } + } + else // Look for bearish OB near or above entry + { + double nearest = DBL_MAX; + int bestIdx = -1; + for(int i = 0; i < g_BearOBCount; i++) + { + if(!g_BearOBs[i].active) continue; + double dist = MathAbs(price - g_BearOBs[i].bottom); + if(dist <= proxPrice && dist < nearest) + { + nearest = dist; + bestIdx = i; + } + } + if(bestIdx >= 0) + { + obTop = g_BearOBs[bestIdx].top; + obBottom = g_BearOBs[bestIdx].bottom; + return true; + } + } + + obTop = obBottom = 0; + return false; +} + +//+------------------------------------------------------------------+ +//| Session-scoped active exposure | +//+------------------------------------------------------------------+ +bool CommentHasSessionTag(const string comment, const string sessionName) +{ + if(comment == "" || sessionName == "") + return false; + return (StringFind(comment, "|" + sessionName + "|") >= 0); +} + +string ResolveSessionNameForUtcTime(datetime utcTime) +{ + MqlDateTime t; + TimeToStruct(utcTime, t); + int nowMin = t.hour * 60 + t.min; + + int bestIdx = -1; + int bestStart = -1; + for(int s = 0; s < g_SessionCount; s++) + { + if(!g_Sessions[s].enabled) + continue; + + int startMin = g_Sessions[s].startHour * 60 + g_Sessions[s].startMin; + if(startMin <= nowMin && startMin >= bestStart) + { + bestStart = startMin; + bestIdx = s; + } + } + + if(bestIdx >= 0) + return g_Sessions[bestIdx].name; + + for(int s = 0; s < g_SessionCount; s++) + { + if(g_Sessions[s].enabled) + return g_Sessions[s].name; + } + + return ""; +} + +int CountOpenPositionsForSession(const string sessionName) +{ + int count = 0; + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + if(g_Position.SelectByIndex(i)) + { + if(g_Position.Magic() == InpMagicNumber && g_Position.Symbol() == g_Symbol) + { + string comment = PositionGetString(POSITION_COMMENT); + if(CommentHasSessionTag(comment, sessionName)) + count++; + } + } + } + return count; +} + +int CountPendingOrdersForSession(const string sessionName) +{ + int count = 0; + for(int i = OrdersTotal() - 1; i >= 0; i--) + { + if(g_Order.SelectByIndex(i)) + { + if(g_Order.Magic() == InpMagicNumber && g_Order.Symbol() == g_Symbol) + { + string comment = OrderGetString(ORDER_COMMENT); + if(CommentHasSessionTag(comment, sessionName)) + count++; + } + } + } + return count; +} + +bool HasActiveExposureForSession(const string sessionName) +{ + return (CountOpenPositionsForSession(sessionName) + CountPendingOrdersForSession(sessionName)) > 0; +} + +bool HasActiveExposureForSession(int sessIdx) +{ + if(sessIdx < 0 || sessIdx >= g_SessionCount) + return false; + return HasActiveExposureForSession(g_Sessions[sessIdx].name); +} + +double SessionMid(const SSession &sess) +{ + return (sess.orbHigh + sess.orbLow) / 2.0; +} + +void ResetLadderProgress(int sessIdx) +{ + g_Sessions[sessIdx].ladderBullLowDone = false; + g_Sessions[sessIdx].ladderBullMidDone = false; + g_Sessions[sessIdx].ladderBearHighDone = false; + g_Sessions[sessIdx].ladderBearMidDone = false; + g_Sessions[sessIdx].ladderTradesThisSession = 0; +} + +void ResetLadderDirection(int sessIdx, int dir) +{ + if(dir > 0) + { + g_Sessions[sessIdx].ladderBullLowDone = false; + g_Sessions[sessIdx].ladderBullMidDone = false; + } + else + { + g_Sessions[sessIdx].ladderBearHighDone = false; + g_Sessions[sessIdx].ladderBearMidDone = false; + } +} + +void RefreshBreakoutEntryLocks() +{ + for(int s = 0; s < g_SessionCount; s++) + { + if(g_Sessions[s].inBreakout && !HasActiveExposureForSession(s)) + g_Sessions[s].breakoutEntryTaken = false; + } +} + +bool OpenLadderTrade(int sessIdx, int dir, double srcLevel, double dstLevel, const string stepTag) +{ + if(g_TradingHalted) return false; + if(g_TodayTrades >= InpMaxTradesPerDay) return false; + if(HasActiveExposureForSession(sessIdx)) return false; + int ladderCap = GetEffectiveLadderCap(sessIdx); + if(ladderCap > 0 && g_Sessions[sessIdx].ladderTradesThisSession >= ladderCap) + return false; + + double ask = SymbolInfoDouble(g_Symbol, SYMBOL_ASK); + double bid = SymbolInfoDouble(g_Symbol, SYMBOL_BID); + double entry = (dir > 0) ? ask : bid; + double tp = NormalizeDouble(dstLevel, (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS)); + double sl = 0.0; + + if(InpLadderSLMode == LADDER_SL_LEVEL_BUFFER) + { + sl = (dir > 0) ? srcLevel - PipsToPrice(InpLadderSLBufferPips) + : srcLevel + PipsToPrice(InpLadderSLBufferPips); + } + else + { + sl = (dir > 0) ? entry - PipsToPrice(InpSLPips) + : entry + PipsToPrice(InpSLPips); + } + sl = NormalizeDouble(sl, (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS)); + + if((dir > 0 && (tp <= entry || sl >= entry)) + || (dir < 0 && (tp >= entry || sl <= entry))) + return false; + + double slPips = PriceToPips(MathAbs(entry - sl)); + if(slPips <= 0) return false; + double lots = CalcLotSize(slPips); + + string comment = StringFormat("NANDR|Ladder|%s|%s|%s", + g_Sessions[sessIdx].name, + (dir > 0 ? "BUY" : "SELL"), + stepTag); + + bool result = (dir > 0) + ? g_Trade.Buy(lots, g_Symbol, 0, sl, tp, comment) + : g_Trade.Sell(lots, g_Symbol, 0, sl, tp, comment); + + if(!result) + { + PrintFormat("NANDR EA: Ladder trade failed. Step=%s Session=%s Error=%d", + stepTag, g_Sessions[sessIdx].name, GetLastError()); + return false; + } + + g_TodayTrades++; + g_Sessions[sessIdx].tradesThisSession++; + g_Sessions[sessIdx].ladderTradesThisSession++; + g_Sessions[sessIdx].breakoutEntryTaken = true; + + PrintFormat("NANDR EA: Ladder trade opened. Step=%s Dir=%s Lots=%.2f Entry=%.2f SL=%.2f TP=%.2f Session=%s", + stepTag, (dir > 0 ? "BUY" : "SELL"), lots, entry, sl, tp, g_Sessions[sessIdx].name); + if(InpShowTradeLabels) + DrawTradeLabel(dir, entry, sl, tp); + + return true; +} + +bool IsLadderEnabledForSession(int sessIdx) +{ + string name = g_Sessions[sessIdx].name; + if(name == "DailyOpen") return InpLadderOnDailyOpen; + if(name == "Tokyo") return InpLadderOnTokyo; + if(name == "London") return InpLadderOnLondon; + if(name == "NY") return InpLadderOnNY; + if(name == "NYORB") return InpLadderOnNYORB; + return true; +} + +int GetLadderSessionCap(int sessIdx) +{ + if(!InpUseLadderSessionCaps) + return 0; + + string name = g_Sessions[sessIdx].name; + if(name == "DailyOpen") return InpLadderCapDailyOpen; + if(name == "Tokyo") return InpLadderCapTokyo; + if(name == "London") return InpLadderCapLondon; + if(name == "NY") return InpLadderCapNY; + if(name == "NYORB") return InpLadderCapNYORB; + return 0; +} + +int GetEffectiveLadderCap(int sessIdx) +{ + int globalCap = InpLadderMaxTradesPerSession; + int sessionCap = GetLadderSessionCap(sessIdx); + + if(globalCap <= 0) return sessionCap; + if(sessionCap <= 0) return globalCap; + return MathMin(globalCap, sessionCap); +} + +bool IsLadderVolatilityTooHigh(double &atrPips) +{ + atrPips = 0.0; + if(!InpUseLadderATRVolCeiling) + return false; + if(g_ATRHandle == INVALID_HANDLE) + return false; + + double atr[1]; + if(CopyBuffer(g_ATRHandle, 0, 0, 1, atr) <= 0) + return false; + + atrPips = PriceToPips(atr[0]); + return (atrPips > InpLadderATRMaxPips); +} + +bool TrySessionLadderEntry(int sessIdx, + double c1, + double h_cur, + double l_cur, + double ask, + double bid, + double tol) +{ + if(!InpUseSessionLevelLadder) return false; + if(!IsLadderEnabledForSession(sessIdx)) return false; + if(!g_Sessions[sessIdx].enabled || !g_Sessions[sessIdx].orbComplete) return false; + if(!g_Sessions[sessIdx].inBreakout) return false; + if(!IsDirectionAllowed(g_Sessions[sessIdx].breakoutDir)) return false; + if(HasActiveExposureForSession(sessIdx)) return false; + if(g_Sessions[sessIdx].retestFiredThisBar) return false; + if(InpBreakoutExpireBars > 0 && g_Sessions[sessIdx].breakoutBarsAgo >= InpBreakoutExpireBars) return false; + int ladderCap = GetEffectiveLadderCap(sessIdx); + if(ladderCap > 0 && g_Sessions[sessIdx].ladderTradesThisSession >= ladderCap) + return false; + + double atrPips = 0.0; + if(IsLadderVolatilityTooHigh(atrPips)) + { + PrintFormat("NANDR EA: Ladder skipped [%s] - ATR %.1f pips exceeds ceiling %.1f pips.", + g_Sessions[sessIdx].name, atrPips, InpLadderATRMaxPips); + return false; + } + + double orbHigh = g_Sessions[sessIdx].orbHigh; + double orbLow = g_Sessions[sessIdx].orbLow; + double orbMid = SessionMid(g_Sessions[sessIdx]); + + if(g_Sessions[sessIdx].breakoutDir > 0) + { + if(!g_Sessions[sessIdx].ladderBullLowDone + && c1 > orbLow && l_cur <= orbLow && ask >= orbLow - tol) + { + if(OpenLadderTrade(sessIdx, 1, orbLow, orbMid, "LOW_TO_MID")) + { + g_Sessions[sessIdx].ladderBullLowDone = true; + g_Sessions[sessIdx].retestFiredThisBar = true; + return true; + } + } + + if(g_Sessions[sessIdx].ladderBullLowDone + && !g_Sessions[sessIdx].ladderBullMidDone + && c1 > orbMid && l_cur <= orbMid && ask >= orbMid - tol) + { + if(OpenLadderTrade(sessIdx, 1, orbMid, orbHigh, "MID_TO_HIGH")) + { + g_Sessions[sessIdx].ladderBullMidDone = true; + g_Sessions[sessIdx].retestFiredThisBar = true; + return true; + } + } + } + else if(g_Sessions[sessIdx].breakoutDir < 0) + { + if(!g_Sessions[sessIdx].ladderBearHighDone + && c1 < orbHigh && h_cur >= orbHigh && bid <= orbHigh + tol) + { + if(OpenLadderTrade(sessIdx, -1, orbHigh, orbMid, "HIGH_TO_MID")) + { + g_Sessions[sessIdx].ladderBearHighDone = true; + g_Sessions[sessIdx].retestFiredThisBar = true; + return true; + } + } + + if(g_Sessions[sessIdx].ladderBearHighDone + && !g_Sessions[sessIdx].ladderBearMidDone + && c1 < orbMid && h_cur >= orbMid && bid <= orbMid + tol) + { + if(OpenLadderTrade(sessIdx, -1, orbMid, orbLow, "MID_TO_LOW")) + { + g_Sessions[sessIdx].ladderBearMidDone = true; + g_Sessions[sessIdx].retestFiredThisBar = true; + return true; + } + } + } + + return false; +} + +//+------------------------------------------------------------------+ +//| Cancel all pending orders | +//+------------------------------------------------------------------+ +void CancelAllPending() +{ + for(int i = OrdersTotal() - 1; i >= 0; i--) + { + if(g_Order.SelectByIndex(i)) + if(g_Order.Magic() == InpMagicNumber && g_Order.Symbol() == g_Symbol) + g_Trade.OrderDelete(g_Order.Ticket()); + } +} + +//+------------------------------------------------------------------+ +//| Open trade | +//+------------------------------------------------------------------+ +void OpenTrade(int dir, int sessIdx, double orbLevel, double obTop, double obBottom) +{ + if(g_TradingHalted) return; + if(!IsDirectionAllowed(dir)) return; + if(g_TodayTrades >= InpMaxTradesPerDay) return; + if(g_Sessions[sessIdx].tradesThisSession >= InpMaxRetestsPerSession) return; + if(HasActiveExposureForSession(sessIdx)) return; + if(g_Sessions[sessIdx].breakoutEntryTaken) return; + + double ask = SymbolInfoDouble(g_Symbol, SYMBOL_ASK); + double bid = SymbolInfoDouble(g_Symbol, SYMBOL_BID); + double entry = (dir > 0) ? ask : bid; + + // For limit orders, entry is at the ORB level + if(InpEntryMode == ENTRY_LIMIT) + entry = orbLevel; + + // Risk: prefer the coupled ORB risk method (SL+TP from the range); fall back + // to the legacy SL/TP inputs when the method is LEGACY or the geometry fails. + double sl, tp; + if(!CalcOrbRisk(dir, entry, g_Sessions[sessIdx].orbHigh, g_Sessions[sessIdx].orbLow, sl, tp)) + { + sl = CalcSL(dir, entry, obBottom, obTop, + g_Sessions[sessIdx].orbHigh, g_Sessions[sessIdx].orbLow); + tp = CalcTP(dir, entry, sl); + } + double slPips = PriceToPips(MathAbs(entry - sl)); + double lots = CalcLotSize(slPips); + + string comment = StringFormat("NANDR|%s|%s", g_Sessions[sessIdx].name, (dir > 0 ? "BUY" : "SELL")); + + bool result = false; + if(InpEntryMode == ENTRY_MARKET) + { + if(dir > 0) + result = g_Trade.Buy(lots, g_Symbol, 0, sl, tp, comment); + else + result = g_Trade.Sell(lots, g_Symbol, 0, sl, tp, comment); + } + else // Limit + { + ENUM_ORDER_TYPE otype = (dir > 0) ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_SELL_LIMIT; + result = g_Trade.OrderOpen(g_Symbol, otype, lots, orbLevel, orbLevel, sl, tp, ORDER_TIME_DAY, 0, comment); + } + + if(result) + { + g_TodayTrades++; + g_Sessions[sessIdx].tradesThisSession++; + g_Sessions[sessIdx].breakoutEntryTaken = true; + // Do NOT clear inBreakout — the ORB level remains valid for subsequent retests + // until the breakout expires, fails, or the daily reset. This allows the EA to + // take multiple retest entries on the same level (e.g. 18:45 AND 20:45 retests). + // Re-entry is naturally gated by session-scoped exposure checks at the top of OpenTrade. + PrintFormat("NANDR EA: Trade opened. Dir=%s Lots=%.2f Entry=%.2f SL=%.2f TP=%.2f Session=%s", + (dir > 0 ? "BUY" : "SELL"), lots, entry, sl, tp, g_Sessions[sessIdx].name); + if(InpShowTradeLabels) + DrawTradeLabel(dir, entry, sl, tp); + } + else + { + PrintFormat("NANDR EA: OrderSend failed. Error=%d Session=%s", + GetLastError(), g_Sessions[sessIdx].name); + } +} + +//+------------------------------------------------------------------+ +//| Tick-based retest entries | +//| Core ORB strategy: | +//| 1. Session ORB complete | +//| 2. Breakout confirmed (close crossed ORB level) | +//| 3. Wick of current bar touches the broken level | +//| 4. Enter in breakout direction | +//+------------------------------------------------------------------+ +void CheckRetestEntriesTick() +{ + if(g_TradingHalted) return; + if(g_TodayTrades >= InpMaxTradesPerDay) return; + + double h_cur = iHigh(g_Symbol, PERIOD_CURRENT, 0); + double l_cur = iLow(g_Symbol, PERIOD_CURRENT, 0); + double c1 = iClose(g_Symbol, PERIOD_CURRENT, 1); + double open0 = iOpen(g_Symbol, PERIOD_CURRENT, 0); + double ask = SymbolInfoDouble(g_Symbol, SYMBOL_ASK); + double bid = SymbolInfoDouble(g_Symbol, SYMBOL_BID); + double tol = PipsToPrice(1.0); + string obSessionName = ResolveSessionNameForUtcTime(TimeCurrent() - g_ServerUtcOffsetSec); + + // ── ORB Session Retest Entries ────────────────────────────────── + if(InpWaitForRetest) + { + for(int s = 0; s < g_SessionCount; s++) + { + // --- Gate 1: session must have a live confirmed breakout --- + if(!g_Sessions[s].enabled || !g_Sessions[s].orbComplete) continue; + if(!g_Sessions[s].inBreakout) continue; + + // --- Gate 2: no active exposure for this session --- + if(HasActiveExposureForSession(s)) continue; + + // --- Gate 3: entry already taken and not yet released --- + if(g_Sessions[s].breakoutEntryTaken) continue; + + // --- Gate 4: session trade count limit --- + if(g_Sessions[s].tradesThisSession >= InpMaxRetestsPerSession) continue; + + // --- Gate 5: breakout expiry --- + if(InpBreakoutExpireBars > 0 && g_Sessions[s].breakoutBarsAgo >= InpBreakoutExpireBars) continue; + + // --- Gate 6: one entry per bar to prevent duplicate ticks --- + if(g_Sessions[s].retestFiredThisBar) continue; + + double orbHigh = g_Sessions[s].orbHigh; + double orbLow = g_Sessions[s].orbLow; + int dir = 0; + double orbLevel = 0; + + if(g_Sessions[s].breakoutDir == 1) // Bullish breakout: price broke above orbHigh + { + // Retest: wick comes back down to touch orbHigh + if(l_cur <= orbHigh) + { + orbLevel = orbHigh; + dir = 1; + } + } + else if(g_Sessions[s].breakoutDir == -1) // Bearish breakout: price broke below orbLow + { + // Retest: wick comes back up to touch orbLow + if(h_cur >= orbLow) + { + orbLevel = orbLow; + dir = -1; + } + } + + if(dir == 0) continue; + + g_Sessions[s].retestFiredThisBar = true; + + double obTop = 0, obBottom = 0; + IsOBNearLevel(orbLevel, dir, obTop, obBottom); + if(InpOBRequireConf && obTop == 0) + { + PrintFormat("NANDR EA: [%s] Retest at %.2f blocked — OB confirmation required but no OB nearby.", + g_Sessions[s].name, orbLevel); + continue; + } + OpenTrade(dir, s, orbLevel, obTop, obBottom); + } + } + + // ── Session Level Ladder (fallback, only when no ORB entry taken this tick) ── + if(InpUseSessionLevelLadder) + { + for(int s = 0; s < g_SessionCount; s++) + { + if(TrySessionLadderEntry(s, c1, h_cur, l_cur, ask, bid, tol)) + break; + } + } + + if(!InpUseOBRetestEntry) return; + + // ── Bearish OB Retest → SELL at wick touch ─────────────────────── + // Gated by the daily bias (only short entries when bias permits). + if(IsDirectionAllowed(-1)) + for(int i = 0; i < g_BearOBCount; i++) + { + if(!g_BearOBs[i].active || g_BearOBs[i].traded) continue; + if(g_TodayTrades >= InpMaxTradesPerDay) break; + + if(obSessionName == "") continue; + if(HasActiveExposureForSession(obSessionName)) continue; + + double obBottom = g_BearOBs[i].bottom; + double obTop2 = g_BearOBs[i].top; + + bool prevBelow = (c1 <= obBottom || open0 <= obBottom); + bool wickTouched = (h_cur >= obBottom); + if(!prevBelow || !wickTouched) continue; + + double entry = bid; + double sl = NormalizeDouble(obTop2 + PipsToPrice(2.0), + (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS)); + double slPips = PriceToPips(MathAbs(entry - sl)); + double tp = CalcTP(-1, entry, sl); + double lots = CalcLotSize(slPips); + + bool result = g_Trade.Sell(lots, g_Symbol, 0, sl, tp, + StringFormat("NANDR|%s|OBRetest|SELL|%.2f", obSessionName, obBottom)); + if(result) + { + g_TodayTrades++; + g_BearOBs[i].traded = true; + PrintFormat("NANDR EA: OB Retest SELL. Entry=%.2f SL=%.2f TP=%.2f OB=[%.2f-%.2f]", + entry, sl, tp, obBottom, obTop2); + if(InpShowTradeLabels) DrawTradeLabel(-1, entry, sl, tp); + } + else + PrintFormat("NANDR EA: OB Retest SELL failed. Error=%d", GetLastError()); + break; + } + + // ── Bullish OB Retest → BUY at wick touch ──────────────────────── + // Gated by the daily bias (only long entries when bias permits). + if(IsDirectionAllowed(1)) + for(int i = 0; i < g_BullOBCount; i++) + { + if(!g_BullOBs[i].active || g_BullOBs[i].traded) continue; + if(g_TodayTrades >= InpMaxTradesPerDay) break; + + if(obSessionName == "") continue; + if(HasActiveExposureForSession(obSessionName)) continue; + + double obTop3 = g_BullOBs[i].top; + double obBottom2 = g_BullOBs[i].bottom; + + bool prevAbove = (c1 >= obTop3 || open0 >= obTop3); + bool wickTouched = (l_cur <= obTop3); + if(!prevAbove || !wickTouched) continue; + + double entry = ask; + double sl = NormalizeDouble(obBottom2 - PipsToPrice(2.0), + (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS)); + double slPips = PriceToPips(MathAbs(entry - sl)); + double tp = CalcTP(1, entry, sl); + double lots = CalcLotSize(slPips); + + bool result = g_Trade.Buy(lots, g_Symbol, 0, sl, tp, + StringFormat("NANDR|%s|OBRetest|BUY|%.2f", obSessionName, obTop3)); + if(result) + { + g_TodayTrades++; + g_BullOBs[i].traded = true; + PrintFormat("NANDR EA: OB Retest BUY. Entry=%.2f SL=%.2f TP=%.2f OB=[%.2f-%.2f]", + entry, sl, tp, obBottom2, obTop3); + if(InpShowTradeLabels) DrawTradeLabel(1, entry, sl, tp); + } + else + PrintFormat("NANDR EA: OB Retest BUY failed. Error=%d", GetLastError()); + break; + } +} + +//+------------------------------------------------------------------+ +//| Check all sessions for entry signals | +//+------------------------------------------------------------------+ +void CheckEntrySignals() +{ + for(int s = 0; s < g_SessionCount; s++) + { + if(!g_Sessions[s].enabled || !g_Sessions[s].orbComplete) continue; + // Guard: allow up to InpMaxRetestsPerSession retest entries on the same breakout level. + // Previously used InpMaxPosPerSession (=1) which permanently blocked the second retest. + if(g_Sessions[s].tradesThisSession >= InpMaxRetestsPerSession) continue; + if(!g_Sessions[s].inBreakout) continue; + + // --- Breakout expiry --- + if(InpBreakoutExpireBars > 0 + && g_Sessions[s].breakoutBarsAgo >= InpBreakoutExpireBars) + { + PrintFormat("NANDR EA: [%s] Breakout expired after %d bars. Resetting.", + g_Sessions[s].name, g_Sessions[s].breakoutBarsAgo); + g_Sessions[s].inBreakout = false; + g_Sessions[s].breakoutDir = 0; + g_Sessions[s].breakoutEntryTaken = false; + ResetLadderProgress(s); + continue; + } + + int dir = g_Sessions[s].breakoutDir; + double orbLevel = (dir > 0) ? g_Sessions[s].orbHigh : g_Sessions[s].orbLow; + + // --- LIMIT mode: place order immediately on breakout bar --- + if(InpEntryMode == ENTRY_LIMIT) + { + // Place once — on the bar breakout is first detected (age 0 or 1 due to update order) + if(g_Sessions[s].breakoutBarsAgo <= 1) + { + double obTop = 0, obBottom = 0; + IsOBNearLevel(orbLevel, dir, obTop, obBottom); + if(InpOBRequireConf && obTop == 0) continue; + OpenTrade(dir, s, orbLevel, obTop, obBottom); + } + continue; + } + + // --- MARKET mode --- + if(!InpWaitForRetest) + { + // Direct entry on the bar the breakout is confirmed (age 0 or 1) + if(g_Sessions[s].breakoutBarsAgo <= 1) + { + double obTop = 0, obBottom = 0; + IsOBNearLevel(orbLevel, dir, obTop, obBottom); + if(InpOBRequireConf && obTop == 0) continue; + OpenTrade(dir, s, orbLevel, obTop, obBottom); + } + } + else + { + // Retest entries fire tick-based in CheckRetestEntriesTick(). + // Here we only update state: sweep reversals and failed retest cancellation. + UpdateRetestState(s); + } + } +} + +//+------------------------------------------------------------------+ +//| Manage open positions: breakeven + trailing stop | +//+------------------------------------------------------------------+ +void ManageOpenTrades() +{ + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + if(!g_Position.SelectByIndex(i)) continue; + if(g_Position.Magic() != InpMagicNumber) continue; + if(g_Position.Symbol() != g_Symbol) continue; + + double entry = g_Position.PriceOpen(); + double sl = g_Position.StopLoss(); + double currentSL= sl; + double price = g_Position.PriceCurrent(); + ulong ticket = g_Position.Ticket(); + int posDir = (g_Position.PositionType() == POSITION_TYPE_BUY) ? 1 : -1; + + // Breakeven + Partial Close + if(InpBreakevenTrigPips > 0) + { + double trigDist = PipsToPrice(InpBreakevenTrigPips); + double beDist = PipsToPrice(InpBreakevenOffPips); + double beLevel = (posDir > 0) ? entry + beDist : entry - beDist; + bool triggered = (posDir > 0) + ? (price >= entry + trigDist && sl < beLevel) + : (price <= entry - trigDist && (sl > beLevel || sl == 0)); + + if(triggered) + { + // Step 1: partial close — fire once per position + if(InpPartialCloseRatio > 0 && !IsPartialClosed(ticket)) + { + double fullLots = g_Position.Volume(); + double closeLots = NormalizeLot(fullLots * InpPartialCloseRatio); + double minLot = SymbolInfoDouble(g_Symbol, SYMBOL_VOLUME_MIN); + double stepLot = SymbolInfoDouble(g_Symbol, SYMBOL_VOLUME_STEP); + // Ensure we leave at least minLot remaining so the position stays open + double remaining = NormalizeLot(fullLots - closeLots); + if(closeLots >= minLot && remaining >= minLot) + { + PrintFormat("NANDR EA: Partial close trigger. Ticket=%llu Full=%.2f Ratio=%.2f Close=%.2f Remaining=%.2f Step=%.2f Min=%.2f", + ticket, fullLots, InpPartialCloseRatio, closeLots, remaining, stepLot, minLot); + ResetLastError(); + bool partResult = g_Trade.PositionClosePartial(ticket, closeLots); + if(partResult) + { + // Verify the same position volume actually decreased. + if(PositionSelectByTicket(ticket)) + { + double afterLots = PositionGetDouble(POSITION_VOLUME); + if(afterLots < fullLots) + { + MarkPartialClosed(ticket); + PrintFormat("NANDR EA: Partial close %.2f lots at %.2f (%.0f pips profit). Ticket=%llu Remaining=%.2f", + closeLots, price, InpBreakevenTrigPips, ticket, afterLots); + } + else + { + PrintFormat("NANDR EA: Partial close returned true but volume did not reduce. Ticket=%llu Before=%.2f After=%.2f", + ticket, fullLots, afterLots); + } + } + else + { + // If ticket no longer exists it may have been fully closed; do not mark partial as successful. + PrintFormat("NANDR EA: Partial close check could not reselect ticket %llu after close call.", ticket); + } + } + else + PrintFormat("NANDR EA: Partial close failed. Error=%d Ticket=%llu", + GetLastError(), ticket); + } + } + // Step 2: move SL to breakeven + currentSL = NormalizeDouble(beLevel, (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS)); + } + } + + // Trailing stop + if(InpTrailingStopPips > 0) + { + double trailDist = PipsToPrice(InpTrailingStopPips); + if(posDir > 0) + { + double newSL = price - trailDist; + if(newSL > currentSL) currentSL = NormalizeDouble(newSL, (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS)); + } + else + { + double newSL = price + trailDist; + if(currentSL == 0 || newSL < currentSL) + currentSL = NormalizeDouble(newSL, (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS)); + } + } + + if(currentSL != sl && currentSL > 0) + g_Trade.PositionModify(ticket, currentSL, g_Position.TakeProfit()); + } +} + +//+------------------------------------------------------------------+ +//| Daily reset logic | +//+------------------------------------------------------------------+ +void CheckDailyReset() +{ + datetime now = TimeCurrent(); + // The trading day is anchored to the 22:00 UTC Daily Open session. Shifting + // UTC time forward by 2h moves that anchor onto a UTC-midnight boundary, so + // comparing calendar days resets exactly at 22:00 UTC regardless of the + // broker server timezone. This prevents a server-midnight reset from + // truncating the Daily Open session only a couple of hours after it starts. + datetime tradingNow = (now - g_ServerUtcOffsetSec) + 2 * 3600; + datetime tradingLast = (g_LastDayReset - g_ServerUtcOffsetSec) + 2 * 3600; + MqlDateTime dt, dtLast; + TimeToStruct(tradingNow, dt); + TimeToStruct(tradingLast, dtLast); + + if(dt.day != dtLast.day || dt.mon != dtLast.mon || dt.year != dtLast.year) + { + g_LastDayReset = now; + g_TodayTrades = 0; + g_TodayWins = 0; + g_TodayLosses = 0; + g_TodayPnL = 0; + g_DayStartEquity = AccountInfoDouble(ACCOUNT_EQUITY); + g_TradingHalted = false; + g_GlobalBiasDir = 0; // clear day bias — first session breakout will re-establish it + g_PartialClosedCount = 0; // clear partial close tracker for the new day + + // Reset all session ORB data for the new day + for(int s = 0; s < g_SessionCount; s++) + g_Sessions[s].Reset(); + + // Reset OB traded flags so the same zones can re-trigger on the new day + for(int i = 0; i < g_BullOBCount; i++) g_BullOBs[i].traded = false; + for(int i = 0; i < g_BearOBCount; i++) g_BearOBs[i].traded = false; + + Print("NANDR EA: Daily reset. Equity=", g_DayStartEquity); + } +} + +//+------------------------------------------------------------------+ +//| Check risk limits | +//+------------------------------------------------------------------+ +void CheckRiskLimits() +{ + if(g_TradingHalted) return; + + double equity = AccountInfoDouble(ACCOUNT_EQUITY); + bool halt = false; + + if(InpMaxDailyLossUSD > 0 && (g_DayStartEquity - equity) >= InpMaxDailyLossUSD) + { + PrintFormat("NANDR EA: Max daily loss USD reached (%.2f). Trading halted.", InpMaxDailyLossUSD); + halt = true; + } + if(InpMaxDailyLossPct > 0 && g_DayStartEquity > 0) + { + double lossPct = (g_DayStartEquity - equity) / g_DayStartEquity * 100.0; + if(lossPct >= InpMaxDailyLossPct) + { + PrintFormat("NANDR EA: Max daily loss %% reached (%.2f%%). Trading halted.", InpMaxDailyLossPct); + halt = true; + } + } + + if(halt) + { + g_TradingHalted = true; + CancelAllPending(); + } +} + +//+------------------------------------------------------------------+ +//| Update PnL stats from closed trades | +//+------------------------------------------------------------------+ +void UpdateClosedTrades() +{ + // Scan deal history for today + datetime dayStart = iTime(g_Symbol, PERIOD_D1, 0); + HistorySelect(dayStart, TimeCurrent()); + + int deals = HistoryDealsTotal(); + double newPnL = 0; + int newWins = 0, newLosses = 0; + + for(int i = 0; i < deals; i++) + { + ulong ticket = HistoryDealGetTicket(i); + if(HistoryDealGetInteger(ticket, DEAL_MAGIC) != InpMagicNumber) continue; + if(HistoryDealGetString(ticket, DEAL_SYMBOL) != g_Symbol) continue; + if(HistoryDealGetInteger(ticket, DEAL_ENTRY) != DEAL_ENTRY_OUT) continue; + + double profit = HistoryDealGetDouble(ticket, DEAL_PROFIT) + + HistoryDealGetDouble(ticket, DEAL_SWAP) + + HistoryDealGetDouble(ticket, DEAL_COMMISSION); + newPnL += profit; + if(profit >= 0) newWins++; + else newLosses++; + } + + g_TodayPnL = newPnL; + g_TodayWins = newWins; + g_TodayLosses = newLosses; +} + +//+------------------------------------------------------------------+ +//| Draw ORB lines for a session | +//+------------------------------------------------------------------+ +void DrawORBLines(int sessIdx) +{ + if(sessIdx < 0 || sessIdx >= g_SessionCount) return; + + color lineColor = clrWhite; + if(g_Sessions[sessIdx].name == "Tokyo") lineColor = clrDodgerBlue; + else if(g_Sessions[sessIdx].name == "London") lineColor = clrTomato; + else if(g_Sessions[sessIdx].name == "NY" + || g_Sessions[sessIdx].name == "NYORB") lineColor = clrGold; + else if(g_Sessions[sessIdx].name == "DailyOpen") lineColor = clrSilver; + + string prefix = "NANDR_ORB_" + g_Sessions[sessIdx].name + "_"; + + // High line + string hName = prefix + "High"; + ObjectDelete(0, hName); + if(g_Sessions[sessIdx].orbHigh > 0) + { + ObjectCreate(0, hName, OBJ_HLINE, 0, 0, g_Sessions[sessIdx].orbHigh); + ObjectSetInteger(0, hName, OBJPROP_COLOR, lineColor); + ObjectSetInteger(0, hName, OBJPROP_STYLE, STYLE_SOLID); + ObjectSetInteger(0, hName, OBJPROP_WIDTH, 1); + ObjectSetString(0, hName, OBJPROP_TOOLTIP, + g_Sessions[sessIdx].name + " ORB High: " + + DoubleToString(g_Sessions[sessIdx].orbHigh, 2)); + } + + // Low line + string lName = prefix + "Low"; + ObjectDelete(0, lName); + if(g_Sessions[sessIdx].orbLow < DBL_MAX && g_Sessions[sessIdx].orbLow > 0) + { + ObjectCreate(0, lName, OBJ_HLINE, 0, 0, g_Sessions[sessIdx].orbLow); + ObjectSetInteger(0, lName, OBJPROP_COLOR, lineColor); + ObjectSetInteger(0, lName, OBJPROP_STYLE, STYLE_SOLID); + ObjectSetInteger(0, lName, OBJPROP_WIDTH, 1); + ObjectSetString(0, lName, OBJPROP_TOOLTIP, + g_Sessions[sessIdx].name + " ORB Low: " + + DoubleToString(g_Sessions[sessIdx].orbLow, 2)); + } + + // Mid line (dotted) + if(g_Sessions[sessIdx].orbHigh > 0 + && g_Sessions[sessIdx].orbLow < DBL_MAX && g_Sessions[sessIdx].orbLow > 0) + { + string mName = prefix + "Mid"; + ObjectDelete(0, mName); + double mid = (g_Sessions[sessIdx].orbHigh + g_Sessions[sessIdx].orbLow) / 2.0; + ObjectCreate(0, mName, OBJ_HLINE, 0, 0, mid); + ObjectSetInteger(0, mName, OBJPROP_COLOR, lineColor); + ObjectSetInteger(0, mName, OBJPROP_STYLE, STYLE_DOT); + ObjectSetInteger(0, mName, OBJPROP_WIDTH, 1); + } + + ChartRedraw(0); +} + +//+------------------------------------------------------------------+ +//| Draw OB zone rectangle | +//+------------------------------------------------------------------+ +void DrawOBZone(int idx, bool isBull) +{ + SOrderBlock ob; + if(isBull) ob = g_BullOBs[idx]; + else ob = g_BearOBs[idx]; + if(!ob.active) return; + + color bgColor = isBull ? (color)ColorToARGB(clrGreen, 40) : (color)ColorToARGB(clrRed, 40); + color borderColor = isBull ? clrGreen : clrRed; + + datetime t1 = ob.time; + datetime t2 = ob.time + PeriodSeconds(PERIOD_CURRENT) * 200; // extend right + + ObjectDelete(0, ob.objName); + ObjectCreate(0, ob.objName, OBJ_RECTANGLE, 0, t1, ob.top, t2, ob.bottom); + ObjectSetInteger(0, ob.objName, OBJPROP_COLOR, borderColor); + ObjectSetInteger(0, ob.objName, OBJPROP_BGCOLOR, isBull ? clrGreen : clrRed); + ObjectSetInteger(0, ob.objName, OBJPROP_FILL, true); + ObjectSetInteger(0, ob.objName, OBJPROP_BACK, true); + ObjectSetInteger(0, ob.objName, OBJPROP_STYLE, STYLE_SOLID); + ObjectSetInteger(0, ob.objName, OBJPROP_WIDTH, 1); + ObjectSetString(0, ob.objName, OBJPROP_TOOLTIP, + (isBull ? "Bull OB" : "Bear OB") + " [" + + DoubleToString(ob.bottom, 2) + " - " + DoubleToString(ob.top, 2) + "]"); + + // Mid line + string midName = ob.objName + "_mid"; + ObjectDelete(0, midName); + ObjectCreate(0, midName, OBJ_TREND, 0, t1, ob.mid, t2, ob.mid); + ObjectSetInteger(0, midName, OBJPROP_COLOR, isBull ? clrLimeGreen : clrOrangeRed); + ObjectSetInteger(0, midName, OBJPROP_STYLE, STYLE_DASH); + ObjectSetInteger(0, midName, OBJPROP_RAY_RIGHT, true); + + ChartRedraw(0); +} + +//+------------------------------------------------------------------+ +//| Remove OB zone objects | +//+------------------------------------------------------------------+ +void RemoveOBZone(const string name) +{ + ObjectDelete(0, name); + ObjectDelete(0, name + "_mid"); + ChartRedraw(0); +} + +//+------------------------------------------------------------------+ +//| Draw trade entry label | +//+------------------------------------------------------------------+ +void DrawTradeLabel(int dir, double entry, double sl, double tp) +{ + string name = "NANDR_Trade_" + IntegerToString(TimeCurrent()); + datetime t = iTime(g_Symbol, PERIOD_CURRENT, 0); + + ObjectCreate(0, name, OBJ_ARROW, 0, t, entry); + ObjectSetInteger(0, name, OBJPROP_ARROWCODE, (dir > 0) ? 233 : 234); + ObjectSetInteger(0, name, OBJPROP_COLOR, (dir > 0) ? clrLime : clrRed); + ObjectSetInteger(0, name, OBJPROP_WIDTH, 2); + + // SL line + string slName = name + "_SL"; + ObjectCreate(0, slName, OBJ_HLINE, 0, t, sl); + ObjectSetInteger(0, slName, OBJPROP_COLOR, clrRed); + ObjectSetInteger(0, slName, OBJPROP_STYLE, STYLE_DASH); + + // TP line + string tpName = name + "_TP"; + ObjectCreate(0, tpName, OBJ_HLINE, 0, t, tp); + ObjectSetInteger(0, tpName, OBJPROP_COLOR, clrLime); + ObjectSetInteger(0, tpName, OBJPROP_STYLE, STYLE_DASH); + + ChartRedraw(0); +} + +// Dashboard helper — module-level state set by DrawDashboard before each call +string g_DBPrefix = "NANDR_DB_"; +int g_DBX = 15; +int g_DBY = 30; +int g_DBDY = 18; +int g_DBLine = 0; + +void DBLine(string txt, color col, int fontSize = 9) +{ + string name = g_DBPrefix + IntegerToString(g_DBLine); + ObjectDelete(0, name); + ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0); + ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER); + ObjectSetInteger(0, name, OBJPROP_XDISTANCE, g_DBX); + ObjectSetInteger(0, name, OBJPROP_YDISTANCE, g_DBY + g_DBLine * g_DBDY); + ObjectSetInteger(0, name, OBJPROP_COLOR, col); + ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize); + ObjectSetString(0, name, OBJPROP_FONT, "Consolas"); + ObjectSetString(0, name, OBJPROP_TEXT, txt); + g_DBLine++; +} + +//+------------------------------------------------------------------+ +//| Draw dashboard | +//+------------------------------------------------------------------+ +void DrawDashboard() +{ + if(!InpShowDashboard) return; + + color title = clrGold; + color normal = clrSilver; + color good = clrLimeGreen; + color bad = clrTomato; + + g_DBPrefix = "NANDR_DB_"; + g_DBX = 15; + g_DBY = 30; + g_DBDY = 18; + g_DBLine = 0; + + DBLine("== NANDR ORB+OB EA ==", title, 10); + DBLine("-------------------------", normal); + DBLine(StringFormat("Symbol : %s TF: %s", g_Symbol, EnumToString(Period())), normal); + DBLine(StringFormat("OrbTF : M%d BarsNeeded: %d", (int)InpOrbTimeframe, g_OrbBarsNeeded), normal); + DBLine("-------------------------", normal); + + // Sessions + for(int s = 0; s < g_SessionCount; s++) + { + string status; + if(g_Sessions[s].orbComplete) + status = "ARMED"; + else if(g_Sessions[s].orbBarCount > 0) + status = "ACCUM"; + else + status = "WAIT "; + if(g_Sessions[s].inBreakout) + status = (g_Sessions[s].breakoutDir > 0 ? "BULL+" : "BEAR-"); + color sc = g_Sessions[s].orbComplete ? good : normal; + if(g_TradingHalted) sc = bad; + + DBLine(StringFormat("%-8s H:%.2f L:%.2f [%s]", + g_Sessions[s].name, + g_Sessions[s].orbHigh > 0 ? g_Sessions[s].orbHigh : 0, + g_Sessions[s].orbLow < DBL_MAX ? g_Sessions[s].orbLow : 0, + status), sc); + } + + DBLine("-------------------------", normal); + DBLine(StringFormat("BullOBs: %d BearOBs: %d", g_BullOBCount, g_BearOBCount), normal); + DBLine("-------------------------", normal); + + // Daily stats + double wr = (g_TodayWins + g_TodayLosses > 0) + ? (double)g_TodayWins / (g_TodayWins + g_TodayLosses) * 100.0 : 0; + DBLine(StringFormat("Trades : %d / %d", g_TodayTrades, InpMaxTradesPerDay), normal); + DBLine(StringFormat("W/L : %d / %d WR: %.0f%%", g_TodayWins, g_TodayLosses, wr), + g_TodayWins >= g_TodayLosses ? good : normal); + DBLine(StringFormat("Today PnL: %+.2f USD", g_TodayPnL), + g_TodayPnL >= 0 ? good : bad); + DBLine(StringFormat("Equity : %.2f", AccountInfoDouble(ACCOUNT_EQUITY)), normal); + + if(g_TradingHalted) + DBLine("! TRADING HALTED - Daily limit", bad, 10); + else if(g_SessionCount == 0) + DBLine("! No sessions enabled", bad); + + ChartRedraw(0); +} + +//+------------------------------------------------------------------+ +//| Remove all EA objects from chart | +//+------------------------------------------------------------------+ +void RemoveAllObjects() +{ + int total = ObjectsTotal(0, 0, -1); + for(int i = total - 1; i >= 0; i--) + { + string name = ObjectName(0, i, 0, -1); + if(StringFind(name, "NANDR_") >= 0) + ObjectDelete(0, name); + } + ChartRedraw(0); +} + +//+------------------------------------------------------------------+ +//| Reconstruct ORB levels from historical bars on startup | +//| Handles mid-day EA restarts — finds today's session open bars | +//| in price history and restores High/Low so sessions show ARMED. | +//+------------------------------------------------------------------+ +void ReconstructORBFromHistory() +{ + MqlDateTime dtNow; + TimeToStruct(TimeCurrent(), dtNow); + + for(int s = 0; s < g_SessionCount; s++) + { + if(!g_Sessions[s].enabled || g_Sessions[s].orbComplete) continue; + + // Build this session's open datetime for today + // Session start hours are UTC; convert to server time so iBarShift/iTime + // (which operate in server time) locate the correct opening bar. + MqlDateTime dtSess = dtNow; + dtSess.hour = g_Sessions[s].startHour; + dtSess.min = g_Sessions[s].startMin; + dtSess.sec = 0; + datetime sessOpenTime = StructToTime(dtSess) + g_ServerUtcOffsetSec; + + // Only reconstruct if the opening bar has fully closed + long barSecs = (long)g_OrbBarsNeeded * (int)InpOrbTimeframe * 60; + if(TimeCurrent() < sessOpenTime + barSecs) continue; + + // Find the bar index for this session open time + int barIdx = iBarShift(g_Symbol, PERIOD_CURRENT, sessOpenTime, false); + if(barIdx < 1) continue; // bar[0] is the live bar + + // Verify exact time match (session bar must exist in chart history) + if(iTime(g_Symbol, PERIOD_CURRENT, barIdx) != sessOpenTime) continue; + + // Accumulate ORB range across all required bars (M1: 15 bars, M5: 3, M15: 1) + double h = iHigh(g_Symbol, PERIOD_CURRENT, barIdx); + double l = iLow(g_Symbol, PERIOD_CURRENT, barIdx); + for(int b = 1; b < g_OrbBarsNeeded; b++) + { + int idx = barIdx - b; + if(idx < 1) break; + h = MathMax(h, iHigh(g_Symbol, PERIOD_CURRENT, idx)); + l = MathMin(l, iLow(g_Symbol, PERIOD_CURRENT, idx)); + } + + g_Sessions[s].orbHigh = h; + g_Sessions[s].orbLow = l; + g_Sessions[s].orbBarCount = g_OrbBarsNeeded; + g_Sessions[s].orbStartTime = sessOpenTime; + g_Sessions[s].orbComplete = true; + + PrintFormat("NANDR EA: [%s] ORB reconstructed from history. High=%.2f Low=%.2f", + g_Sessions[s].name, h, l); + if(InpShowORBLines) DrawORBLines(s); + } +} + +//+------------------------------------------------------------------+ +//| Sync daily state from deal history on startup/restart | +//| Restores g_DayStartEquity so the daily loss limit persists across| +//| EA restarts, and syncs g_TodayTrades to prevent double-counting. | +//+------------------------------------------------------------------+ +void SyncDailyState() +{ + datetime dayStart = iTime(g_Symbol, PERIOD_D1, 0); + if(dayStart == 0) return; + HistorySelect(dayStart, TimeCurrent()); + + int deals = HistoryDealsTotal(); + double closedPnL = 0; + int tradeOpens = 0; + + for(int i = 0; i < deals; i++) + { + ulong ticket = HistoryDealGetTicket(i); + if(HistoryDealGetInteger(ticket, DEAL_MAGIC) != InpMagicNumber) continue; + if(HistoryDealGetString(ticket, DEAL_SYMBOL) != g_Symbol) continue; + + long entry = HistoryDealGetInteger(ticket, DEAL_ENTRY); + if(entry == DEAL_ENTRY_IN) + tradeOpens++; + if(entry == DEAL_ENTRY_OUT) + closedPnL += HistoryDealGetDouble(ticket, DEAL_PROFIT) + + HistoryDealGetDouble(ticket, DEAL_SWAP) + + HistoryDealGetDouble(ticket, DEAL_COMMISSION); + } + + // Reconstruct day-start equity: current balance minus today's already-realized P&L. + // This ensures the daily loss limit check works correctly after a mid-day restart. + double currentBalance = AccountInfoDouble(ACCOUNT_BALANCE); + g_DayStartEquity = currentBalance - closedPnL; + g_TodayTrades = tradeOpens; + + PrintFormat("NANDR EA: Daily state synced. DayStartEquity=%.2f TodayTrades=%d ClosedPnL=%.2f", + g_DayStartEquity, g_TodayTrades, closedPnL); +} + +//+------------------------------------------------------------------+ +//| OnInit | +//+------------------------------------------------------------------+ +int OnInit() +{ + // Resolve symbol + g_Symbol = ResolveSymbol(InpSymbolName); + + // Validate timeframe + ENUM_TIMEFRAMES chartTF = Period(); + int chartMins = (int)(PeriodSeconds(chartTF) / 60); + if(chartMins != (int)InpOrbTimeframe) + { + PrintFormat("NANDR EA: WARNING — Chart TF is M%d but ORB Timeframe input is M%d. " + "Attach EA to M%d chart for best results.", + chartMins, (int)InpOrbTimeframe, (int)InpOrbTimeframe); + } + + // Bars needed for 15min ORB at current TF + switch(InpOrbTimeframe) + { + case ORB_TF_M1: g_OrbBarsNeeded = 15; break; + case ORB_TF_M5: g_OrbBarsNeeded = 3; break; + case ORB_TF_M15: g_OrbBarsNeeded = 1; break; + default: g_OrbBarsNeeded = 1; break; + } + + // Breakout confirmation bars + if(InpBreakoutConfBars > 0) + g_BreakoutConfBars = InpBreakoutConfBars; + else + { + switch(InpOrbTimeframe) + { + case ORB_TF_M1: g_BreakoutConfBars = 5; break; + case ORB_TF_M5: g_BreakoutConfBars = 3; break; + case ORB_TF_M15: g_BreakoutConfBars = 2; break; + default: g_BreakoutConfBars = 2; break; + } + } + + // OB pivot length + if(InpOBPivotLength > 0) + g_OBPivotLength = InpOBPivotLength; + else + { + switch(InpOrbTimeframe) + { + case ORB_TF_M1: g_OBPivotLength = 10; break; + default: g_OBPivotLength = 5; break; + } + } + + // Pip size: 1 pip = 10 points for all standard decimal counts (2, 3, 5). + // digits==2 (XAUUSD): point=0.01 → pip=0.10 → 100 pips = $10 price distance + // digits==3 (USDJPY): point=0.001 → pip=0.01 + // digits==5 (EURUSD): point=0.00001 → pip=0.0001 + // BUG FIX: digits==2 previously used g_PointSize directly (0.01) making 100 pips = $1.00, + // causing a near-zero SL and massively oversized lot on XAUUSD. + double tickSize = SymbolInfoDouble(g_Symbol, SYMBOL_TRADE_TICK_SIZE); + int digits = (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS); + g_PointSize = SymbolInfoDouble(g_Symbol, SYMBOL_POINT); + g_PipSize = (digits == 2 || digits == 3 || digits == 5) ? g_PointSize * 10 : g_PointSize; + g_ServerUtcOffsetSec = (int)(TimeCurrent() - TimeGMT()); + PrintFormat("NANDR EA: PipSize=%.5f (digits=%d). 100 pips = %.2f price units.", + g_PipSize, digits, PipsToPrice(100)); + PrintFormat("NANDR EA: Server UTC offset = %+d seconds (%+.1f hours)", + g_ServerUtcOffsetSec, g_ServerUtcOffsetSec / 3600.0); + + // Initialize OB arrays + ArrayResize(g_BullOBs, InpOBMaxCount); + ArrayResize(g_BearOBs, InpOBMaxCount); + g_BullOBCount = 0; + g_BearOBCount = 0; + + // ATR indicator + g_ATRHandle = iATR(g_Symbol, PERIOD_CURRENT, InpATRPeriod); + if(g_ATRHandle == INVALID_HANDLE) + Print("NANDR EA: WARNING — could not create ATR indicator handle."); + + // Trade object + g_Trade.SetExpertMagicNumber(InpMagicNumber); + g_Trade.SetDeviationInPoints(20); + g_Trade.SetTypeFilling(ORDER_FILLING_IOC); + + // Init sessions + InitSessions(); + if(g_SessionCount == 0) + Print("NANDR EA: WARNING — No sessions enabled. EA will not trade. Enable at least one session."); + + // Daily init — set conservative defaults; SyncDailyState() will correct them from history + g_DayStartEquity = AccountInfoDouble(ACCOUNT_EQUITY); + g_LastDayReset = TimeCurrent(); + + // Sync trade count and day-start equity from today's deal history + // (handles mid-day restarts so daily loss limits carry over correctly) + SyncDailyState(); + + // Restore ORB levels from historical bars (handles mid-day restarts + // where the session opening bars have already closed before EA loaded) + ReconstructORBFromHistory(); + + PrintFormat("NANDR EA: Initialized. Symbol=%s OrbBarsNeeded=%d BreakoutConf=%d OBPivot=%d Sessions=%d", + g_Symbol, g_OrbBarsNeeded, g_BreakoutConfBars, g_OBPivotLength, g_SessionCount); + + DrawDashboard(); + return INIT_SUCCEEDED; +} + +//+------------------------------------------------------------------+ +//| OnDeinit | +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) +{ + RemoveAllObjects(); + if(g_ATRHandle != INVALID_HANDLE) + IndicatorRelease(g_ATRHandle); + Comment(""); +} + +//+------------------------------------------------------------------+ +//| Returns true if current server time is Saturday or Sunday | +//+------------------------------------------------------------------+ +bool IsWeekend() +{ + MqlDateTime dt; + TimeToStruct(TimeCurrent(), dt); + return (dt.day_of_week == 0 || dt.day_of_week == 6); // 0=Sunday, 6=Saturday +} + +//+------------------------------------------------------------------+ +//| OnTick | +//+------------------------------------------------------------------+ +void OnTick() +{ + // Silence all processing during weekend market closure + if(IsWeekend()) return; + + // New bar detection + datetime barTime = iTime(g_Symbol, PERIOD_CURRENT, 0); + g_IsNewBar = (barTime != g_LastBarTime); + if(g_IsNewBar) + g_LastBarTime = barTime; + + // Reset day/session state before any tick-based entry logic runs. + if(g_IsNewBar) + CheckDailyReset(); + + // Per-tick management + ManageOpenTrades(); + RefreshBreakoutEntryLocks(); + CheckRiskLimits(); + + // Retest entries fire on tick (at wick touch), not on bar close + CheckRetestEntriesTick(); + + if(!g_IsNewBar) return; + + // Always update ORB and OB state regardless of halt status. + // This keeps session levels current for display and ensures breakouts are detected + // as soon as trading resumes (e.g. next day after a daily-loss halt). + UpdateORBSessions(); + ScanOrderBlocks(); + MitigateOrderBlocks(); + DetectBreakouts(); + UpdateClosedTrades(); + DrawDashboard(); + + // Skip trade execution only when halted — state tracking continues above + if(g_TradingHalted) + return; + + // Entry signals (direct breakout / limit orders on new bar) + CheckEntrySignals(); +} + +//+------------------------------------------------------------------+ +//| OnTradeTransaction — handle position close events | +//+------------------------------------------------------------------+ +void OnTradeTransaction(const MqlTradeTransaction &trans, + const MqlTradeRequest &request, + const MqlTradeResult &result) +{ + if(trans.type == TRADE_TRANSACTION_DEAL_ADD) + { + ulong ticket = trans.deal; + if(HistoryDealSelect(ticket)) + { + if(HistoryDealGetInteger(ticket, DEAL_MAGIC) == InpMagicNumber && + HistoryDealGetString(ticket, DEAL_SYMBOL) == g_Symbol && + HistoryDealGetInteger(ticket, DEAL_ENTRY) == DEAL_ENTRY_OUT) + { + double profit = HistoryDealGetDouble(ticket, DEAL_PROFIT) + + HistoryDealGetDouble(ticket, DEAL_SWAP) + + HistoryDealGetDouble(ticket, DEAL_COMMISSION); + g_TotalPnL += profit; + PrintFormat("NANDR EA: Trade closed. Profit=%.2f TotalPnL=%.2f", profit, g_TotalPnL); + } + } + } +} +//+------------------------------------------------------------------+ diff --git a/README.md b/README.md index 537c7b9..fa63817 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,13 @@ mt5_backtest/ │ ├── ini_generator.py # 生成MT5配置文件 │ ├── mt5_auto_runner.py # 自动运行回测 │ ├── batch_executor.py # 批量执行器 -│ ├── result_parser.py # 解析HTM/XML报告 -│ └── report_generator.py # 生成汇总报告 +│ ├── result_parser.py # 解析HTM/XML报告 +│ ├── report_generator.py # 生成汇总报告 +│ ├── symbol_resolver.py # 根品种别名解析(XAUUSD→XAUUSDc) +│ └── optimizer/ # EA参数优化系统 ├── config/ # 配置文件目录 ├── reports/ # 回测报告输出目录 +├── tests/ # 自检脚本(test_*.py / smoke_*.py) ├── logs/ # 日志目录 └── README.md # 本文档 ``` @@ -26,6 +29,7 @@ mt5_backtest/ - 扫描 MT5 的 `MQL5/Experts` 目录获取 EA 列表 - 根据配置的 EA品种、时间周期、参数组合生成 INI 配置文件 - 每个 INI 文件对应一次回测任务 +- 写入 INI 的 `Symbol=` 行通过 `scripts/symbol_resolver.py` 查表替换(见下文"根品种别名映射") ### 2. 自动执行阶段 (mt5_auto_runner.py) - 通过命令行调用 MT5 终端: `terminal64.exe /config:xxx.ini` @@ -79,6 +83,10 @@ eas: backtest_settings: symbols: ["EURUSD", "GBPUSD", "XAUUSD", "USDJPY"] timeframes: ["M1", "M5", "M15", "H1"] + # 可选:根品种别名映射(见下文"根品种别名映射") + # symbol_aliases: + # XAUUSD: XAUUSDc # 逻辑 XAUUSD -> 实际 XAUUSDc + # XAGUSD: XAGUSDm # 逻辑 XAGUSD -> 实际 XAGUSDm date_range: from: "2026.01.01" to: "2026.01.04" @@ -147,6 +155,66 @@ backtest_settings: | 原油 | XBRUSD, XTIUSD | | 数字货币 | BTCUSD, ETHUSD, LTCUSD | +## 根品种别名映射 (`symbol_aliases`) + +MT5 自带品种(如 `XAUUSD`)历史深度和精度有限;用户常在 MT5 里添加自定义品种 +(如 `XAUUSDc` / `XAUUSDm`)作为更长时间范围或优化用途的"替身"。本系统通过 `symbol_aliases` +让你在 yaml 里写一次逻辑品种名、由系统按表替换成实际 MT5 品种。 + +### 写入 `config/ea_configs.yaml` + +```yaml +backtest_settings: + symbols: [XAUUSD, XAGUSD, EURUSD] # 用户的"逻辑品种" + symbol_aliases: + XAUUSD: XAUUSDc # 逻辑 XAUUSD -> 实际 XAUUSDc + XAGUSD: XAGUSDm # 逻辑 XAGUSD -> 实际 XAGUSDm + # EURUSD 不列 -> 始终 EURUSD +``` + +### 工作机制 + +1. `scripts/ini_generator.py` 和 `scripts/optimizer/ea_batch_optimizer.py` 在 + 写 INI 的 `Symbol=` 之前调用 `scripts/symbol_resolver.resolve_symbol()`。 +2. 解析器是**纯查表**——没有任何"基于日期/优化/周期的智能判断"——写啥用啥: + - 别名表里有这个逻辑品种 → 用别名替换 + - 没有 → 原样写入 `Symbol=` +3. 两条调用路径共用同一份解析器(`scripts/symbol_resolver.py`),保证 GUI 回测页 + 和优化器生成的 INI 行为一致。 + +### GUI 优化页 + +GUI 优化页下拉只显示**逻辑品种**(不再同时列出 XAUUSD / XAUUSDc)。旁边有一个 +`→ XAUUSDc` 提示标签,**实时反映 yaml 里 `symbol_aliases` 当前配置下实际会用什么品种**。 +改 yaml 后重启 GUI 即可看到提示变化,无需在 GUI 里反复切换。 + +### 验证 + +无需 MT5 也能验: + +```bash +python tests/test_symbol_resolver.py # 9 条查表断言 +python tests/test_ini_generator.py # 5 条端到端断言 +``` + +需要 MT5 验证完整流水线: + +```bash +python tests/smoke_alias_backtest.py +# 跑两次真实回测:一次无别名 (Symbol=XAUUSD),一次有别名 (Symbol=XAUUSDc) +# 期望:C:\Users\Administrator\Desktop\mt5-backtest\reports 下生成两份 .htm 报告 +# 报告"交易品种"字段分别是 XAUUSD 和 XAUUSDc +``` + +> 注:`smoke_alias_backtest.py` 顶部硬编码了 MT5 路径和 data dir hash,复制到其他电脑后 +> 需要相应修改 `MT5_PATH` / `MT5_DATA_DIR` 常量。 + +### 扩展点(ponytail) + +`scripts/symbol_resolver.py` 是共享函数入口,将来要把"配置源"切换成数据库或要做 +MT5 `SymbolSelect` 校验,**只改这一个文件**即可,所有调用方(ini 生成器、批量优化器、 +GUI 预览)同步生效。 + ## 如何添加新的时间周期 编辑 `config/ea_configs.yaml`: @@ -537,6 +605,7 @@ mt5_backtest/ │ ├── batch_executor.py # 批量执行器 │ ├── result_parser.py # 解析HTM/XML报告 │ ├── report_generator.py # 生成汇总报告 +│ ├── symbol_resolver.py # 根品种别名解析(symbol_aliases 查表) │ └── optimizer/ # EA优化系统 │ ├── ea_config_parser.py # EA配置解析 │ ├── smart_search.py # 搜索策略 @@ -545,6 +614,7 @@ mt5_backtest/ │ └── ea_batch_optimizer.py # 批量优化器 ├── config/ # 配置文件 ├── reports/ # 回测报告目录 +├── tests/ # 自检脚本(test_symbol_resolver / test_ini_generator / smoke_alias_backtest) ├── optimizer/ # 优化系统目录 │ ├── configs/ # EA配置 │ ├── set_files/ # SET文件 @@ -566,6 +636,19 @@ mt5_backtest/ ### Q: 中文显示乱码 HTM 报告采用 UTF-16-LE 编码,result_parser.py 会自动处理,无需手动转换 +### Q: 想用 MT5 自定义品种(如 XAUUSDc)跑同一 EA +在 `config/ea_configs.yaml` 的 `backtest_settings` 下加: +```yaml +symbol_aliases: + XAUUSD: XAUUSDc +``` +yaml 符号表里仍写 `XAUUSD`,系统会自动把 INI 的 `Symbol=` 替换为 `XAUUSDc`。 +详见上文"根品种别名映射"一节。 + +### Q: 改了 `symbol_aliases` 后 GUI 提示没变 +GUI 启动时读取一次 `config/ea_configs.yaml`,改完后重启 GUI 即可看到 +`→ XAUUSDc` 提示标签更新。无需重新生成报告历史。 + ## 许可证 MIT License \ No newline at end of file diff --git a/config/ea_configs.yaml b/config/ea_configs.yaml index 45aec16..28fb55d 100644 --- a/config/ea_configs.yaml +++ b/config/ea_configs.yaml @@ -14,10 +14,46 @@ backtest_settings: shutdown_terminal: true symbols: - EURUSD + - GBPUSD + - USDJPY + - USDCHF + - EURJPY + - EURGBP + - GBPJPY - XAUUSD timeframes: - H1 visual: 0 + + # ---- 根品种别名映射(可选,留空/删除整块 = 关闭此功能,行为与传统一致)---- + # 纯查表,没有任何"自动判断"。逻辑品种写在上面的 symbols 列表里, + # 想让系统实际用哪个 MT5 品种就在这里写死。 + # + # 用法:取消下面想启用的那一行(或多行)的注释即可。 + # 同一时间只能有一条 XAUUSD 映射生效;切换另一种后缀时把上一条注释掉。 + # 注意:GUI 的"保存配置"不会动这块内容(symbol_aliases 在 GUI 里没有编辑控件), + # 改完直接保存即可,存盘不会被覆盖。GUI 重启后提示标签自动更新。 + # symbol_aliases: + # XAUUSD: XAUUSDc # 常用:自定义品种 XAUUSDc(带扩展历史 / 精度) + # XAUUSD: XAUUSDm # 切换:自定义品种 XAUUSDm + # XAUUSD: Gold.v2 # 切换:完全不同的名字(broker 自定义 / ECN 后缀等) + # XAUUSD: XAUUSD # 显式直通:等价于不写这条(写出来仅为自解释) + # XAGUSD: XAGUSDc # 银同理 + # XAGUSD: XAGUSDm + # BTCUSD: BTCUSD.sb # 加密货币自定义后缀 + # # EURUSD 没列 # 永远不需要列:未列出的逻辑品种按原名直传 + # + # 关闭整个功能:把整块(包括 symbol_aliases: 这行)注释掉或删除。 + # 关闭后 GUI "实际品种"提示行自动消失,INI 里的 Symbol= 与逻辑名完全一致。 + symbol_aliases: + EURUSD: EURUSDc + GBPUSD: GBPUSDc + USDJPY: USDJPYc + USDCHF: USDCHFc + EURJPY: EURJPYc + EURGBP: EURGBPc + GBPJPY: GBPJPYc + XAUUSD: XAUUSDc eas: [] execution: kill_between: true diff --git a/config/generated_smoke/no_alias/MACD_MA_XAUUSD_5M_XAUUSD_H1_default.ini b/config/generated_smoke/no_alias/MACD_MA_XAUUSD_5M_XAUUSD_H1_default.ini new file mode 100644 index 0000000000000000000000000000000000000000..de3537dd01742cb8001294a249554602128bc457 GIT binary patch literal 2673 zcmbtW(QeZ)6!mkY{6l_#k(RY+o$^qvj7=g?tsO!_s0yuHBGM#soNYyXJm=VPlhRHg zz_hln?>#>E_}blN5YGp}Osho8%`TYBN=p@775O^d)G($+?t7QPTbWC`tVTgU>`29N4*gLeDA7OO6JR51c`j_oqT}G8yWRRSogh;X;mW(qp2#^ zslLspX<2c;*ZVdvYh6M0OA&}lB;r9xvBEeJwa}svTa2_w#S(pnw!>N>a(wqih|f@* zp?__@hp>jQjKn}(;QJNqzQ_MW*Ww#+DvZ|XGwc%pl~@|sDSEDD7+0_a*uQGHaIG|a z0`!Sc_-wJJ@lg$8ff4tRCYli&@e=4IY$?1`@x$z~fmbZX;z~@!!i*N;6Xy45Gh-XT z8UW`BvBW2r1e!b`z5=4!=hWsF3AO+T8tYU{Vt;BiJ;~Sl+xT0P9XTz;HN2Vq0&(0s z#eORwQ|q5-uJ7$<*8aZX)Ww=X%R)RJVGSBA*IO*kp2v#8hUaJ5*a+)wzh4PWfX5yWxp~BoB4Fw|HZ$=Qm9d+yIFh`3sM@_S?yEu-IW!frc zvE1q@w6U0ryQA9fiSs6F*IVTChV5a6It-g(j29+@&Z)U?PPP7CNh-zr=NgeYr)S$} zJ-v&~zq`x%9=jb8k8E8d9_P>R(ygFtuxAeLT|9ng?s0w++Od{%RB<;VLZpHU(rCCg zcZJ^MV9%htewZZ@`ASMu3*Yn&RP1{HE85K5H@?FEgU9>ygx9$=d6Q>^UniTB>ut?( zcBIoXXJ#DN}+xM>1ZS0i=~1&V{9+O#X2ED02q#6>^;W=PSJ zYX^6@D{GTn?ha>%OFB0}yc`7!tx_$w`(P<+Emd$^7MpC_z?hYV@7)9+Wg+RZo&>{i zIDQviy$i<}PKH;L;n4Sd@2d^{)Av5SR#NFGp5D%qc$$2fK0PgF$tb>%uOqiQPgS~; z*s6|vZ?WHfEAz-Yu6%DURaUN}`+@JpQK z80@zA-spAtQY@v~Wks4tu+4y2XF3{$_~(1KjZ(5$?IVcfd!OVpbl%EnFu}U-{gKrT z!Z4bva+B$YVxCns=Lf|fimK5ybiWaSs6{HCg%oRyQ_%=5O0mO8i%hK0=V*JZm7>7+ zP=xr5#Rd8+^F4+&gk>T|;u_!YVfQ`$C%P8jfm36&L7!uv0I0;$z|PQfEyuWqCBXi5 z%Y|!|;S-=wg~DftHI0vI5lf7?hcwZQ*oxnPUcr{ZI}<<59$R?DVk&OMOeAKMh%cBw zp)HJU1Zx1CSHu#ZTvBNAjQARe>X1{HTO`;5AZV;pF^T=D)%+}9>u=*9Om^gyh&y;Q z`vh^^JHvi!AXDp~Xs#a|X4d|D%c+mGfR?3rIl&sWSgyBNoV|?|gALEmvau1?+kU?{ zd`?+dm0ebm7$5`Y(e-Zj*4`Gu$#shzS?RnZ&aYoJF2YvJ zs`aGKyp>j4HzLQuftOpg%+GVP)wB?wObwOh{%9x=;eIoM_}!>qN5?r@oF!_Sb=}8t zd@R#WF^lC^&!J7lQaqm2_CQ>=S-ajMU$$(IE7W1w3}d`7Idsm4{q^H4iO5${qFVT-Z=qt}`@f_>B6JXgM8 z=iUP5xW)OeP=5_xjc None: @@ -66,6 +68,18 @@ def _mt5_startupinfo(): return si +# MT5 策略测试器回测模式(与 MT5 官方界面"每个报价/1分钟OHLC/仅开盘价/数学计算/基于每个真实报价"一致) +# 值是写入 INI 的 Model= 编号。批量页和优化页共用本列表,切换显示文字时只改这一处。 +MT5_MODELS = [ + '每个报价 (0)', + '1分钟OHLC (1)', + '仅开盘价 (2)', + '数学计算 (3)', + '基于每个真实报价 (4)', +] +MT5_MODEL_MAP = {int(s.split('(')[1].rstrip(')')): s for s in MT5_MODELS} + + def _find_mt5_hwnds(pid, max_seconds=3.0): hwnds = [] deadline = time.time() + max_seconds @@ -401,6 +415,13 @@ class MT5BacktestGUI: cb = ttk.Checkbutton(sym_frame, text=sym, variable=var) cb.grid(row=i//10, column=i%10, sticky='w', padx=2, pady=1) self.symbol_vars[sym] = var + var.trace_add('write', lambda *_: self._update_batch_symbol_hint()) + + self.batch_sym_hint_var = tk.StringVar(value="") + ttk.Label(sym_frame, textvariable=self.batch_sym_hint_var, + foreground='#0066cc', wraplength=900, justify='left' + ).grid(row=99, column=0, columnspan=10, sticky='w', padx=2, pady=(4, 2)) + self._update_batch_symbol_hint() tf_frame = ttk.LabelFrame(main_frame, text="时间周期") tf_frame.pack(fill='x', padx=5, pady=5) @@ -450,21 +471,32 @@ class MT5BacktestGUI: model_frame.pack(side='left', fill='x', padx=5) self.model_var = tk.IntVar(value=self.config["backtest_settings"]["model"]) - ttk.Radiobutton(model_frame, text="Every Tick (0)", variable=self.model_var, value=0).pack(anchor='w', padx=2) - ttk.Radiobutton(model_frame, text="1分钟OHLC (1)", variable=self.model_var, value=1).pack(anchor='w', padx=2) - ttk.Radiobutton(model_frame, text="真实Tick (4)", variable=self.model_var, value=4).pack(anchor='w', padx=2) + for m in MT5_MODELS: + val = int(m.split('(')[1].rstrip(')')) + ttk.Radiobutton(model_frame, text=m, variable=self.model_var, value=val).pack(anchor='w', padx=2) delay_frame = ttk.LabelFrame(date_model_frame, text="执行延迟") delay_frame.pack(side='left', fill='x', padx=5) - self.delay_type = tk.IntVar(value=0) + saved_delay = int((self.config.get("backtest_settings") or {}).get("execution_delay", 0) or 0) + if saved_delay == 0: + delay_type_value = 0 + delay_value_text = "100" + elif saved_delay == -1: + delay_type_value = 1 + delay_value_text = "100" + else: + delay_type_value = 2 + delay_value_text = str(saved_delay) + + self.delay_type = tk.IntVar(value=delay_type_value) ttk.Radiobutton(delay_frame, text="无延迟 (0)", variable=self.delay_type, value=0).pack(anchor='w', padx=2) ttk.Radiobutton(delay_frame, text="随机延迟 (-1)", variable=self.delay_type, value=1).pack(anchor='w', padx=2) delay_row = ttk.Frame(delay_frame) delay_row.pack() ttk.Radiobutton(delay_row, text="固定延迟:", variable=self.delay_type, value=2).pack(side='left', padx=2) self.delay_value = ttk.Entry(delay_row, width=6) - self.delay_value.insert(0, "100") + self.delay_value.insert(0, delay_value_text) self.delay_value.pack(side='left') ttk.Label(delay_row, text="ms").pack(side='left') @@ -645,7 +677,9 @@ class MT5BacktestGUI: "shutdown_terminal": True, "optimization": 0, "forward_mode": forward_mode, - "forward_date": forward_date + "forward_date": forward_date, + # symbol_aliases 在 GUI 里没有编辑控件,直接从内存配置透传(避免保存时把它清掉) + "symbol_aliases": (self.config.get("backtest_settings") or {}).get("symbol_aliases") or {}, }, "mt5_settings": { "terminal_path": self.mt5_path.get(), @@ -1469,18 +1503,21 @@ class MT5BacktestGUI: sym_row = ttk.Frame(test_frame) sym_row.pack(fill='x', padx=5, pady=3) - ttk.Label(sym_row, text="品种:").pack(side='left', padx=5) + ttk.Label(sym_row, text="逻辑品种:").pack(side='left', padx=5) self.opt_symbol = ttk.Combobox(sym_row, width=12, values=['EURUSD', 'GBPUSD', 'USDJPY', 'USDCAD', 'USDCHF', 'AUDUSD', 'NZDUSD', 'EURGBP', 'EURJPY', 'GBPJPY', 'EURCHF', 'AUDJPY', 'EURAUD', 'GBPAUD', 'EURCAD', 'GBPCAD', 'XAUUSD', 'XAGUSD', 'XBRUSD', 'XTIUSD', 'BTCUSD', 'ETHUSD', 'LTCUSD', 'USDCNH', 'USDHKD', 'USDCZK', 'USDDKK', 'USDHUF', 'USDMXN', 'USDPLN', 'USDTHB', 'NOKJPY', 'SEKJPY', 'XRPUSD', 'XLMUSD', 'AUS200', 'CHINA50', 'ES35', 'STOXX50', 'F40', 'HK50', 'IT40', 'JP225', 'UK100', 'US2000', 'US30', 'US500', 'USTEC', 'CA60', 'NETH25', 'SE30', 'SWI20', 'CHINAH', 'NOR25', 'TecDE30']) self.opt_symbol.set('XAUUSD') self.opt_symbol.pack(side='left', padx=5) + self.opt_symbol_resolved = ttk.Label(sym_row, text="→ XAUUSD", foreground='#666666') + self.opt_symbol_resolved.pack(side='left', padx=(10, 2)) + ttk.Label(sym_row, text="周期:").pack(side='left', padx=10) self.opt_period = ttk.Combobox(sym_row, width=8, values=['M1', 'M5', 'M15', 'M30', 'H1', 'H4', 'D1', 'W1']) self.opt_period.set('M1') self.opt_period.pack(side='left', padx=5) ttk.Label(sym_row, text="模式:").pack(side='left', padx=10) - self.opt_model = ttk.Combobox(sym_row, width=18, values=['Every Tick (0)', '1分钟OHLC (1)', '仅开盘价 (2)', '数学计算 (3)', '真实Tick (4)']) + self.opt_model = ttk.Combobox(sym_row, width=20, values=MT5_MODELS) self.opt_model.set('1分钟OHLC (1)') self.opt_model.pack(side='left', padx=5) @@ -1521,6 +1558,9 @@ class MT5BacktestGUI: self.opt_to_date.insert(0, "2026.01.04") self.opt_to_date.pack(side='left', padx=5) + self.opt_symbol.bind('<>', lambda e: self._update_resolved_symbol_hint()) + self.opt_symbol.bind('', lambda e: self._update_resolved_symbol_hint()) + acct_row = ttk.Frame(test_frame) acct_row.pack(fill='x', padx=5, pady=3) ttk.Label(acct_row, text="保证金:").pack(side='left', padx=5) @@ -1574,6 +1614,7 @@ class MT5BacktestGUI: self.opt_log.pack(fill='both', expand=True, padx=5, pady=5) self._refresh_opt_ea_list() + self._update_resolved_symbol_hint() def _browse_opt_dir(self): path = filedialog.askdirectory(initialdir=self.opt_dir.get()) @@ -1736,18 +1777,12 @@ class MT5BacktestGUI: if test_cfg.get('symbol'): self.opt_symbol.set(str(test_cfg.get('symbol'))) + self._update_resolved_symbol_hint() if test_cfg.get('period'): self.opt_period.set(str(test_cfg.get('period'))) if 'model' in test_cfg: model_val = int(test_cfg.get('model', 1)) - model_map = { - 0: 'Every Tick (0)', - 1: '1分钟OHLC (1)', - 2: '仅开盘价 (2)', - 3: '数学计算 (3)', - 4: '真实Tick (4)', - } - self.opt_model.set(model_map.get(model_val, '1分钟OHLC (1)')) + self.opt_model.set(MT5_MODEL_MAP.get(model_val, '1分钟OHLC (1)')) if test_cfg.get('from_date'): self.opt_from_date.delete(0, tk.END) self.opt_from_date.insert(0, str(test_cfg.get('from_date'))) @@ -1861,6 +1896,53 @@ class MT5BacktestGUI: mode_val = self.wf_mode_map.get(self.wf_mode.get(), 2) if enabled else 0 self.wf_custom_date.configure(state='normal' if (enabled and mode_val == 4) else 'disabled') + def _format_resolved_hint(self, symbols): + """Format a hint string showing logical -> resolved symbol mapping. + + Shared by batch backtest tab (multi-select) and optimizer tab (single). + Returns "" when nothing is selected or no aliases are configured. + """ + if not symbols: + return "" + aliases = (self.config.get('backtest_settings') or {}).get('symbol_aliases') or {} + if not aliases: + return "" + parts = [] + for sym in symbols: + if not sym: + continue + resolved = resolve_symbol(sym, {'symbol_aliases': aliases}) if resolve_symbol else sym + if resolved == sym: + parts.append(f"{sym}(直通)") + else: + parts.append(f"{sym}→{resolved}") + return "实际品种:" + ", ".join(parts) if parts else "" + + def _update_batch_symbol_hint(self): + if not hasattr(self, 'batch_sym_hint_var'): + return + checked = [s for s, v in self.symbol_vars.items() if v.get()] + self.batch_sym_hint_var.set(self._format_resolved_hint(checked)) + + def _resolve_test_config(self): + """Build the minimal dict the resolver needs (only symbol_aliases).""" + return { + 'symbol_aliases': (self.config.get('backtest_settings') or {}).get('symbol_aliases') or {}, + } + + def _update_resolved_symbol_hint(self): + if not hasattr(self, 'opt_symbol_resolved') or resolve_symbol is None: + return + try: + sym = self.opt_symbol.get().split()[0] if ' ' in self.opt_symbol.get() else self.opt_symbol.get() + resolved = resolve_symbol(sym, self._resolve_test_config()) + except Exception: + resolved = self.opt_symbol.get() + if resolved == sym: + self.opt_symbol_resolved.config(text=f"→ {resolved}", foreground='#666666') + else: + self.opt_symbol_resolved.config(text=f"→ {resolved} (别名)", foreground='#0066cc') + def _save_ea_config(self, quiet=False): ea_name = self.opt_ea_var.get() if not ea_name: @@ -1895,39 +1977,39 @@ class MT5BacktestGUI: 'step': step_val, 'optimize': poptimize == 'Y' if poptimize else True } - import json - config_path = os.path.join(configs_dir, ea_name + '.json') - config_data = { + import json + config_path = os.path.join(configs_dir, ea_name + '.json') + config_data = { 'ea_name': ea_name, 'ea_path': self.opt_ea_paths.get(ea_name, f"MY-EA\\{ea_name}.ex5"), 'description': 'Edited via GUI', - 'search_strategy': 'auto', - 'parameters': params, - 'test_config': { + 'search_strategy': 'auto', + 'parameters': params, +'test_config': { 'symbol': self.opt_symbol.get().split()[0] if ' ' in self.opt_symbol.get() else self.opt_symbol.get(), 'period': self.opt_period.get(), 'from_date': self.opt_from_date.get(), 'to_date': self.opt_to_date.get(), - 'model': int(self.opt_model.get().split('(')[1].split(')')[0]) if '(' in self.opt_model.get() else 1, - 'execution_delay': self._get_opt_execution_delay(), - 'optimization_mode': self._get_opt_optimization_mode(), - 'optimization_criterion': self._get_opt_optimization_criterion(), - 'deposit': int(self.opt_deposit.get()) if self.opt_deposit.get().isdigit() else 10000, - 'leverage': self.opt_leverage.get(), - 'timeout_min': int(self.opt_timeout_min.get()) if self.opt_timeout_min.get().isdigit() else 60 - }, - 'walk_forward': { - 'enabled': self.wf_enabled.get(), - 'forward_mode': wf_mode_val, - 'forward_date': wf_forward_date if (self.wf_enabled.get() and wf_mode_val == 4) else '' - } + 'model': int(self.opt_model.get().split('(')[1].split(')')[0]) if '(' in self.opt_model.get() else 1, + 'execution_delay': self._get_opt_execution_delay(), + 'optimization_mode': self._get_opt_optimization_mode(), + 'optimization_criterion': self._get_opt_optimization_criterion(), + 'deposit': int(self.opt_deposit.get()) if self.opt_deposit.get().isdigit() else 10000, + 'leverage': self.opt_leverage.get(), + 'timeout_min': int(self.opt_timeout_min.get()) if self.opt_timeout_min.get().isdigit() else 60 + }, + 'walk_forward': { + 'enabled': self.wf_enabled.get(), + 'forward_mode': wf_mode_val, + 'forward_date': wf_forward_date if (self.wf_enabled.get() and wf_mode_val == 4) else '' + } } with open(config_path, 'w', encoding='utf-8') as f: json.dump(config_data, f, indent=4, ensure_ascii=False) - self.opt_log.insert('end', f"配置已保存: {config_path}\n") - if not quiet: - self.opt_log.update() - return True + self.opt_log.insert('end', f"配置已保存: {config_path}\n") + if not quiet: + self.opt_log.update() + return True def _get_opt_execution_delay(self): delay_type = self.opt_delay_type.get() diff --git a/logs/smoke_alias.log b/logs/smoke_alias.log new file mode 100644 index 0000000..f1c6d02 --- /dev/null +++ b/logs/smoke_alias.log @@ -0,0 +1,28 @@ +2026-07-06 20:26:24,598 - INFO - Executing: MACD_MA_XAUUSD_5M_XAUUSD_H1_default.ini +2026-07-06 20:26:24,620 - INFO - MT5 started PID: 18668 +2026-07-06 20:27:22,638 - INFO - MT5 process ended +2026-07-06 20:27:22,638 - INFO - Report copied to: C:\Users\Administrator\Desktop\mt5-backtest\reports\MACD_MA_XAUUSD_5M_XAUUSD_H1_default.htm +2026-07-06 20:27:22,638 - INFO - Success: MACD_MA_XAUUSD_5M_XAUUSD_H1_default.ini (58s) +2026-07-06 20:27:22,644 - INFO - Executing: MACD_MA_XAUUSD_5M_XAUUSDc_H1_default.ini +2026-07-06 20:27:22,670 - INFO - MT5 started PID: 21372 +2026-07-06 20:27:48,676 - INFO - MT5 process ended +2026-07-06 20:27:48,679 - INFO - Report copied to: C:\Users\Administrator\Desktop\mt5-backtest\reports\MACD_MA_XAUUSD_5M_XAUUSDc_H1_default.htm +2026-07-06 20:27:48,679 - INFO - Success: MACD_MA_XAUUSD_5M_XAUUSDc_H1_default.ini (26s) +=== run 1: no alias (should pick XAUUSD) === +[no_alias] INI Symbol line: Symbol=XAUUSD +[no_alias] runner status: completed, report_path: C:\Users\Administrator\Desktop\mt5-backtest\reports\MACD_MA_XAUUSD_5M_XAUUSD_H1_default.htm + +=== run 2: alias XAUUSD -> XAUUSDc (should pick XAUUSDc) === +[with_alias] INI Symbol line: Symbol=XAUUSDc +[with_alias] runner status: completed, report_path: C:\Users\Administrator\Desktop\mt5-backtest\reports\MACD_MA_XAUUSD_5M_XAUUSDc_H1_default.htm + +=== summary === + INI1 (MACD_MA_XAUUSD_5M_XAUUSD_H1_default.ini) -> completed, C:\Users\Administrator\Desktop\mt5-backtest\reports\MACD_MA_XAUUSD_5M_XAUUSD_H1_default.htm + INI2 (MACD_MA_XAUUSD_5M_XAUUSDc_H1_default.ini) -> completed, C:\Users\Administrator\Desktop\mt5-backtest\reports\MACD_MA_XAUUSD_5M_XAUUSDc_H1_default.htm + +Reports in C:\Users\Administrator\Desktop\mt5-backtest\reports: + - MACD_MA_XAUUSD_5M_XAUUSD_H1_default.htm (52680 bytes) symbol=XAUUSD + - MACD_MA_XAUUSD_5M_XAUUSDc_H1_default.htm (28932 bytes) symbol=XAUUSDc + +Distinct symbols found: ['XAUUSD', 'XAUUSDc'] +SUCCESS: both XAUUSD and XAUUSDc reports present in target dir. diff --git a/scripts/ini_generator.py b/scripts/ini_generator.py index 0fbe87f..e81b766 100644 --- a/scripts/ini_generator.py +++ b/scripts/ini_generator.py @@ -7,6 +7,7 @@ from typing import Dict, List, Any, Optional sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from mt5_paths import resolve_mt5_settings +from symbol_resolver import resolve_symbol class INIGenerator: @@ -115,12 +116,16 @@ class INIGenerator: else: ini_lines.append("ExpertParameters=") + # MT5 tester delay is controlled by ExecutionMode itself: + # 0 = no delay + # -1 = random delay + # > 0 = fixed delay in milliseconds + execution_delay = int(bt_settings.get('execution_delay', 0) or 0) ini_lines.extend([ f"Symbol={symbol}", f"Period={self._get_timeframe_code(timeframe)}", f"Model={bt_settings['model']}", - f"ExecutionMode={bt_settings.get('execution_mode', 0)}", - f"ExecutionDelay={bt_settings.get('execution_delay', 0)}", + f"ExecutionMode={execution_delay}", f"Optimization={bt_settings['optimization']}", ]) if bt_settings.get("optimization"): @@ -184,14 +189,15 @@ class INIGenerator: param_combinations = self._generate_parameter_combinations(parameters) for symbol in bt_settings["symbols"]: + resolved_symbol = resolve_symbol(symbol, bt_settings) for timeframe in bt_settings["timeframes"]: for param_combo in param_combinations: ini_content = self._build_ini_content( - ea_filename, symbol, timeframe, + ea_filename, resolved_symbol, timeframe, bt_settings, param_combo, ea_name, set_file ) ini_filename = self._generate_filename( - ea_name, symbol, timeframe, param_combo + ea_name, resolved_symbol, timeframe, param_combo ) ini_path = os.path.join(self.output_dir, ini_filename) @@ -253,4 +259,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/scripts/mt5_auto_runner.py b/scripts/mt5_auto_runner.py index 0fff17e..fa17f11 100644 --- a/scripts/mt5_auto_runner.py +++ b/scripts/mt5_auto_runner.py @@ -225,6 +225,13 @@ class MT5AutoRunner: logger.info(f"Total INI files: {total}") + # First run must also start from a clean tester state. Otherwise the first test + # can inherit an already-open terminal's previous deposit/delay/symbol settings, + # while later tests look correct only because kill_between_tests runs after them. + if kill_between_tests: + self._kill_mt5() + time.sleep(2) + for idx, ini_path in enumerate(ini_files, 1): logger.info(f"[{idx}/{total}] {os.path.basename(ini_path)}") @@ -352,4 +359,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/scripts/optimizer/ea_batch_optimizer.py b/scripts/optimizer/ea_batch_optimizer.py index 3d8b252..6cd71ed 100644 --- a/scripts/optimizer/ea_batch_optimizer.py +++ b/scripts/optimizer/ea_batch_optimizer.py @@ -13,6 +13,7 @@ from ea_config_parser import load_ea_config, load_all_ea_configs, EAConfig from parameter_constraint import create_constraint_engine from smart_search import SmartSearch from analyze_results import ResultAnalyzer +from symbol_resolver import resolve_symbol class EABatchOptimizer: @@ -157,7 +158,7 @@ class EABatchOptimizer: '[Tester]', 'Expert=' + ea_config.ea_path, 'ExpertParameters=' + set_filename, - 'Symbol=' + test_config.get('symbol', 'EURUSD'), + 'Symbol=' + resolve_symbol(test_config.get('symbol', 'EURUSD'), test_config), 'Period=' + test_config.get('period', 'H1'), 'Model=' + str(test_config.get('model', 1)), 'FromDate=' + date_from, diff --git a/scripts/symbol_resolver.py b/scripts/symbol_resolver.py new file mode 100644 index 0000000..1c810ea --- /dev/null +++ b/scripts/symbol_resolver.py @@ -0,0 +1,28 @@ +"""Root symbol alias resolver (pure lookup, no auto-switching). + +用户视角的"逻辑品种"(如 XAUUSD)映射到 MT5 里实际使用的品种。 + +ponytail: + - 没有别名表 / 没条目 -> 原样返回(零行为变化,向后兼容) + - 有条目 -> 一比一映射,写啥用啥,不做任何基于日期/优化/周期的判断 + - 升级路径:要从 yaml 切到数据库 / MT5 SymbolSelect 校验,只改这里 + +配置示例(ea_configs.yaml): + symbol_aliases: + XAUUSD: XAUUSDc # 逻辑 XAUUSD -> 实际 XAUUSDc + XAGUSD: XAGUSDm # 逻辑 XAGUSD -> 实际 XAGUSDm + # EURUSD 没列 -> 始终 EURUSD +""" + +from typing import Any, Dict + + +def resolve_symbol(symbol: str, bt_settings: Dict[str, Any]) -> str: + """逻辑品种 -> 实际 MT5 品种。无任何自动判断,按表直查。""" + if not symbol: + return symbol + aliases = bt_settings.get("symbol_aliases") or {} + mapped = aliases.get(symbol) + if not mapped: # None / "" / 缺失 -> 直通 + return symbol + return mapped \ No newline at end of file diff --git a/tests/smoke_alias_backtest.py b/tests/smoke_alias_backtest.py new file mode 100644 index 0000000..5cd27c3 --- /dev/null +++ b/tests/smoke_alias_backtest.py @@ -0,0 +1,143 @@ +# Real backtest smoke test for the symbol alias resolver. +# Runs two backtests via the project's pipeline (INIGenerator + MT5AutoRunner): +# 1) XAUUSD (no alias) -> INI Symbol=XAUUSD +# 2) XAUUSD (alias -> XAUUSDc) -> INI Symbol=XAUUSDc +# Success: two .htm reports in C:\Users\Administrator\Desktop\mt5-backtest\reports, +# one with Symbol=XAUUSD, one with Symbol=XAUUSDc. + +import os +import sys +import re +import shutil + +HERE = os.path.dirname(os.path.abspath(__file__)) +PROJECT_ROOT = os.path.normpath(os.path.join(HERE, "..")) +SCRIPTS = os.path.join(PROJECT_ROOT, "scripts") +EXAMPLES_ROOT = os.path.normpath(os.path.join(PROJECT_ROOT, "..", "examples")) + +TARGET_REPORTS = r"C:\Users\Administrator\Desktop\mt5-backtest\reports" +MT5_PATH = r"C:\Program Files\MetaTrader 5 IC Markets Global\terminal64.exe" +MT5_DATA_DIR = r"C:\Users\Administrator\AppData\Roaming\MetaQuotes\Terminal\010E047102812FC0C18890992854220E" +MT5_TESTER_DIR = os.path.join(MT5_DATA_DIR, "MQL5", "Profiles", "Tester") + +EA_NAME = "MACD_MA_XAUUSD_5M" +EA_BASENAME = f"{EA_NAME}.ex5" +EA_FULL_PATH = os.path.join(MT5_DATA_DIR, "MQL5", "Experts", EA_BASENAME) +SET_SRC = os.path.join(EXAMPLES_ROOT, "configs", f"{EA_NAME}.set") +SET_DEST_NAME = "MACD_MA_XAUUSD_5M_smoke.set" +SET_DEST_PATH = os.path.join(MT5_TESTER_DIR, SET_DEST_NAME) + +os.makedirs(TARGET_REPORTS, exist_ok=True) +shutil.copy2(SET_SRC, SET_DEST_PATH) + +sys.path.insert(0, SCRIPTS) +from ini_generator import INIGenerator +from mt5_auto_runner import MT5AutoRunner + + +def build_cfg(aliases, out_ini_dir): + return { + "backtest_settings": { + "currency": "USD", + "date_range": {"from": "2026.05.01", "to": "2026.06.05"}, + "deposit": 10000, + "execution_delay": 0, + "forward_date": "", + "forward_mode": 0, + "leverage": "1:100", + "model": 0, + "optimization": 0, + "replace_report": True, + "shutdown_terminal": True, + "symbols": ["XAUUSD"], + "timeframes": ["H1"], + "visual": 0, + "symbol_aliases": aliases or {}, + }, + "eas": [ + {"name": EA_NAME, "filename": EA_FULL_PATH, "set_file": SET_DEST_NAME}, + ], + "mt5_settings": { + "terminal_path": MT5_PATH, + "data_dir": MT5_DATA_DIR, + "ini_dir": out_ini_dir, + "reports_dir": TARGET_REPORTS, + }, + } + + +def patch_ini_to_inline(ini_path): + """Patch the INI to use relative Expert + inline [TesterInputs] + (the format MT5 actually auto-starts with on this terminal).""" + with open(ini_path, encoding="utf-8") as f: + text = f.read() + text = text.replace(f"Expert={EA_FULL_PATH}", f"Expert={EA_BASENAME}") + text = text.replace(f"ExpertParameters={SET_DEST_NAME}", "ExpertParameters=") + with open(SET_DEST_PATH, encoding="utf-8", errors="ignore") as f: + set_text = f.read() + if not text.rstrip().endswith("[TesterInputs]"): + text = text.rstrip() + "\n\n[TesterInputs]\n" + set_text + with open(ini_path, "w", encoding="utf-8") as f: + f.write(text) + + +def run_one(label, aliases, out_dir): + cfg = build_cfg(aliases=aliases, out_ini_dir=out_dir) + gen = INIGenerator(cfg) + ini_files = gen.generate_ini_files() + assert len(ini_files) == 1, f"{label}: expected 1 INI, got {len(ini_files)}" + ini = ini_files[0] + patch_ini_to_inline(ini) + with open(ini, encoding="utf-8") as f: + sym_line = next(l for l in f if l.startswith("Symbol=")) + print(f"[{label}] INI Symbol line: {sym_line.strip()}") + + runner = MT5AutoRunner(cfg) + res = runner._execute_single_ini(ini, timeout_min=15) + print(f"[{label}] runner status: {res['status']}, report_path: {res.get('report_path')}") + return ini, res + + +def main(): + tmp_root = os.path.join(PROJECT_ROOT, "config", "generated_smoke") + if os.path.exists(tmp_root): + shutil.rmtree(tmp_root) + os.makedirs(tmp_root, exist_ok=True) + out1 = os.path.join(tmp_root, "no_alias"); os.makedirs(out1, exist_ok=True) + out2 = os.path.join(tmp_root, "with_alias"); os.makedirs(out2, exist_ok=True) + + print("=== run 1: no alias (should pick XAUUSD) ===") + ini1, r1 = run_one("no_alias", aliases=None, out_dir=out1) + print() + print("=== run 2: alias XAUUSD -> XAUUSDc (should pick XAUUSDc) ===") + ini2, r2 = run_one("with_alias", aliases={"XAUUSD": "XAUUSDc"}, out_dir=out2) + print() + + print("=== summary ===") + print(f" INI1 ({os.path.basename(ini1)}) -> {r1['status']}, {r1.get('report_path')}") + print(f" INI2 ({os.path.basename(ini2)}) -> {r2['status']}, {r2.get('report_path')}") + + reports = sorted([f for f in os.listdir(TARGET_REPORTS) + if f.startswith('MACD_MA_XAUUSD_5M_') and f.endswith('.htm')]) + print(f"\nReports in {TARGET_REPORTS}:") + found = {"XAUUSD": [], "XAUUSDc": []} + for r in reports: + path = os.path.join(TARGET_REPORTS, r) + size = os.path.getsize(path) + # MT5 saves reports as UTF-16 LE with BOM + text = open(path, encoding='utf-16', errors='ignore').read() + m = re.search(r'交易品种:\s*]*>([^<]+)', text) + sym = m.group(1) if m else "?" + found[sym].append(r) + print(f" - {r} ({size} bytes) symbol={sym}") + + print(f"\nDistinct symbols found: {sorted({k for k, v in found.items() if v})}") + if not found["XAUUSD"]: + raise AssertionError("No report with Symbol=XAUUSD") + if not found["XAUUSDc"]: + raise AssertionError("No report with Symbol=XAUUSDc") + print("SUCCESS: both XAUUSD and XAUUSDc reports present in target dir.") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/test_ini_generator.py b/tests/test_ini_generator.py new file mode 100644 index 0000000..f90a38d --- /dev/null +++ b/tests/test_ini_generator.py @@ -0,0 +1,96 @@ +"""End-to-end: INIGenerator picks the right MT5 Symbol from explicit alias map. + +Run: python tests/test_ini_generator.py +""" + +import os +import sys +import tempfile + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, os.path.normpath(os.path.join(HERE, "..", "scripts"))) + +from ini_generator import INIGenerator + + +def _make_cfg(aliases, symbols, out_dir): + return { + "mt5_settings": {"terminal_path": "", "data_dir": "", + "reports_dir": "{PROJECT_ROOT}/reports", "ini_dir": out_dir}, + "backtest_settings": { + "currency": "USD", + "date_range": {"from": "2010.01.01", "to": "2024.12.31"}, + "deposit": 10000, "execution_delay": 0, "forward_date": "", + "forward_mode": 0, "leverage": "1:100", "model": 0, + "optimization": 0, "replace_report": True, "shutdown_terminal": True, + "symbols": symbols, "timeframes": ["H1"], "visual": 0, + "symbol_aliases": aliases, + }, + "eas": [{"name": "DummyEA", "filename": "MQL5/Experts/DummyEA.ex5", "parameters": {}}], + } + + +def _grep_symbol(ini_path, target): + with open(ini_path, "r", encoding="utf-8") as f: + for line in f: + if line.startswith("Symbol="): + return line.strip().split("=", 1)[1] == target + return False + + +def _get_ini_value(ini_path, key): + with open(ini_path, "r", encoding="utf-8") as f: + for line in f: + if line.startswith(f"{key}="): + return line.strip().split("=", 1)[1] + return None + + +def expect(label, ok): + print(f" {'PASS' if ok else 'FAIL'} {label}") + if not ok: + raise AssertionError(label) + + +def main(): + with tempfile.TemporaryDirectory() as tmp: + cfg = _make_cfg({"XAUUSD": "XAUUSDc"}, ["XAUUSD", "EURUSD"], tmp) + files = INIGenerator(cfg).generate_ini_files() + expect("2 files", len(files) == 2) + expect("XAUUSD alias applied", any(_grep_symbol(f, "XAUUSDc") for f in files)) + expect("EURUSD passes through", any(_grep_symbol(f, "EURUSD") for f in files)) + + with tempfile.TemporaryDirectory() as tmp: + cfg = _make_cfg({}, ["XAUUSD"], tmp) + files = INIGenerator(cfg).generate_ini_files() + expect("no alias -> bare", any(_grep_symbol(f, "XAUUSD") for f in files)) + + with tempfile.TemporaryDirectory() as tmp: + cfg = _make_cfg({"XAUUSD": "XAUUSDm"}, ["XAUUSD"], tmp) + files = INIGenerator(cfg).generate_ini_files() + expect("explicit override wins", any(_grep_symbol(f, "XAUUSDm") for f in files)) + + with tempfile.TemporaryDirectory() as tmp: + cfg = _make_cfg({}, ["XAUUSD"], tmp) + cfg["backtest_settings"]["execution_delay"] = 0 + files = INIGenerator(cfg).generate_ini_files() + expect("delay=0 -> ExecutionMode=0", _get_ini_value(files[0], "ExecutionMode") == "0") + expect("delay=0 -> no ExecutionDelay line", _get_ini_value(files[0], "ExecutionDelay") is None) + + with tempfile.TemporaryDirectory() as tmp: + cfg = _make_cfg({}, ["XAUUSD"], tmp) + cfg["backtest_settings"]["execution_delay"] = -1 + files = INIGenerator(cfg).generate_ini_files() + expect("delay=-1 -> ExecutionMode=-1", _get_ini_value(files[0], "ExecutionMode") == "-1") + + with tempfile.TemporaryDirectory() as tmp: + cfg = _make_cfg({}, ["XAUUSD"], tmp) + cfg["backtest_settings"]["execution_delay"] = 250 + files = INIGenerator(cfg).generate_ini_files() + expect("delay=250 -> ExecutionMode=250", _get_ini_value(files[0], "ExecutionMode") == "250") + + print("\nAll INI generator alias checks passed.") + + +if __name__ == "__main__": + main() diff --git a/tests/test_symbol_resolver.py b/tests/test_symbol_resolver.py new file mode 100644 index 0000000..798da35 --- /dev/null +++ b/tests/test_symbol_resolver.py @@ -0,0 +1,60 @@ +"""Symbol resolver self-check (pure lookup, no auto logic). + +Run: python tests/test_symbol_resolver.py +""" + +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, os.path.normpath(os.path.join(HERE, "..", "scripts"))) + +from symbol_resolver import resolve_symbol + + +def _bt(aliases=None): + return {"symbol_aliases": aliases or {}} + + +def expect(label, got, want): + ok = got == want + print(f" {'PASS' if ok else 'FAIL'} {label}: got={got!r} want={want!r}") + if not ok: + raise AssertionError(label) + + +def main(): + print("[1] has alias -> use alias") + expect("XAUUSD -> XAUUSDc", resolve_symbol("XAUUSD", _bt({"XAUUSD": "XAUUSDc"})), "XAUUSDc") + + print("[2] has alias for another symbol -> that one passes through") + expect("EURUSD no alias", resolve_symbol("EURUSD", _bt({"XAUUSD": "XAUUSDc"})), "EURUSD") + + print("[3] no aliases config -> bare (back-compat)") + expect("no config", resolve_symbol("XAUUSD", _bt()), "XAUUSD") + + print("[4] alias value can be any string (suffix, full name, even identical)") + expect("alias = c suffix", resolve_symbol("XAUUSD", _bt({"XAUUSD": "c"})), "c") + + print("[5] empty symbol -> empty") + expect("empty", resolve_symbol("", _bt({"": "XAUUSDc"})), "") + + print("[6] aliases value None -> bare") + expect("None value", resolve_symbol("XAUUSD", _bt({"XAUUSD": None})), "XAUUSD") + + print("[7] symbol_aliases missing key entirely -> bare") + expect("missing key", resolve_symbol("XAUUSD", {"date_range": {"from": "2020.01.01", "to": "2024.01.01"}}), "XAUUSD") + + print("[8] long date span does NOT auto-switch (explicit override only)") + expect("long span no alias", resolve_symbol("XAUUSD", {"date_range": {"from": "2010.01.01", "to": "2024.01.01"}}), "XAUUSD") + + print("[9] explicit alias always wins regardless of date/optimization context") + ctx = {"date_range": {"from": "2010.01.01", "to": "2024.01.01"}, + "optimization": 1, "symbol_aliases": {"XAUUSD": "XAUUSDm"}} + expect("explicit + opt", resolve_symbol("XAUUSD", ctx), "XAUUSDm") + + print("\nAll symbol resolver checks passed.") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/verify_reports.py b/tests/verify_reports.py new file mode 100644 index 0000000..dcadd5a --- /dev/null +++ b/tests/verify_reports.py @@ -0,0 +1,16 @@ +# Verify the two backtest reports. +import os, re + +TARGET = r"C:\Users\Administrator\Desktop\mt5-backtest\reports" +reports = sorted([f for f in os.listdir(TARGET) if f.startswith('MACD_MA_XAUUSD_5M_') and f.endswith('.htm')]) + +print(f"Found {len(reports)} candidate reports:") +for r in reports: + p = os.path.join(TARGET, r) + size = os.path.getsize(p) + text = open(p, encoding='utf-8', errors='ignore').read() + m = re.search(r'交易品种:\s*]*>([^<]+)', text) + sym = m.group(1) if m else "(not found)" + has_xauusd = 'XAUUSD' in text + has_xauusdc = 'XAUUSDc' in text + print(f" {r} size={size} symbol_in_report={sym} contains_XAUUSD={has_xauusd} contains_XAUUSDc={has_xauusdc}") \ No newline at end of file