commit 6450d1efc65fda24653613877015394b55b73242 Author: ironsan2kk-pixel Date: Mon Jun 8 10:36:15 2026 +0200 Fix download links diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..3095335 Binary files /dev/null and b/.DS_Store differ diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/01-GridEA/GridEA.mq4 b/01-GridEA/GridEA.mq4 new file mode 100644 index 0000000..c66c643 --- /dev/null +++ b/01-GridEA/GridEA.mq4 @@ -0,0 +1,96 @@ +//+------------------------------------------------------------------+ +//| GridEA.mq4 | +//| Copyright 2018, Valentinos Galanos | +//+------------------------------------------------------------------+ +#define ver "1.00" +#property copyright "Copyright 2018, Valentinos Galanos " +#property version ver +#property strict + +//--- input parameters +input int GridGap = 50; // Grid Gap (Pips) +input double LotSize = 0.01; // Trade Lot Volume +input int TotalGridLines = 7; // Total Grid Lines Each Side + +//--- Includes +#include "Library\GridExpert.mqh" + +//--- Expert +CGridExpert* GridEA; +//--- Memory +bool initialized=false; +bool timerCalled=false; + + +//+------------------------------------------------------------------+ +//| Expert initialization function | +//+------------------------------------------------------------------+ +int OnInit() +{ + //--- create timer + EventSetMillisecondTimer(500); + + if( initialized == false ) + { + initialized = true; + GridEA = new CGridExpert; + return GridEA.OnInit(); + } + + //--- + return(INIT_SUCCEEDED); +} +//+------------------------------------------------------------------+ +//| Expert deinitialization function | +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) +{ + //--- destroy timer + EventKillTimer(); + + switch(reason) + { + case REASON_CLOSE: + case REASON_INITFAILED: + case REASON_RECOMPILE: + case REASON_REMOVE: + case REASON_ACCOUNT: + case REASON_CHARTCLOSE: + case REASON_PROGRAM: + if( initialized && GridEA != NULL) + { + GridEA.OnDeinit(reason); + initialized = false; + delete GridEA; + } + break; + } +} +//+------------------------------------------------------------------+ +//| Expert tick function | +//+------------------------------------------------------------------+ +void OnTick() +{ + //--- + if( initialized && GridEA != NULL ) + { + GridEA.OnTick(); + } +} +//+------------------------------------------------------------------+ +//| Timer function | +//+------------------------------------------------------------------+ +void OnTimer() +{ + if(timerCalled == false) + { + timerCalled = true; + //--- + if( initialized && GridEA != NULL ) + { + GridEA.OnTimer(); + } + timerCalled = false; + } +} +//+------------------------------------------------------------------+ diff --git a/01-GridEA/GridEA.pdf b/01-GridEA/GridEA.pdf new file mode 100644 index 0000000..fa8b0d7 Binary files /dev/null and b/01-GridEA/GridEA.pdf differ diff --git a/02-GridMaster-Pro/GridMaster-Pro.mq5 b/02-GridMaster-Pro/GridMaster-Pro.mq5 new file mode 100644 index 0000000..f51bc76 --- /dev/null +++ b/02-GridMaster-Pro/GridMaster-Pro.mq5 @@ -0,0 +1,332 @@ +//+------------------------------------------------------------------+ +//| GridMaster Pro.mq5 | +//| Copyright 2024, Sajid. | +//| https://www.mql5.com/en/users/sajidmahamud835 | +//+------------------------------------------------------------------+ +#property copyright "Copyright 2024, Sajid." +#property link "https://www.mql5.com/en/users/sajidmahamud835" +#property version "2.00" +#property strict +#property description "GridMaster Pro v2 — Bi-directional ATR grid with proper MM, min-stop awareness, and drawdown protection." + +#include + +//--- Enums +enum ENUM_GRID_MODE { + GRID_NEUTRAL = 0, // Neutral: BUY below + SELL above + GRID_BULLISH = 1, // Bullish: BUY only + GRID_BEARISH = 2, // Bearish: SELL only +}; + +enum ENUM_LOT_MODE { + LOT_FIXED = 0, // Fixed lot size + LOT_DYNAMIC = 1, // Risk-based (% of balance per order) +}; + +//--- Input Parameters — Grid +input ENUM_GRID_MODE GridMode = GRID_NEUTRAL; // Grid Mode +input int MaxOrders = 5; // Max orders per side +input int ATRPeriod = 14; // ATR period +input double ATRMultiplier = 1.5; // ATR multiplier for grid distance + +//--- Input Parameters — Orders +input ENUM_LOT_MODE LotMode = LOT_FIXED; // Lot sizing mode +input double LotSize = 0.1; // Fixed lot size +input double RiskPercent = 1.0; // Risk % per order (dynamic mode) +input bool UseTakeProfit = true; // Enable Take Profit +input double DefaultTP = 200.0; // Min TP in points (auto-adjusted for broker) +input bool UseStopLoss = true; // Enable Stop Loss +input double DefaultSL = 1000.0; // Min SL in points (auto-adjusted for broker) +input bool UseTrailingStop = true; // Enable Trailing Stop +input double TrailingPoints = 100.0; // Trailing stop in points +input double TrailingStep = 20.0; // Trailing step in points + +//--- Input Parameters — Risk Management +input double MaxDrawdownPct = 5.0; // Max drawdown % before pausing +input bool CloseOnDrawdown = true; // Close all orders on drawdown breach + +//--- Input Parameters — Magic & Debug +input int MagicBase = 47291; // Base magic number +input bool DebugMode = false; // Enable debug logging + +//--- Global Variables +CTrade trade; +int magicNumber; +double gridDistance; +double accountEquityStart; +bool gridPaused = false; +string logFile; + +//+------------------------------------------------------------------+ +//| Expert initialization | +//+------------------------------------------------------------------+ +int OnInit() { + // Generate collision-safe magic number: base + symbol hash + timeframe + magicNumber = MagicBase + (int)(StringLen(_Symbol) * 1000) + (int)Period(); + trade.SetExpertMagicNumber(magicNumber); + trade.SetDeviationInPoints(50); + trade.SetTypeFilling(ORDER_FILLING_IOC); + + accountEquityStart = AccountInfoDouble(ACCOUNT_EQUITY); + logFile = "GridMasterPro_" + _Symbol + "_" + IntegerToString(Period()) + ".log"; + + WriteLog("GridMaster Pro v2.00 initialized | Magic: " + IntegerToString(magicNumber) + + " | Symbol: " + _Symbol + " | Grid mode: " + EnumToString(GridMode)); + + return INIT_SUCCEEDED; +} + +//+------------------------------------------------------------------+ +//| Expert deinitialization | +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) { + WriteLog("EA deinitialized. Reason: " + IntegerToString(reason) + " | Open positions left: " + IntegerToString(CountOurPositions(ORDER_TYPE_BUY) + CountOurPositions(ORDER_TYPE_SELL))); +} + +//+------------------------------------------------------------------+ +//| Expert tick | +//+------------------------------------------------------------------+ +void OnTick() { + // Drawdown check + if (CloseOnDrawdown && CheckDrawdown()) { + if (!gridPaused) { + WriteLog("DRAWDOWN LIMIT REACHED — closing all positions and pausing grid"); + CloseAllPositions(); + gridPaused = true; + } + return; + } + + // Resume grid if paused and equity has recovered + if (gridPaused) { + double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY); + if (currentEquity >= accountEquityStart * (1.0 - MaxDrawdownPct / 200.0)) { + gridPaused = false; + accountEquityStart = currentEquity; + WriteLog("Grid resumed after equity recovery"); + } else { + return; + } + } + + // Recalculate grid distance every tick + gridDistance = CalculateGridDistance(); + if (gridDistance <= 0) return; + + double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + + int buyCount = CountOurPositions(ORDER_TYPE_BUY); + int sellCount = CountOurPositions(ORDER_TYPE_SELL); + + // Manage trailing stops + if (UseTrailingStop) ManageTrailingStops(); + + // Place BUY grid orders + if (GridMode != GRID_BEARISH && buyCount < MaxOrders) { + double buyPrice = ask - gridDistance * (buyCount + 1) * _Point; + PlaceGridOrder(ORDER_TYPE_BUY, buyPrice); + } + + // Place SELL grid orders + if (GridMode != GRID_BULLISH && sellCount < MaxOrders) { + double sellPrice = bid + gridDistance * (sellCount + 1) * _Point; + PlaceGridOrder(ORDER_TYPE_SELL, sellPrice); + } +} + +//+------------------------------------------------------------------+ +//| Place a grid order with proper SL/TP | +//+------------------------------------------------------------------+ +void PlaceGridOrder(ENUM_ORDER_TYPE type, double price) { + // Check if order already exists near this price level + if (OrderExistsNearPrice(type, price, gridDistance * 0.5 * _Point)) return; + + // Broker minimum stop distance + long stopLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); + double minStop = MathMax((double)stopLevel, 10.0) * _Point * 1.2; // 20% buffer over broker min + + double sl = 0, tp = 0; + double lot = CalculateLot(); + + if (type == ORDER_TYPE_BUY) { + if (UseTakeProfit) tp = price + MathMax(DefaultTP * _Point, minStop * 1.5); + if (UseStopLoss) sl = price - MathMax(DefaultSL * _Point, minStop * MaxOrders); + } else { + if (UseTakeProfit) tp = price - MathMax(DefaultTP * _Point, minStop * 1.5); + if (UseStopLoss) sl = price + MathMax(DefaultSL * _Point, minStop * MaxOrders); + } + + // Normalize prices + int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); + price = NormalizeDouble(price, digits); + sl = sl > 0 ? NormalizeDouble(sl, digits) : 0; + tp = tp > 0 ? NormalizeDouble(tp, digits) : 0; + + bool sent; + if (type == ORDER_TYPE_BUY) { + sent = trade.Buy(lot, _Symbol, 0, sl, tp, "Grid BUY"); + } else { + sent = trade.Sell(lot, _Symbol, 0, sl, tp, "Grid SELL"); + } + + if (sent) { + WriteLog("Placed " + (type == ORDER_TYPE_BUY ? "BUY" : "SELL") + + " | Lot: " + DoubleToString(lot, 2) + + " | Price: ~" + DoubleToString(price, digits) + + " | SL: " + DoubleToString(sl, digits) + + " | TP: " + DoubleToString(tp, digits)); + } else { + WriteLog("FAILED to place " + (type == ORDER_TYPE_BUY ? "BUY" : "SELL") + + " | Error: " + IntegerToString(GetLastError()) + + " | Price: " + DoubleToString(price, digits) + + " | MinStop: " + DoubleToString(minStop / _Point, 0) + " pts"); + } +} + +//+------------------------------------------------------------------+ +//| Manage trailing stops for all our positions | +//+------------------------------------------------------------------+ +void ManageTrailingStops() { + double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); + long stopLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); + double minStop = MathMax((double)stopLevel, 10.0) * _Point * 1.2; + + for (int i = PositionsTotal() - 1; i >= 0; i--) { + if (!PositionSelectByTicket(PositionGetTicket(i))) continue; + if (PositionGetString(POSITION_SYMBOL) != _Symbol) continue; + if (PositionGetInteger(POSITION_MAGIC) != magicNumber) continue; + + ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); + double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); + double currentSL = PositionGetDouble(POSITION_SL); + ulong ticket = PositionGetInteger(POSITION_TICKET); + + double newSL = 0; + + if (posType == POSITION_TYPE_BUY) { + double profit = bid - openPrice; + if (profit >= TrailingPoints * _Point) { + newSL = NormalizeDouble(bid - TrailingPoints * _Point, digits); + if (newSL > currentSL + TrailingStep * _Point && newSL > openPrice - minStop) { + trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP)); + } + } + } else { + double profit = openPrice - ask; + if (profit >= TrailingPoints * _Point) { + newSL = NormalizeDouble(ask + TrailingPoints * _Point, digits); + if ((currentSL == 0 || newSL < currentSL - TrailingStep * _Point) && newSL < openPrice + minStop) { + trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP)); + } + } + } + } +} + +//+------------------------------------------------------------------+ +//| Calculate ATR-based grid distance in points | +//+------------------------------------------------------------------+ +double CalculateGridDistance() { + double atr = iATR(_Symbol, 0, ATRPeriod); + if (atr <= 0) return 0; + return (atr * ATRMultiplier) / _Point; // Return in points +} + +//+------------------------------------------------------------------+ +//| Calculate lot size based on mode | +//+------------------------------------------------------------------+ +double CalculateLot() { + if (LotMode == LOT_FIXED) return LotSize; + + double balance = AccountInfoDouble(ACCOUNT_BALANCE); + double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); + double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); + if (tickValue <= 0 || tickSize <= 0 || DefaultSL <= 0) return LotSize; + + double riskAmount = balance * RiskPercent / 100.0; + double slValue = DefaultSL * _Point / tickSize * tickValue; + double lot = NormalizeDouble(riskAmount / slValue, 2); + + double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); + double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); + double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); + + lot = MathMax(minLot, MathMin(maxLot, MathRound(lot / lotStep) * lotStep)); + return lot; +} + +//+------------------------------------------------------------------+ +//| Count our open positions by type | +//+------------------------------------------------------------------+ +int CountOurPositions(ENUM_ORDER_TYPE type) { + int count = 0; + for (int i = PositionsTotal() - 1; i >= 0; i--) { + if (!PositionSelectByTicket(PositionGetTicket(i))) continue; + if (PositionGetString(POSITION_SYMBOL) != _Symbol) continue; + if (PositionGetInteger(POSITION_MAGIC) != magicNumber) continue; + ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); + if ((type == ORDER_TYPE_BUY && posType == POSITION_TYPE_BUY) || + (type == ORDER_TYPE_SELL && posType == POSITION_TYPE_SELL)) { + count++; + } + } + return count; +} + +//+------------------------------------------------------------------+ +//| Check if order already exists near a price level | +//+------------------------------------------------------------------+ +bool OrderExistsNearPrice(ENUM_ORDER_TYPE type, double price, double tolerance) { + for (int i = PositionsTotal() - 1; i >= 0; i--) { + if (!PositionSelectByTicket(PositionGetTicket(i))) continue; + if (PositionGetString(POSITION_SYMBOL) != _Symbol) continue; + if (PositionGetInteger(POSITION_MAGIC) != magicNumber) continue; + ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); + if ((type == ORDER_TYPE_BUY && posType == POSITION_TYPE_BUY) || + (type == ORDER_TYPE_SELL && posType == POSITION_TYPE_SELL)) { + double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); + if (MathAbs(openPrice - price) <= tolerance) return true; + } + } + return false; +} + +//+------------------------------------------------------------------+ +//| Check if drawdown limit breached | +//+------------------------------------------------------------------+ +bool CheckDrawdown() { + double equity = AccountInfoDouble(ACCOUNT_EQUITY); + double maxLoss = accountEquityStart * MaxDrawdownPct / 100.0; + return (accountEquityStart - equity) >= maxLoss; +} + +//+------------------------------------------------------------------+ +//| Close all our positions | +//+------------------------------------------------------------------+ +void CloseAllPositions() { + for (int i = PositionsTotal() - 1; i >= 0; i--) { + ulong ticket = PositionGetTicket(i); + if (!PositionSelectByTicket(ticket)) continue; + if (PositionGetString(POSITION_SYMBOL) != _Symbol) continue; + if (PositionGetInteger(POSITION_MAGIC) != magicNumber) continue; + trade.PositionClose(ticket); + } +} + +//+------------------------------------------------------------------+ +//| Append log to file (fixes overwrite bug) | +//+------------------------------------------------------------------+ +void WriteLog(string message) { + if (!DebugMode && StringFind(message, "FAILED") < 0 && StringFind(message, "DRAWDOWN") < 0) return; + + int handle = FileOpen(logFile, FILE_READ | FILE_WRITE | FILE_TXT | FILE_COMMON); + if (handle != INVALID_HANDLE) { + FileSeek(handle, 0, SEEK_END); // Append — seek to end + string ts = TimeToString(TimeCurrent(), TIME_DATE | TIME_MINUTES | TIME_SECONDS); + FileWriteString(handle, ts + " | " + message + "\n"); + FileClose(handle); + } +} diff --git a/02-GridMaster-Pro/GridMaster-Pro.pdf b/02-GridMaster-Pro/GridMaster-Pro.pdf new file mode 100644 index 0000000..a48618a Binary files /dev/null and b/02-GridMaster-Pro/GridMaster-Pro.pdf differ diff --git a/03-GridMartingale-TA/GridMartingale-TA.mq4 b/03-GridMartingale-TA/GridMartingale-TA.mq4 new file mode 100644 index 0000000..0cff2dc Binary files /dev/null and b/03-GridMartingale-TA/GridMartingale-TA.mq4 differ diff --git a/03-GridMartingale-TA/GridMartingale-TA.pdf b/03-GridMartingale-TA/GridMartingale-TA.pdf new file mode 100644 index 0000000..8c66615 Binary files /dev/null and b/03-GridMartingale-TA/GridMartingale-TA.pdf differ diff --git a/04-MQL4-Martingale/MQL4-Martingale.mq4 b/04-MQL4-Martingale/MQL4-Martingale.mq4 new file mode 100644 index 0000000..96e3756 --- /dev/null +++ b/04-MQL4-Martingale/MQL4-Martingale.mq4 @@ -0,0 +1 @@ +//Pending: Awaiting confirmation on whether it will be released for free or as a paid-only option. \ No newline at end of file diff --git a/04-MQL4-Martingale/MQL4-Martingale.pdf b/04-MQL4-Martingale/MQL4-Martingale.pdf new file mode 100644 index 0000000..68a9917 Binary files /dev/null and b/04-MQL4-Martingale/MQL4-Martingale.pdf differ diff --git a/05-Biased-Martingale/Biased-Martingale.mq4 b/05-Biased-Martingale/Biased-Martingale.mq4 new file mode 100644 index 0000000..dc026b7 --- /dev/null +++ b/05-Biased-Martingale/Biased-Martingale.mq4 @@ -0,0 +1,540 @@ +//+------------------------------------------------------------------+ +//| Biased Martingale.mq4 | +//| Matthew Kastor | +//| https://github.com/matthewkastor | +//+------------------------------------------------------------------+ +#property copyright "Matthew Kastor" +#property link "https://github.com/matthewkastor" +#property version "1.00" +#property strict +bool TestDisabled=false; +//+------------------------------------------------------------------+ +//|Enumeration to indicate directional bias. | +//+------------------------------------------------------------------+ +enum Enum_Direction + { + BUYING, + SELLING, + NONE + }; + +Enum_Direction Direction=NONE; + +input double StartFactor=0.5; +input double IncreaseFactor=2; +input ENUM_TIMEFRAMES BiasTimeframe=PERIOD_D1; +input int BiasPeriod=20; +input int BiasLookback=12; + +input ENUM_DAY_OF_WEEK StartDay=SUNDAY; +input ENUM_DAY_OF_WEEK EndDay=SUNDAY; +input int StartHour=0; +input int EndHour=0; + +datetime lastBarTime=0; +//+------------------------------------------------------------------+ +//| Expert initialization function | +//+------------------------------------------------------------------+ +int OnInit() + { + return(INIT_SUCCEEDED); + } +//+------------------------------------------------------------------+ +//| Expert deinitialization function | +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) + { + } +//+------------------------------------------------------------------+ +//| Expert tick function | +//+------------------------------------------------------------------+ +void OnTick() + { + if(IsTesting() && TestDisabled==true) + { + return; + } + PositionManagement(); + } +//+------------------------------------------------------------------+ +//| | +//+------------------------------------------------------------------+ +bool IsTimeBetween(int startDay,int startHour,int endDay,int endHour) + { + if((startDay==endDay) && (startHour==endHour)) + { + // The schedule starts and ends at the same time, it is neither + // day trading nor night trading... so it's always trading because + // never trading is as simple as removing the EA. + return true; + } + int D=TimeDayOfWeek(TimeCurrent()); + int H=TimeHour(TimeCurrent()); + + if((startDay<=D) && (endDay>=D) && (startHour<=H) && (endHour>=H)) + { + //Print(StartDay+" "+StartHour+" < "+D+" "+H+" < "+EndDay+" "+EndHour); + return true; + } +//Print(StartDay+" "+StartHour+" > "+D+" "+H+" > "+EndDay+" "+EndHour); + return false; + } +//+------------------------------------------------------------------+ +//|Gets the profit of the last closed order. | +//+------------------------------------------------------------------+ +double PairProfitSince(string symbol,int secondsAgo) + { + double num=0; + for(int i=OrdersHistoryTotal()-1;i>=0;i--) + { + if(OrderSelect(i,SELECT_BY_POS,MODE_HISTORY) && OrderSymbol()==symbol && (OrderType()==OP_BUY || OrderType()==OP_SELL)) + { + if(OrderCloseTime()>(Time[0]-secondsAgo)) + { + num+=OrderProfit(); + } + } + } + return num; + } +//+------------------------------------------------------------------+ +//|Gets the total size on the given currency pair. | +//+------------------------------------------------------------------+ +double PairLotsTotal(string symbol) + { + double num=0; + for(int i=0;i0 && sum>0) + { + return (sum / num); + } + else + { + return 0; + } + } +//+------------------------------------------------------------------+ +//|Gets the lowest price paid for any order on the given pair. | +//+------------------------------------------------------------------+ +double PairLowestPricePaid(string symbol) + { + double num=0; + for(int i=0;inum) + { + num=OrderOpenPrice(); + } + } + } + return num; + } +//+------------------------------------------------------------------+ +//|Gets the direction the given symbol is already traded in. | +//+------------------------------------------------------------------+ +Enum_Direction PairDirection(string symbol) + { + for(int i=0;i0) + { + for(int i=0;iOrderStopLoss()) + { + bool ret=OrderModify(OrderTicket(),OrderOpenPrice(),sl,OrderTakeProfit(),0); + if(!ret) + { + PrintFormat("Attempt to set stop loss on ticket %25.0i for %2.2f %s @ %2.5f failed. (mql error %5.0i)",OrderTicket(),OrderLots(),symbol,bid,GetLastError()); + } + } + } + else if(OrderType()==OP_SELL) + { + sl=NormalizeDouble((ask+pa)/2,Digits); + if(OrderStopLoss()==0 || slaccountSizedLots) + { + orderSize=minLot; + } + else + { + orderSize=accountSizedLots; + } + } + orderSize=NormalizeDouble(orderSize,2); + if(orderSize>maxLot) + { + //PrintFormat("Not Opening order, maximum lot size exceeded. Reduce the StartFactor. (Max) %2.2f < (Requested) %2.2f lots.",maxLot,orderSize); + //CloseOpenOrders(symbol); + orderSize=maxLot-minLot; + //return; + } + if(buyingOrSelling==BUYING) + { + if(AccountFreeMarginCheck(symbol,OP_BUY,orderSize)<=AccountEquity()*0.1 || GetLastError()==134) + { + PrintFormat("Not Opening order, not enough free margin for %2.2f lots. Reduce the StartFactor.",orderSize); + //CloseOpenOrders(symbol); + return; + } + int ret=OrderSend(symbol,OP_BUY,orderSize,ask,0,0,0); + if(ret == -1) + { + PrintFormat("Failed attempt to BUY %2.2f %s @ %2.5f (mql error %5.0i)",orderSize,symbol,ask,GetLastError()); + } + } + else if(buyingOrSelling==SELLING) + { + if(AccountFreeMarginCheck(symbol,OP_SELL,orderSize)<=AccountEquity()*0.1 || GetLastError()==134) + { + PrintFormat("Not Opening order, not enough free margin for %2.2f lots. Reduce the StartFactor.",orderSize); + //CloseOpenOrders(symbol); + return; + } + int ret=OrderSend(symbol,OP_SELL,orderSize,bid,0,0,0); + if(ret == -1) + { + PrintFormat("Failed attempt to SELL %2.2f %s @ %2.5f (mql error %5.0i)",orderSize,symbol,bid,GetLastError()); + } + } + } +//+------------------------------------------------------------------+ +//|Gets the direction to trade in for the given symbol. | +//+------------------------------------------------------------------+ +Enum_Direction GetBiasDirection(string symbol) + { + double custAvgNow=iMA(symbol,BiasTimeframe,BiasPeriod,0,MODE_SMA,PRICE_CLOSE,1); + double custAvgLookback=iMA(symbol,BiasTimeframe,BiasPeriod,0,MODE_SMA,PRICE_CLOSE,BiasLookback); + if(custAvgNow>custAvgLookback) + { + return BUYING; + } + if(custAvgNow2) + { + vf=2; + } + if(vf<=0) + { + vf=1; + } + double output=1.5 *(vf*iATR(symbol,BiasTimeframe,BiasPeriod,1)); + if(output-1) + { + lockFactor=-1; + } + return lockFactor; + } +//+------------------------------------------------------------------+ +//|Manages the position. | +//+------------------------------------------------------------------+ +void PositionManagement() + { + string Pair=Symbol(); + + int biasBarCount=Bars(Pair,BiasTimeframe); + int volatilityBarsCount=Bars(Pair,PERIOD_W1); + if((BiasLookback+BiasPeriod)>biasBarCount) + { + Print("Not enough history to form bias : BiasLookback + BiasPeriod"); + return; + } + if(52>volatilityBarsCount) + { + Print("Not enough history to form bias : volatilityBarsCount"); + return; + } + + double ProfitPoints=GetProfitPoints(Pair); + double PairAveragePrice=PairAveragePrice(Pair); + double PairLots=PairLotsTotal(Pair); + double bid = MarketInfo(Pair, MODE_BID); + double ask = MarketInfo(Pair, MODE_ASK); + double ProfitTarget=0; + double CurrentOpenPrice=0; + double CurrentClosePrice=0; + string dirMsg="No Direction"; + +// manual entry could initialize the trades while the +// "Direction" latch is considering the opposite direction. + if(PairAveragePrice>0) + { + Direction=PairDirection(Pair); + } + + if(Direction==BUYING) + { + dirMsg="Buying"; + ProfitTarget=PairAveragePrice+ProfitPoints; + CurrentOpenPrice=ask; + CurrentClosePrice=bid; + } + + if(Direction==SELLING) + { + dirMsg="Selling"; + ProfitTarget=PairAveragePrice-ProfitPoints; + CurrentOpenPrice=bid; + CurrentClosePrice=ask; + } + + double dd=GetDrawdownPercent(); + Comment( + StringFormat( + "%s %3.2f Lots at %3.5f, Targeting %3.5f %2.2f DD %2.2f" + ,dirMsg,PairLots,PairAveragePrice,ProfitTarget,PairProfitSince(Pair,60*60*24*30),dd)); + + bool tradingTime=IsTimeBetween(StartDay,StartHour,EndDay,EndHour); + if(PairAveragePrice==0 && tradingTime) + { + Direction=GetBiasDirection(Pair); + if(Direction!=NONE) + { + Print("Opening order, initializing position."); + OpenOrder(Pair,Direction); + } + return; + } + else if(AccountFreeMargin()ProfitTarget) + { + Print("Closing orders, profit target reached."); + CloseOpenOrders(Pair); + return; + } + else if(PairAveragePrice!=0 && Direction==SELLING && CurrentClosePricePairHighestPricePaid(Pair)) + { + Print("Opening order, averaging down."); + OpenOrder(Pair,Direction); + } + return; + } + } +//+------------------------------------------------------------------+ diff --git a/05-Biased-Martingale/Biased-Martingale.pdf b/05-Biased-Martingale/Biased-Martingale.pdf new file mode 100644 index 0000000..5a33d7c Binary files /dev/null and b/05-Biased-Martingale/Biased-Martingale.pdf differ diff --git a/06-Basket-Case/Basket-Case.mq4 b/06-Basket-Case/Basket-Case.mq4 new file mode 100644 index 0000000..e37a6fa --- /dev/null +++ b/06-Basket-Case/Basket-Case.mq4 @@ -0,0 +1,90 @@ +//+------------------------------------------------------------------+ +//| Basket Case.mq4 | +//| Matthew Kastor | +//| https://github.com/matthewkastor | +//+------------------------------------------------------------------+ +#property copyright "Matthew Kastor" +#property link "https://github.com/matthewkastor" +#property version "1.00" +#property strict +#include + +#property indicator_separate_window +#property indicator_buffers 1 +#property indicator_plots 1 +//--- plot Label1 +#property indicator_type1 DRAW_LINE +#property indicator_color1 clrDeepSkyBlue +#property indicator_style1 STYLE_SOLID +#property indicator_width1 1 +//--- indicator buffers +double IndexLineBuffer[]; + +extern int BarsLimit=5000; +extern color colorOne=clrDeepSkyBlue; //Line Color +extern string BasketSpecs1="EURUSDpro,sell,0.576"; //Pair,direction,weight;Pair,direction,weight +extern string BasketSpecs2=";USDJPYpro,buy,0.136"; //;Pair,direction,weight;Pair,direction,weight +extern string BasketSpecs3=";GBPUSDpro,sell,0.119"; //;Pair,direction,weight;Pair,direction,weight +extern string BasketSpecs4=";USDCADpro,buy,0.091"; //;Pair,direction,weight;Pair,direction,weight +extern string BasketSpecs5=";USDSEKpro,buy,0.042"; //;Pair,direction,weight;Pair,direction,weight +extern string BasketSpecs6=";USDCHFpro,buy,0.036"; //;Pair,direction,weight;Pair,direction,weight +extern string BasketSpecs7=""; //;Pair,direction,weight;Pair,direction,weight + +Basket *basket; +//+------------------------------------------------------------------+ +//| Custom indicator initialization function | +//+------------------------------------------------------------------+ +int OnInit() + { + IndicatorShortName("Basket Case"); + SetIndexBuffer(0,IndexLineBuffer); + SetIndexLabel(0,"Basket Points"); + SetIndexStyle(0,0,0,1,colorOne); + + string basketSpecs=StringConcatenate(BasketSpecs1,BasketSpecs2,BasketSpecs3,BasketSpecs4,BasketSpecs5,BasketSpecs6,BasketSpecs7); + basket=new Basket(basketSpecs); + if(!basket.ValidatePairsExist()) + { + return (INIT_FAILED); + } + return(INIT_SUCCEEDED); + } +//+------------------------------------------------------------------+ +//| Custom indicator iteration function | +//+------------------------------------------------------------------+ +int start() + { + int i=Bars-IndicatorCounted()-1; + + if(i>BarsLimit) + { + i=BarsLimit; + } + + double val=0; + + while(i>0) + { + val=basket.GetWeightedPoints(i); + if(val==0) + { + IndexLineBuffer[i]=EMPTY_VALUE; + } + else + { + IndexLineBuffer[i]=val; + } + + i--; + } + return (0); + } +//+------------------------------------------------------------------+ +//| | +//+------------------------------------------------------------------+ +int deinit() + { + delete basket; + return(0); + } +//+------------------------------------------------------------------+ diff --git a/06-Basket-Case/Basket-Case.pdf b/06-Basket-Case/Basket-Case.pdf new file mode 100644 index 0000000..ce2aec8 Binary files /dev/null and b/06-Basket-Case/Basket-Case.pdf differ diff --git a/07-MA-Cross/MA-Cross.mq4 b/07-MA-Cross/MA-Cross.mq4 new file mode 100644 index 0000000..f8b75c4 --- /dev/null +++ b/07-MA-Cross/MA-Cross.mq4 @@ -0,0 +1,165 @@ +//+------------------------------------------------------------------+ +//| MA Cross.mq4 | +//| Copyright 2013, Eugene Sia | +//| http://eugenesia.co.uk | +//+------------------------------------------------------------------+ + +/** + * This is a simple Metatrader 4 Expert Advisor I made to revise my + * MQL4, after a long hiatus. Hoping to re-explore automated forex + * trading! + * + * This EA trades based on a moving average crossovers, a common + * breakout strategy. When the short MA crosses the long MA, enter a + * trade. + */ + +#property copyright "Copyright 2013, Eugene Sia" +#property link "http://eugenesia.co.uk" + +//--- Constant definitions + +// Prefix a unique identifier e.g. MACROSS so we don't conflict with +// other predefined constants. + +// This defines the magic number for this EA. A magic number can be +// assigned to an order, so that orders opened by this EA have this magic +// number. This is how we distinguish between orders opened by this EA, +// and those opened by the user or other EAs. +// Ref: http://articles.mql4.com/145 +#define MACROSS_MAGIC_NUM 20130715 +#define MACROSS_OPEN_BUY_SIGNAL 1 +#define MACROSS_OPEN_SELL_SIGNAL -1 +#define MACROSS_NO_SIGNAL 0 + +//--- input parameters + +// extern keyword defines parameters that can be set by the user in the +// "Expert properties" dialog. +extern int ShortMaPeriod = 10; +extern int LongMaPeriod = 50; + +// These are in fractional pips, which are 0.1 of a pip. +extern int StopLoss = 500; +extern int TakeProfit = 1600; + +// Number of lots for each trade. +extern double Lots = 1; + +/** + * Get moving average values for the most recent price points. + * + * Params: + * maPeriod: period of the MA. + * numValues: Number of values to insert into the returned array. + * ma: returned array of MA values, with ma[0] being the value for the + * current price, ma[1] the value for the previous bar's price, etc. + * + */ +void MaRecentValues(double& ma[], int maPeriod, int numValues = 3) + { + // i is the index of the price array to calculate the MA value for. + // e.g. i=0 is the current price, i=1 is the previous bar's price. + for (int i=0; i < numValues; i++) + { + ma[i] = iMA(NULL,0,maPeriod,0,MODE_SMA,PRICE_CLOSE,i); + } + } + + +/** + * Check if we should open a trade. + * + * Returns: +1 to open a buy order, -1 to open a sell order, 0 for no action. + */ +int OpenSignal() + { + int signal = MACROSS_NO_SIGNAL; + + // Execute only on the first tick of a new bar, to avoid repeatedly + // opening orders when an open condition is satisfied. + if (Volume[0] > 1) return(0); + + //---- get Moving Average values + + double shortMa[3]; + MaRecentValues(shortMa, ShortMaPeriod, 3); + + double longMa[3]; + MaRecentValues(longMa, LongMaPeriod, 3); + + //---- buy conditions + if (shortMa[2] < longMa[2] + && shortMa[1] > longMa[1]) + { + signal = MACROSS_OPEN_BUY_SIGNAL; + } + + //---- sell conditions + if (shortMa[2] > longMa[2] + && shortMa[1] < longMa[1]) + { + signal = MACROSS_OPEN_SELL_SIGNAL; + } + + //---- + return(signal); + } + + + +//+------------------------------------------------------------------+ +//| expert initialization function | +//+------------------------------------------------------------------+ +int init() + { +//---- + + +//---- + return(0); + } +//+------------------------------------------------------------------+ +//| expert deinitialization function | +//+------------------------------------------------------------------+ +int deinit() + { +//---- + +//---- + return(0); + } +//+------------------------------------------------------------------+ +//| expert start function | +//+------------------------------------------------------------------+ +int start() + { +//---- + int signal = OpenSignal(); + + // Set slippage to a large enough number to avoid error 138 - quote + // outdated. + int slippage = 30; + + if (signal == MACROSS_OPEN_BUY_SIGNAL) + { + Print("Buy signal"); + OrderSend(Symbol(),OP_BUY,Lots,Bid,slippage, + Bid-StopLoss*Point, // Stop loss price. + Bid+TakeProfit*Point, // Take profit price. + NULL,MACROSS_MAGIC_NUM,0,Green); + } + + else if (signal == MACROSS_OPEN_SELL_SIGNAL) + { + Print("Sell signal"); + OrderSend(Symbol(),OP_SELL,Lots,Ask,slippage, + Ask+StopLoss*Point, // Stop loss price. + Ask-TakeProfit*Point, // Take profit price. + NULL,MACROSS_MAGIC_NUM,0,Red); + } + +//---- + return(0); + } +//+------------------------------------------------------------------+ \ No newline at end of file diff --git a/07-MA-Cross/MA-Cross.pdf b/07-MA-Cross/MA-Cross.pdf new file mode 100644 index 0000000..49ae2b5 Binary files /dev/null and b/07-MA-Cross/MA-Cross.pdf differ diff --git a/08-MA-Crossover-MT5/MA-Crossover-MT5.mq5 b/08-MA-Crossover-MT5/MA-Crossover-MT5.mq5 new file mode 100644 index 0000000..8079a23 --- /dev/null +++ b/08-MA-Crossover-MT5/MA-Crossover-MT5.mq5 @@ -0,0 +1,78 @@ +/** + * @copyright 2019, pipbolt.io + * @license https://github.com/pipbolt/experts/blob/master/LICENSE + */ + +#include + +#define NAME "Moving Average Cross EA" +#define VERSION "0.022" + +#property copyright COPYRIGHT +#property link LINK +#property icon ICON +#property description DESCRIPTION +#property version VERSION + +#include + +input group "Entry Strategy"; + +input group "Exit Strategy"; +input bool UseExitStrategy = false; // Use Exit Strategy + +input group "Moving Averages"; + +input string Fast_Moving_Average = "----------"; // ---------- Fast Moving Average ---------- +input int MaFastPeriod = 12; // Moving Average Period +input ENUM_MA_METHOD MaFastMethod = MODE_SMA; // Method +input ENUM_APPLIED_PRICE MaFastAppliedPrice = PRICE_CLOSE; // Applied Price + +input string Slow_Moving_Average = "----------"; // ---------- Slow Moving Average ---------- +input int MaSlowPeriod = 30; // Moving Average Period +input ENUM_MA_METHOD MaSlowMethod = MODE_SMA; // Method +input ENUM_APPLIED_PRICE MaSlowAppliedPrice = PRICE_CLOSE; // Applied Price + +#include + +CiMA MAFast; +CiMA MASlow; + +int OnInit(void) +{ + if (ONINIT() != INIT_SUCCEEDED) + return INIT_FAILED; + + MAFast.Init(MaFastPeriod, 0, MaFastMethod, MaFastAppliedPrice); + MASlow.Init(MaSlowPeriod, 0, MaSlowMethod, MaSlowAppliedPrice); + + return INIT_SUCCEEDED; +} + +void OnTick(void) { ONTICK(); } +void OnDeinit(const int reason) { ONDEINIT(reason); } +void OnTimer() { ONTIMER(); } + +void CheckForOpen(bool &openBuy, bool &openSell) +{ + // Buy Entry Strategy + if (MAFast.Main(1) < MASlow.Main(1) && MAFast.Main(0) >= MASlow.Main(0)) + openBuy = true; + + // Sell Entry Strategy + else if (MAFast.Main(1) > MASlow.Main(1) && MAFast.Main(0) <= MASlow.Main(0)) + openSell = true; + + // Apply MA Filter + openBuy = openBuy && MAFilter.Check(DIR_BUY); + openSell = openSell && MAFilter.Check(DIR_SELL); +} + +void CheckForClose(bool &closeBuy, bool &closeSell) +{ + // Buy Exit Strategy + closeBuy = (MAFast.Main(1) > MASlow.Main(1) && MAFast.Main(0) <= MASlow.Main(0)); + + // Sell Exit Strategy + closeSell = (MAFast.Main(1) < MASlow.Main(1) && MAFast.Main(0) >= MASlow.Main(0)); +} diff --git a/08-MA-Crossover-MT5/MA-Crossover-MT5.pdf b/08-MA-Crossover-MT5/MA-Crossover-MT5.pdf new file mode 100644 index 0000000..fd96605 Binary files /dev/null and b/08-MA-Crossover-MT5/MA-Crossover-MT5.pdf differ diff --git a/09-Moving-Average-EA/Moving-Average-EA.mq5 b/09-Moving-Average-EA/Moving-Average-EA.mq5 new file mode 100644 index 0000000..07863e0 --- /dev/null +++ b/09-Moving-Average-EA/Moving-Average-EA.mq5 @@ -0,0 +1,74 @@ +/** + * @copyright 2019, pipbolt.io + * @license https://github.com/pipbolt/experts/blob/master/LICENSE + */ + +#include + +#define NAME "Moving Average EA" +#define VERSION "0.022" + +#property copyright COPYRIGHT +#property link LINK +#property icon ICON +#property description DESCRIPTION +#property version VERSION + +#include + +input group "Entry Strategy"; + +input group "Exit Strategy"; +input bool UseExitStrategy = false; // Use Exit Strategy + +input group "Moving Average"; +input int MaPeriod = 10; // Period +input ENUM_MA_METHOD MaMethod = MODE_SMA; // Method +input ENUM_APPLIED_PRICE MaAppliedPrice = PRICE_CLOSE; // Applied Price + +#include + +CiMA MA; + +int OnInit(void) +{ + if (ONINIT() != INIT_SUCCEEDED) + return INIT_FAILED; + + MA.Init(MaPeriod, 0, MaMethod, MaAppliedPrice); + + return INIT_SUCCEEDED; +} + +void OnTick(void) { ONTICK(); } +void OnDeinit(const int reason) { ONDEINIT(reason); } +void OnTimer() { ONTIMER(); } + +void CheckForOpen(bool &openBuy, bool &openSell) +{ + // Close prices + double close0 = iClose(NULL, NULL, _indicatorShift + 0); + double close1 = iClose(NULL, NULL, _indicatorShift + 1); + + // Buy Entry Strategy + openBuy = (close0 > MA.Main(0) && close1 <= MA.Main(1)); + + // Sell Entry Strategy + openSell = (close0 < MA.Main(0) && close1 >= MA.Main(1)); + + // Apply MA Filter + openBuy = openBuy && MAFilter.Check(DIR_BUY); + openSell = openSell && MAFilter.Check(DIR_SELL); +} + +void CheckForClose(bool &closeBuy, bool &closeSell) +{ + // Close price + double close0 = iClose(NULL, NULL, _indicatorShift + 0); + + // Buy Exit Strategy + closeBuy = (close0 < MA.Main(0)); + + // Sell Exit Strategy + closeSell = (close0 > MA.Main(0)); +} diff --git a/09-Moving-Average-EA/Moving-Average-EA.pdf b/09-Moving-Average-EA/Moving-Average-EA.pdf new file mode 100644 index 0000000..0d24240 Binary files /dev/null and b/09-Moving-Average-EA/Moving-Average-EA.pdf differ diff --git a/10-Spike-Trader/Spike-Trader.mq4 b/10-Spike-Trader/Spike-Trader.mq4 new file mode 100644 index 0000000..c34bf92 --- /dev/null +++ b/10-Spike-Trader/Spike-Trader.mq4 @@ -0,0 +1,169 @@ +//+------------------------------------------------------------------+ +//| SpikeTrader.mq4 | +//| Copyright © 2012-2022, EarnForex.com | +//| https://www.earnforex.com/ | +//+------------------------------------------------------------------+ +#property copyright "Copyright © 2012-2022, EarnForex" +#property link "https://www.earnforex.com/metatrader-expert-advisors/Spike-Trader/" +#property version "1.01" +#property strict + +#property description "Trades on spikes that are:" +#property description "1) Higher/lower than N preceding bars;" +#property description "2) Higher/lower than the previous bar by X percent;" +#property description "3) Close in bottom or upper third/half of the bar." + +input group "Main" +input int Hold = 11; // Hold: Position holding time in bars. +input int BarsNumber = 3; // BarsNumber: N preceding bars to check. +input double PercentageDifference = 0.003; // PercentageDifference1: X percentage for bar comparison. +input double ThirdOrHalf = 0.5; // ThirdOrHalf: Top/bottom share of a bar to close in. +input group "Money management" +input double Lots = 0.1; +input group "Miscellaneous" +input int Slippage = 30; +input string OrderCommentary = "Spike Trader"; +input int Magic = 173923183; + +int LastBars = 0; +int Timer = 0; + +void OnTick() +{ + if ((!IsTradeAllowed()) || (IsTradeContextBusy()) || (!IsConnected()) || ((!MarketInfo(Symbol(), MODE_TRADEALLOWED)) && (!IsTesting()))) return; + + //Wait for the new Bar in a chart. + if (LastBars == Bars) return; + else LastBars = Bars; + + if (Timer == 1) ClosePrev(); + if (Timer > 0) Timer--; + + CheckEntry(); +} + +//+------------------------------------------------------------------+ +//| Check for entry conditions and trade if necessary. | +//+------------------------------------------------------------------+ +void CheckEntry() +{ + // Empty bar. + if (High[1] - Low[1] == 0) return; + + if (CheckSellEntry()) + { + // If found a BUY order, close it and open a SELL. Otherwise, only reset timer. + if (ClosePrev(OP_SELL)) fSell(); + Timer = Hold; + } + else if (CheckBuyEntry()) + { + // If found a SELL order, close it and open a BUY. Otherwise, only reset timer. + if (ClosePrev(OP_BUY)) fBuy(); + Timer = Hold; + } +} + +bool CheckSellEntry() +{ + // If the bar isn't higher than at least one of the previous bars - return false. + for (int i = 2; i < BarsNumber + 2; i++) + if (High[1] <= High[i]) return false; + + // If not higher than the previous bar by required percentage difference - return false. + if ((High[1] - High[2]) / High[2] < PercentageDifference) return false; + + // If closed above the lower third/half - return false. + if ((Close[1] - Low[1]) / (High[1] - Low[1]) > ThirdOrHalf) return false; + + // Passed all tests. + return true; +} + +bool CheckBuyEntry() +{ + // If the bar isn't lower than at least one of the previous bars - return false. + for (int i = 2; i < BarsNumber + 2; i++) + if (Low[1] >= Low[i]) return false; + + // If not lower than the previous bar by required percentage difference - return false. + if ((Low[2] - Low[1]) / Low[2] < PercentageDifference) return false; + + // If closed below the upper third/half - return false. + if ((High[1] - Close[1]) / (High[1] - Low[1]) > ThirdOrHalf) return false; + + // Passed all tests. + return true; +} + +//+------------------------------------------------------------------+ +//| Close previous position. | +//| order_type - skip positions of this directions. | +//+------------------------------------------------------------------+ +bool ClosePrev(int order_type = -1) +{ + int total = OrdersTotal(); + for (int i = total - 1; i >= 0; i--) + { + if (OrderSelect(i, SELECT_BY_POS) == false) continue; + if ((OrderSymbol() == Symbol()) && (OrderMagicNumber() == Magic)) + { + if (OrderType() == OP_BUY) + { + if (order_type == OP_BUY) return false; + RefreshRates(); + if (!OrderClose(OrderTicket(), OrderLots(), Bid, Slippage)) + { + int e = GetLastError(); + Print("OrderClose Error: ", e); + } + return true; + } + else if (OrderType() == OP_SELL) + { + if (order_type == OP_SELL) return false; + RefreshRates(); + if (!OrderClose(OrderTicket(), OrderLots(), Ask, Slippage)) + { + int e = GetLastError(); + Print("OrderClose Error: ", e); + } + return true; + } + } + } + return true; +} + +//+------------------------------------------------------------------+ +//| Sell | +//+------------------------------------------------------------------+ +int fSell() +{ + RefreshRates(); + int result = OrderSend(Symbol(), OP_SELL, Lots, Bid, Slippage, 0, 0, OrderCommentary, Magic); + if (result == -1) + { + int e = GetLastError(); + Print("OrderSend Error: ", e); + } + else return result; + return 0; +} + +//+------------------------------------------------------------------+ +//| Buy | +//+------------------------------------------------------------------+ +int fBuy() +{ + RefreshRates(); + int result = OrderSend(Symbol(), OP_BUY, Lots, Ask, Slippage, 0, 0, OrderCommentary, Magic); + if (result == -1) + { + int e = GetLastError(); + Print("OrderSend Error: ", e); + } + else return result; + return 0; +} +//+------------------------------------------------------------------+ \ No newline at end of file diff --git a/10-Spike-Trader/Spike-Trader.pdf b/10-Spike-Trader/Spike-Trader.pdf new file mode 100644 index 0000000..d7a6dc8 Binary files /dev/null and b/10-Spike-Trader/Spike-Trader.pdf differ diff --git a/11-Donchian-Turtle/Donchian-Turtle.mq5 b/11-Donchian-Turtle/Donchian-Turtle.mq5 new file mode 100644 index 0000000..5ee84ca --- /dev/null +++ b/11-Donchian-Turtle/Donchian-Turtle.mq5 @@ -0,0 +1,519 @@ +//+------------------------------------------------------------------+ +//| DonchianTurtle_v3_Consensus.mq5 | +//| EA v4 — Donchian Turtle + QuantAgent-Inspired Consensus Filter | +//| | +//| Base: V3.13 (Volatility Scaling + CSV Logging) | +//| New: Consensus gate — RSI + MACD + LinReg must agree | +//| Inspired by QuantAgent paper (arXiv:2509.09995) | +//| | +//| Validated params (Python IS/OOS + MT5 86% quality OOS): | +//| S1: Donchian(20/8), S2: Donchian(40/8) | +//| ADX>20, SL=2.0xATR(20), Vol Scaling ON | +//| Consensus: RSI>50 + MACD cross + Price>LinReg(50) | +//| Scorecard: 81/100 Grade B (OOS 2023-2025) | +//+------------------------------------------------------------------+ +#property copyright "EA v4 — Turtle Consensus" +#property version "4.00" + +#include + +//--- System 1 +input group "=== System 1 (Donchian 20) ===" +input int S1_EntryPeriod = 20; +input int S1_ExitPeriod = 8; // Updated: was 10 +input double S1_RiskPct = 0.5; + +//--- System 2 +input group "=== System 2 (Donchian 40) ===" +input int S2_EntryPeriod = 40; // Updated: was 55 +input int S2_ExitPeriod = 8; // Updated: was 10 +input double S2_RiskPct = 0.5; + +//--- ATR / Base Filters +input group "=== Filters ===" +input int ADX_Period = 20; +input double ADX_MinLevel = 20.0; // Updated: was 25 +input int ATR_Period = 20; +input double ATR_StopMult = 2.0; +input int MA_Period = 200; +input double ATR_SpikeMult = 3.0; +input double MaxDrawdownPct = 20.0; + +//--- Break-Even + Trailing +input group "=== Break-Even + Trailing Stop ===" +input bool UseBreakEven = true; +input double BE_RMultiple = 1.0; +input bool UseTrailing = true; +input double Trail_RMultiple = 2.0; +input double Trail_ATRMult = 1.5; + +//--- Volatility Scaling +input group "=== Volatility Scaling ===" +input bool UseVolScaling = true; +input int VolScale_Period = 252; +input double VolScale_LowPct = 0.33; +input double VolScale_HighPct = 0.67; +input double VolScale_LowMult = 1.5; +input double VolScale_HighMult = 0.5; + +//--- Consensus Filter (QuantAgent-Inspired) +input group "=== Consensus Filter (QuantAgent-Inspired) ===" +input bool UseConsensus = true; // เปิด/ปิด consensus gate +input int ConsensusMin = 2; // ต้องผ่านอย่างน้อยกี่ conditions (max=3) +// Condition 1: RSI momentum +input int RSI_Period = 14; +input double RSI_BullLevel = 50.0; // RSI > 50 = bullish +// Condition 2: MACD direction +input int MACD_Fast = 12; +input int MACD_Slow = 26; +input int MACD_Signal = 9; +// Condition 3: Price vs OLS trend line +input int LinReg_Period = 50; // Linear regression period (TrendAgent) + +//--- Magic Numbers +input group "=== Order Settings ===" +input int MagicS1 = 202901; // New magic (v4) +input int MagicS2 = 202902; +input string TradeComment = "Turtle_v4_Consensus"; + +//+------------------------------------------------------------------+ +//--- Globals +CTrade trade; +int g_hATR = INVALID_HANDLE; +int g_hADX = INVALID_HANDLE; +int g_hMA = INVALID_HANDLE; +int g_hRSI = INVALID_HANDLE; +int g_hMACD = INVALID_HANDLE; +int g_hLR = INVALID_HANDLE; // Linear Regression handle + +double g_AccountPeak = 0; +datetime g_LastBarTime = 0; +int g_hLog = INVALID_HANDLE; +double g_lastVolMult = 1.0; +double g_lastATRpct = -1.0; +int g_lastConsensus = 0; + +//+------------------------------------------------------------------+ +int OnInit() +{ + // Base indicators + g_hATR = iATR(Symbol(), PERIOD_D1, ATR_Period); + g_hADX = iADX(Symbol(), PERIOD_D1, ADX_Period); + g_hMA = iMA(Symbol(), PERIOD_D1, MA_Period, 0, MODE_SMA, PRICE_CLOSE); + + // Consensus indicators + g_hRSI = iRSI(Symbol(), PERIOD_D1, RSI_Period, PRICE_CLOSE); + g_hMACD = iMACD(Symbol(), PERIOD_D1, MACD_Fast, MACD_Slow, MACD_Signal, PRICE_CLOSE); + g_hLR = iMA(Symbol(), PERIOD_D1, LinReg_Period, 0, MODE_SMA, PRICE_CLOSE); // SMA50 as trend proxy (iLinReg not in MQL5 std) + + if(g_hATR == INVALID_HANDLE || g_hADX == INVALID_HANDLE || + g_hMA == INVALID_HANDLE || g_hRSI == INVALID_HANDLE || + g_hMACD == INVALID_HANDLE || g_hLR == INVALID_HANDLE) + { + Print("ERROR: Failed to create indicator handles"); + return INIT_FAILED; + } + + // Warmup (skip in backtester) + bool inTester = (bool)MQLInfoInteger(MQL_TESTER); + if(!inTester) + { + double dummy[1]; + int attempts = 0; + while(CopyBuffer(g_hMA, 0, 1, 1, dummy) <= 0 && attempts < 100) + { + Sleep(100); + attempts++; + } + if(attempts >= 100) + { + Print("ERROR: Indicators not ready."); + return INIT_FAILED; + } + } + + trade.SetDeviationInPoints(50); + trade.SetTypeFilling(ORDER_FILLING_IOC); + g_AccountPeak = AccountInfoDouble(ACCOUNT_BALANCE); + + // CSV log + string fname = "TurtleConsensus_" + Symbol() + "_trades.csv"; + g_hLog = FileOpen(fname, FILE_WRITE|FILE_READ|FILE_CSV|FILE_ANSI|FILE_SHARE_READ, ','); + if(g_hLog == INVALID_HANDLE) + Print("WARNING: Cannot open log file"); + else + { + if(FileTell(g_hLog) == 0) + FileWrite(g_hLog, + "Timestamp","Event","System","Magic", + "Lots","Price","SL","RiskPct", + "ATR_pct","VolMult","Consensus","PnL","Balance","Note"); + FileSeek(g_hLog, 0, SEEK_END); + FileFlush(g_hLog); + } + + PrintFormat("DonchianTurtle v4 Consensus | %s D1 | S1:%d/%d S2:%d/%d ADX>%.0f SL=%.1fx ConsMin=%d", + Symbol(), S1_EntryPeriod, S1_ExitPeriod, + S2_EntryPeriod, S2_ExitPeriod, + ADX_MinLevel, ATR_StopMult, ConsensusMin); + return INIT_SUCCEEDED; +} + +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) +{ + IndicatorRelease(g_hATR); + IndicatorRelease(g_hADX); + IndicatorRelease(g_hMA); + IndicatorRelease(g_hRSI); + IndicatorRelease(g_hMACD); + IndicatorRelease(g_hLR); + if(g_hLog != INVALID_HANDLE) { FileFlush(g_hLog); FileClose(g_hLog); } +} + +//+------------------------------------------------------------------+ +void OnTick() +{ + // Trail runs every tick + double atrNow[1]; + double atr = 0; + if(CopyBuffer(g_hATR, 0, 0, 1, atrNow) > 0) atr = atrNow[0]; + if(atr > 0) + { + ManageTrail(MagicS1, atr); + ManageTrail(MagicS2, atr); + } + + // New bar check + datetime barTime = iTime(Symbol(), PERIOD_D1, 0); + if(barTime == g_LastBarTime) return; + g_LastBarTime = barTime; + + double balance = AccountInfoDouble(ACCOUNT_BALANCE); + if(balance > g_AccountPeak) g_AccountPeak = balance; + + // DD halt + if(g_AccountPeak > 0) + { + double dd = (g_AccountPeak - balance) / g_AccountPeak * 100.0; + if(dd >= MaxDrawdownPct) + { + PrintFormat("DD HALT: %.1f%% >= %.1f%%", dd, MaxDrawdownPct); + return; + } + } + + // Read base indicators (bar[1] = last completed bar) + double atrBuf[1], adxBuf[1], maBuf[1]; + if(CopyBuffer(g_hATR, 0, 1, 1, atrBuf) <= 0) return; + if(CopyBuffer(g_hADX, 0, 1, 1, adxBuf) <= 0) return; + if(CopyBuffer(g_hMA, 0, 1, 1, maBuf) <= 0) return; + + double atrD1 = atrBuf[0]; + double adx = adxBuf[0]; + double ma200 = maBuf[0]; + double close1 = iClose(Symbol(), PERIOD_D1, 1); + + if(atrD1 <= 0 || adx <= 0 || ma200 <= 0 || close1 <= 0) return; + + // ATR spike filter + double atrArr[20]; + double atrAvg = 0; + if(CopyBuffer(g_hATR, 0, 1, 20, atrArr) == 20) + { + for(int k = 0; k < 20; k++) atrAvg += atrArr[k]; + atrAvg /= 20.0; + } + if(atrAvg > 0 && atrD1 > atrAvg * ATR_SpikeMult) return; + + // Donchian exits + ManageExits(MagicS1, S1_ExitPeriod); + ManageExits(MagicS2, S2_ExitPeriod); + + // Base entry filters + if(close1 <= ma200) return; // Below MA200 + if(adx < ADX_MinLevel) return; // Weak trend + + // Consensus check (QuantAgent-inspired) + g_lastConsensus = 0; + if(UseConsensus) + { + g_lastConsensus = GetConsensusScore(); + if(g_lastConsensus < ConsensusMin) + { + PrintFormat("Consensus FAIL: score=%d/%d (need %d) — skip entry", + g_lastConsensus, 3, ConsensusMin); + return; + } + PrintFormat("Consensus PASS: score=%d/3", g_lastConsensus); + } + + // Volatility scaling + double scaledRiskS1 = S1_RiskPct; + double scaledRiskS2 = S2_RiskPct; + g_lastVolMult = 1.0; + g_lastATRpct = -1.0; + if(UseVolScaling) + { + double atrPct = GetATRPercentile(VolScale_Period, 1); + double mult = 1.0; + if(atrPct >= 0 && atrPct < VolScale_LowPct) mult = VolScale_LowMult; + else if(atrPct > VolScale_HighPct) mult = VolScale_HighMult; + scaledRiskS1 = S1_RiskPct * mult; + scaledRiskS2 = S2_RiskPct * mult; + g_lastVolMult = mult; + g_lastATRpct = atrPct; + } + + // Entries + if(!HasPosition(MagicS1)) + TryEntry(MagicS1, S1_EntryPeriod, scaledRiskS1, atrD1, close1, "S1"); + if(!HasPosition(MagicS2)) + TryEntry(MagicS2, S2_EntryPeriod, scaledRiskS2, atrD1, close1, "S2"); +} + +//+------------------------------------------------------------------+ +//| Consensus Score — 3 conditions from QuantAgent | +//| Returns 0-3. Called after base filters pass. | +//+------------------------------------------------------------------+ +int GetConsensusScore() +{ + int score = 0; + + // Condition 1: RSI(14) > 50 — bullish momentum (IndicatorAgent) + double rsiBuf[1]; + if(CopyBuffer(g_hRSI, 0, 1, 1, rsiBuf) > 0) + { + if(rsiBuf[0] > RSI_BullLevel) + { + score++; + PrintFormat(" [C1] RSI=%.1f > %.1f PASS", rsiBuf[0], RSI_BullLevel); + } + else + PrintFormat(" [C1] RSI=%.1f <= %.1f FAIL", rsiBuf[0], RSI_BullLevel); + } + + // Condition 2: MACD line > Signal line — directional confirm (IndicatorAgent) + double macdMain[1], macdSig[1]; + if(CopyBuffer(g_hMACD, MAIN_LINE, 1, 1, macdMain) > 0 && + CopyBuffer(g_hMACD, SIGNAL_LINE, 1, 1, macdSig) > 0) + { + if(macdMain[0] > macdSig[0]) + { + score++; + PrintFormat(" [C2] MACD=%.4f > Signal=%.4f PASS", macdMain[0], macdSig[0]); + } + else + PrintFormat(" [C2] MACD=%.4f <= Signal=%.4f FAIL", macdMain[0], macdSig[0]); + } + + // Condition 3: Price above OLS Linear Regression line — trend bias (TrendAgent) + double lrBuf[1]; + double close1 = iClose(Symbol(), PERIOD_D1, 1); + if(CopyBuffer(g_hLR, 0, 1, 1, lrBuf) > 0) + { + if(close1 > lrBuf[0]) + { + score++; + PrintFormat(" [C3] Close=%.2f > LinReg=%.2f PASS", close1, lrBuf[0]); + } + else + PrintFormat(" [C3] Close=%.2f <= LinReg=%.2f FAIL", close1, lrBuf[0]); + } + + return score; +} + +//+------------------------------------------------------------------+ +void TryEntry(int magic, int period, double riskPct, double atr, + double close1, string label) +{ + // Donchian entry band (bars 2..period+1, shift=2 matching MQL5 convention) + int hiIdx = iHighest(Symbol(), PERIOD_D1, MODE_HIGH, period, 2); + if(hiIdx < 0) return; + double prevBand = iHigh(Symbol(), PERIOD_D1, hiIdx); + + if(close1 <= prevBand) return; // No breakout + + double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK); + double sl = ask - atr * ATR_StopMult; + double lots = CalcLots(ask, sl, riskPct); + if(lots <= 0) return; + + trade.SetExpertMagicNumber(magic); + if(trade.Buy(lots, Symbol(), ask, sl, 0, TradeComment + "_" + label)) + { + PrintFormat("%s ENTRY | Ask=%.2f SL=%.2f Lots=%.2f Band=%.2f Vol=%.1f Cons=%d/3", + label, ask, sl, lots, prevBand, g_lastVolMult, g_lastConsensus); + if(g_hLog != INVALID_HANDLE) + { + FileWrite(g_hLog, + TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS), + "ENTRY", label, magic, + DoubleToString(lots, 2), + DoubleToString(ask, 2), + DoubleToString(sl, 2), + DoubleToString(riskPct, 3), + DoubleToString(g_lastATRpct, 3), + DoubleToString(g_lastVolMult, 2), + IntegerToString(g_lastConsensus), + "", + DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2), + ""); + FileFlush(g_hLog); + } + } + else + PrintFormat("%s FAIL | code=%d %s", label, + trade.ResultRetcode(), trade.ResultRetcodeDescription()); +} + +//+------------------------------------------------------------------+ +void ManageTrail(int magic, double atr) +{ + if(!UseBreakEven && !UseTrailing) return; + if(atr <= 0) return; + + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + ulong ticket = PositionGetTicket(i); + if(!PositionSelectByTicket(ticket)) continue; + if(PositionGetInteger(POSITION_MAGIC) != magic) continue; + if(PositionGetString(POSITION_SYMBOL) != Symbol()) continue; + if(PositionGetInteger(POSITION_TYPE) != POSITION_TYPE_BUY) continue; + + double entry = PositionGetDouble(POSITION_PRICE_OPEN); + double curSL = PositionGetDouble(POSITION_SL); + double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID); + double initRisk = atr * ATR_StopMult; + double profit = bid - entry; + double newSL = curSL; + + if(UseBreakEven && profit >= BE_RMultiple * initRisk) + { + double beLevel = entry + 2 * SymbolInfoDouble(Symbol(), SYMBOL_POINT); + if(beLevel > curSL) newSL = MathMax(newSL, beLevel); + } + + if(UseTrailing && profit >= Trail_RMultiple * initRisk) + { + double trailLevel = bid - atr * Trail_ATRMult; + if(trailLevel > curSL) newSL = MathMax(newSL, trailLevel); + } + + if(newSL > curSL + SymbolInfoDouble(Symbol(), SYMBOL_POINT)) + { + double tp = PositionGetDouble(POSITION_TP); + trade.SetExpertMagicNumber(magic); + trade.PositionModify(ticket, NormalizeDouble(newSL, _Digits), tp); + } + } +} + +//+------------------------------------------------------------------+ +void ManageExits(int magic, int exitPeriod) +{ + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + ulong ticket = PositionGetTicket(i); + if(!PositionSelectByTicket(ticket)) continue; + if(PositionGetInteger(POSITION_MAGIC) != magic) continue; + if(PositionGetString(POSITION_SYMBOL) != Symbol()) continue; + + int loIdx = iLowest(Symbol(), PERIOD_D1, MODE_LOW, exitPeriod, 1); + if(loIdx < 0) continue; + double exitLow = iLow(Symbol(), PERIOD_D1, loIdx); + double close1 = iClose(Symbol(), PERIOD_D1, 1); + + if(close1 < exitLow) + { + trade.SetExpertMagicNumber(magic); + trade.PositionClose(ticket); + } + } +} + +//+------------------------------------------------------------------+ +void OnTradeTransaction(const MqlTradeTransaction& trans, + const MqlTradeRequest& request, + const MqlTradeResult& result) +{ + if(trans.type != TRADE_TRANSACTION_DEAL_ADD) return; + if(g_hLog == INVALID_HANDLE) return; + if(!HistoryDealSelect(trans.deal)) return; + + long dealEntry = HistoryDealGetInteger(trans.deal, DEAL_ENTRY); + if(dealEntry != DEAL_ENTRY_OUT && dealEntry != DEAL_ENTRY_INOUT) return; + + long magic = HistoryDealGetInteger(trans.deal, DEAL_MAGIC); + if(magic != MagicS1 && magic != MagicS2) return; + + string system = (magic == MagicS1) ? "S1" : "S2"; + double profit = HistoryDealGetDouble(trans.deal, DEAL_PROFIT) + + HistoryDealGetDouble(trans.deal, DEAL_SWAP) + + HistoryDealGetDouble(trans.deal, DEAL_COMMISSION); + double price = HistoryDealGetDouble(trans.deal, DEAL_PRICE); + double lots = HistoryDealGetDouble(trans.deal, DEAL_VOLUME); + string outcome = (profit >= 0) ? "WIN" : "LOSS"; + + FileWrite(g_hLog, + TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS), + "EXIT_" + outcome, system, magic, + DoubleToString(lots, 2), + DoubleToString(price, 2), + "", "", "", "", "", + DoubleToString(profit, 2), + DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2), + outcome); + FileFlush(g_hLog); +} + +//+------------------------------------------------------------------+ +bool HasPosition(int magic) +{ + for(int i = 0; i < PositionsTotal(); i++) + { + ulong ticket = PositionGetTicket(i); + if(!PositionSelectByTicket(ticket)) continue; + if(PositionGetInteger(POSITION_MAGIC) == magic && + PositionGetString(POSITION_SYMBOL) == Symbol()) + return true; + } + return false; +} + +//+------------------------------------------------------------------+ +double GetATRPercentile(int period, int shift) +{ + double atrArr[]; + ArraySetAsSeries(atrArr, true); + int copied = CopyBuffer(g_hATR, 0, shift, period, atrArr); + if(copied < period) return -1.0; + double curATR = atrArr[0]; + int rank = 0; + for(int i = 1; i < period; i++) + if(atrArr[i] < curATR) rank++; + return (double)rank / (double)(period - 1); +} + +//+------------------------------------------------------------------+ +double CalcLots(double entry, double sl, double riskPct) +{ + double balance = AccountInfoDouble(ACCOUNT_BALANCE); + double riskAmt = balance * riskPct / 100.0; + double slDist = MathAbs(entry - sl); + if(slDist <= 0) return 0; + + double tickVal = SymbolInfoDouble(Symbol(), SYMBOL_TRADE_TICK_VALUE); + double tickSize = SymbolInfoDouble(Symbol(), SYMBOL_TRADE_TICK_SIZE); + if(tickSize <= 0 || tickVal <= 0) return 0; + + double lots = riskAmt / ((slDist / tickSize) * tickVal); + double step = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_STEP); + double minL = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN); + double maxL = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MAX); + + lots = MathFloor(lots / step) * step; + return MathMax(minL, MathMin(maxL, lots)); +} +//+------------------------------------------------------------------+ diff --git a/11-Donchian-Turtle/Donchian-Turtle.pdf b/11-Donchian-Turtle/Donchian-Turtle.pdf new file mode 100644 index 0000000..0bfe6ef Binary files /dev/null and b/11-Donchian-Turtle/Donchian-Turtle.pdf differ diff --git a/12-TrendEngine/TrendEngine.mq5 b/12-TrendEngine/TrendEngine.mq5 new file mode 100644 index 0000000..af1c330 --- /dev/null +++ b/12-TrendEngine/TrendEngine.mq5 @@ -0,0 +1,38 @@ +//+------------------------------------------------------------------+ +//| TrendEngine Core Demo – MQL5 EA | +//| Author: Hossein Asgari (Fintor AI) | +//+------------------------------------------------------------------+ +#property strict +#property copyright "Hossein Asgari - Fintor AI" +#property link "https://fintorai.com" +#property version "1.00" + +#include + +//--- inputs +input double InpLots = 0.10; +input int InpMaFast = 20; +input int InpMaSlow = 50; +input int InpSlPoints = 300; +input int InpTpPoints = 600; + +//--- core engine +CTrendEngineCore g_engine; + +//+------------------------------------------------------------------+ +int OnInit() + { + g_engine.Init(InpLots,InpMaFast,InpMaSlow,InpSlPoints,InpTpPoints); + return(INIT_SUCCEEDED); + } +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) + { + g_engine.Deinit(); + } +//+------------------------------------------------------------------+ +void OnTick() + { + g_engine.OnTick(); + } +//+------------------------------------------------------------------+ diff --git a/12-TrendEngine/TrendEngine.pdf b/12-TrendEngine/TrendEngine.pdf new file mode 100644 index 0000000..1d26a5f Binary files /dev/null and b/12-TrendEngine/TrendEngine.pdf differ diff --git a/13-Trend-Follower/Trend-Follower.mq4 b/13-Trend-Follower/Trend-Follower.mq4 new file mode 100644 index 0000000..e1afaf9 --- /dev/null +++ b/13-Trend-Follower/Trend-Follower.mq4 @@ -0,0 +1,106 @@ +#include +#include + +#property copyright "Copyright 2023, MetaQuotes Ltd." +#property link "https://www.mql5.com" +#property version "1.00" +#property strict + +// config parameters +double stopLoss = 2.5; +double takeProfit = 3; +double Lots = 0.01; +int waitForOrderTime = 60; // seconds + +// global variables +double tickPrices[]; +int tickCounts = 0; +datetime lastOrderOpenTime = 0; + + +int OnInit() +{ + // create zero array for tickPrices + ArraySetAsSeries(tickPrices, true); + ArrayResize(tickPrices, stdTicksNumbers); + ArraySetAsSeries(tickPrices, false); + ArrayInitialize(tickPrices, 0.0); + + return(INIT_SUCCEEDED); +} + +void OnDeinit(const int reason) +{ +} + + +void PlaceOrderBuy() +{ + double orderPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); + double tp = orderPrice + takeProfitDistance; + double sl = orderPrice - stopLossDistance; + int result = OrderSend(_Symbol, OP_BUY, Lots, orderPrice , 10, sl, tp, "Buy Order", 0, 0, clrGreen); + if (result > 0){ + Print("Buy order placed. Ticket: ", result); + } + else{ + int error = GetLastError(); + string errorDescription = ErrorDescription(error); + Print("OrderSend failed with error #", error, ": ", errorDescription); + } +} + +void PlaceOrderSell() +{ + double orderPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); + double tp = orderPrice - takeProfitDistance; + double sl = orderPrice + stopLossDistance; + int result = OrderSend(_Symbol, OP_SELL, Lots, orderPrice , 10, sl, tp, "Sell Order", 0, 0, clrRed); + if (result > 0){ + Print("Sell order placed. Ticket: ", result); + } + else{ + int error = GetLastError(); + string errorDescription = ErrorDescription(error); + Print("OrderSend failed with error #", error, ": ", errorDescription); + } +} + +void updateTickData() +{ + tickCounts ++; + // shift + for(int i = stdTicksNumbers - 1; i > 0; i--) + tickPrices[i] = tickPrices[i-1]; + tickPrices[0] = (Bid + Ask) / 2.0; +} + +double getSlope(const double &array[]){ + return 0; +} +bool BuyIsOK(){ + +} + +bool SellIsOK(){ +} + +void OnTick() +{ + updateTickData(); + datetime currentTime = iTime(NULL, 0, 0); + int timeDifferenceSinceLastOrder = currentTime - lastOrderOpenTime; + if (timeDifferenceSinceLastOrder > waitForOrderTime || lastOrderOpenTime == 0) + { + if (BuyIsOK()) + { + PlaceOrderBuy(); + lastOrderOpenTime = iTime(NULL, 0, 0); + } + else if (SellIsOK()) + { + PlaceOrderSell(); + lastOrderOpenTime = iTime(NULL, 0, 0); + } + } +} \ No newline at end of file diff --git a/13-Trend-Follower/Trend-Follower.pdf b/13-Trend-Follower/Trend-Follower.pdf new file mode 100644 index 0000000..e0a8f84 Binary files /dev/null and b/13-Trend-Follower/Trend-Follower.pdf differ diff --git a/14-EMA-Trend-EA/EMA-Trend-EA.mq5 b/14-EMA-Trend-EA/EMA-Trend-EA.mq5 new file mode 100644 index 0000000..4840624 Binary files /dev/null and b/14-EMA-Trend-EA/EMA-Trend-EA.mq5 differ diff --git a/14-EMA-Trend-EA/EMA-Trend-EA.pdf b/14-EMA-Trend-EA/EMA-Trend-EA.pdf new file mode 100644 index 0000000..3550498 Binary files /dev/null and b/14-EMA-Trend-EA/EMA-Trend-EA.pdf differ diff --git a/15-Bollinger-Bands-EA/Bollinger-Bands-EA.mq5 b/15-Bollinger-Bands-EA/Bollinger-Bands-EA.mq5 new file mode 100644 index 0000000..4b6108b --- /dev/null +++ b/15-Bollinger-Bands-EA/Bollinger-Bands-EA.mq5 @@ -0,0 +1,73 @@ +/** + * @copyright 2019, pipbolt.io + * @license https://github.com/pipbolt/experts/blob/master/LICENSE + */ + +#include + +#define NAME "Bollinger Bands EA" +#define VERSION "0.022" + +#property copyright COPYRIGHT +#property link LINK +#property icon ICON +#property description DESCRIPTION +#property version VERSION + +#include + +input group "Entry Strategy"; + +input group "Exit Strategy"; +input bool UseExitStrategy = false; // Use Exit Strategy + +input group "Bollinger Bands"; +input int Bands_Period = 20; // Period +input double Bands_Deviation = 2; // Deviation +input ENUM_APPLIED_PRICE Bands_Applied_Price = PRICE_CLOSE; // Applied Price + +#include + +CiBollinger BBands; + +int OnInit(void) +{ + if (ONINIT() != INIT_SUCCEEDED) + return INIT_FAILED; + + BBands.Init(NULL, NULL, Bands_Period, 0, Bands_Deviation, Bands_Applied_Price); + + return INIT_SUCCEEDED; +} + +void OnTick(void) { ONTICK(); } +void OnDeinit(const int reason) { ONDEINIT(reason); } +void OnTimer() { ONTIMER(); } + +void CheckForOpen(bool &openBuy, bool &openSell) +{ + // Close variable + double close = iClose(NULL, NULL, _indicatorShift); + + // Buy Entry Strategy + openBuy = close < BBands.Lower(0); + + // Sell Entry Strategy + openSell = close > BBands.Upper(0); + + // Apply MA Filter + openBuy = openBuy && MAFilter.Check(DIR_BUY); + openSell = openSell && MAFilter.Check(DIR_SELL); +} + +void CheckForClose(bool &closeBuy, bool &closeSell) +{ + // Close variable + double close = iClose(NULL, NULL, _indicatorShift); + + // Buy Exit Strategy + closeBuy = close > BBands.Upper(0); + + // Sell Exit Strategy + closeSell = close < BBands.Lower(0); +} \ No newline at end of file diff --git a/15-Bollinger-Bands-EA/Bollinger-Bands-EA.pdf b/15-Bollinger-Bands-EA/Bollinger-Bands-EA.pdf new file mode 100644 index 0000000..b03180d Binary files /dev/null and b/15-Bollinger-Bands-EA/Bollinger-Bands-EA.pdf differ diff --git a/16-Ichimoku-EA/Ichimoku-EA.mq5 b/16-Ichimoku-EA/Ichimoku-EA.mq5 new file mode 100644 index 0000000..80251b8 --- /dev/null +++ b/16-Ichimoku-EA/Ichimoku-EA.mq5 @@ -0,0 +1,68 @@ +/** + * @copyright 2019, pipbolt.io + * @license https://github.com/pipbolt/experts/blob/master/LICENSE + */ + +#include + +#define NAME "Ichimoku EA" +#define VERSION "0.022" + +#property copyright COPYRIGHT +#property link LINK +#property icon ICON +#property description DESCRIPTION +#property version VERSION + +#include + +input group "Entry Strategy"; + +input group "Exit Strategy"; +input bool UseExitStrategy = false; // Use Exit Strategy + +input group "Ichimoku Kynko Hyo"; +input int tenkanSen = 9; // period of Tenkan-sen +input int kijunSen = 26; // period of Kijun-sen +input int senkouSpanB = 52; // period of Senkou Span B + +#include + +CiIchimoku Ichimoku; + +int OnInit(void) +{ + if (ONINIT() != INIT_SUCCEEDED) + return INIT_FAILED; + + Ichimoku.Init(NULL, NULL, tenkanSen, kijunSen, senkouSpanB); + + return INIT_SUCCEEDED; +} + +void OnTick(void) { ONTICK(); } +void OnDeinit(const int reason) { ONDEINIT(reason); } +void OnTimer() { ONTIMER(); } + +void CheckForOpen(bool &openBuy, bool &openSell) +{ + // Buy Entry Strategy + openBuy = Ichimoku.TenkanSen(0) > Ichimoku.KijunSen(0) && Ichimoku.TenkanSen(1) <= Ichimoku.KijunSen(1); + + // Sell Entry Stategy + openSell = Ichimoku.TenkanSen(0) < Ichimoku.KijunSen(0) && Ichimoku.TenkanSen(1) >= Ichimoku.KijunSen(1); + + // Apply MA Filter + openBuy = openBuy && MAFilter.Check(DIR_BUY); + openSell = openSell && MAFilter.Check(DIR_SELL); +} + +void CheckForClose(bool &closeBuy, bool &closeSell) +{ + // Buy Exit Strategy + closeBuy = Ichimoku.TenkanSen(0) <= Ichimoku.KijunSen(0); + + // Sell Exit Stategy + closeSell = Ichimoku.TenkanSen(0) >= Ichimoku.KijunSen(0); +} +//+------------------------------------------------------------------+ diff --git a/16-Ichimoku-EA/Ichimoku-EA.pdf b/16-Ichimoku-EA/Ichimoku-EA.pdf new file mode 100644 index 0000000..1f9d191 Binary files /dev/null and b/16-Ichimoku-EA/Ichimoku-EA.pdf differ diff --git a/17-MA-Crossover-MT4/MA-Crossover-MT4.mq4 b/17-MA-Crossover-MT4/MA-Crossover-MT4.mq4 new file mode 100644 index 0000000..9f17dee Binary files /dev/null and b/17-MA-Crossover-MT4/MA-Crossover-MT4.mq4 differ diff --git a/17-MA-Crossover-MT4/MA-Crossover-MT4.pdf b/17-MA-Crossover-MT4/MA-Crossover-MT4.pdf new file mode 100644 index 0000000..e6f5817 Binary files /dev/null and b/17-MA-Crossover-MT4/MA-Crossover-MT4.pdf differ diff --git a/18-Conservative-Scalper/Conservative-Scalper.mq4 b/18-Conservative-Scalper/Conservative-Scalper.mq4 new file mode 100644 index 0000000..7667222 --- /dev/null +++ b/18-Conservative-Scalper/Conservative-Scalper.mq4 @@ -0,0 +1,825 @@ +//+------------------------------------------------------------------+ +//| ConservativeScalper.mq4 | +//| MT4 Conservative Scalping Expert Advisor| +//| | +//| Strategy: | +//| - M15 trend bias via EMA 50/200 | +//| - M5 execution via EMA 20 + RSI 14 + candle breakout | +//| - Session, spread, ATR, rollover, and day-of-week filters | +//| - Fixed-fractional risk sizing (default 0.25% per trade) | +//| - Hard SL/TP on every trade — no martingale, no grid | +//| - Break-even, trailing stop, time-based exit | +//| - Daily loss cap, max trades/day, consecutive loss pause | +//| - Equity drawdown hard stop | +//| | +//| Pairs: EURUSD, GBPUSD, USDJPY (tune per pair) | +//| Timeframe: M5 (with M15 bias) | +//+------------------------------------------------------------------+ +#property copyright "NAK" +#property link "https://github.com/NadirAliOffical/conservative-scalper-ea" +#property version "1.00" +#property strict + +//==================================================================== +// GENERAL INPUTS +//==================================================================== +extern int MagicNumber = 20260409; // Unique EA identifier +extern string TradeComment = "CScalp"; // Order comment tag +extern bool EnableLong = true; // Allow buy trades +extern bool EnableShort = true; // Allow sell trades +extern bool OneTradePerSymbol = true; // One open trade per symbol +extern bool AllowNewTrades = true; // Master on/off switch + +//==================================================================== +// SESSION / TIME FILTERS +//==================================================================== +extern int SessionStartHour = 8; // Server hour to start trading +extern int SessionEndHour = 17; // Server hour to stop new trades +extern bool AllowMonday = true; +extern bool AllowTuesday = true; +extern bool AllowWednesday = true; +extern bool AllowThursday = true; +extern bool AllowFriday = false; // Off by default — thin close +extern int RolloverBlockBefore = 30; // Mins to block before 00:00 +extern int RolloverBlockAfter = 30; // Mins to block after 00:00 + +//==================================================================== +// BIAS INDICATORS (Higher timeframe) +//==================================================================== +extern ENUM_TIMEFRAMES BiasTimeframe = PERIOD_M15; // Trend filter timeframe +extern int BiasFastEMA = 50; // Fast EMA period on bias TF +extern int BiasSlowEMA = 200; // Slow EMA period on bias TF + +//==================================================================== +// EXECUTION INDICATORS (Chart timeframe — run EA on M5) +//==================================================================== +extern int ExecEMA_Period = 20; // EMA for local direction +extern int RSI_Period = 14; // RSI period +extern double RSI_LongLevel = 50.0; // RSI cross-above for longs +extern double RSI_ShortLevel = 50.0; // RSI cross-below for shorts +extern int ATR_Period = 14; // ATR period +extern double MinATR_Pips = 3.0; // Min ATR (pips) — avoid dead mkt +extern int BreakoutBars = 1; // Bars back for high/low breakout + +//==================================================================== +// RISK SIZING +//==================================================================== +extern int LotSizingMode = 1; // 0=Fixed lot 1=Risk % +extern double FixedLot = 0.01; // Used when mode=0 +extern double RiskPercent = 0.50; // % of equity risked per trade +extern double MaxSpreadPips = 2.5; // Max allowed spread in pips +extern int MaxSlippagePts = 3; // Max slippage in broker points + +//==================================================================== +// STOP LOSS / TAKE PROFIT +//==================================================================== +extern int StopLossMode = 1; // 0=Fixed pips 1=ATR multiple +extern double StopLossPips = 8.0; // Fixed SL (pips) mode=0 +extern double StopLossATRMult = 1.2; // ATR multiplier for SL mode=1 +extern int TakeProfitMode = 1; // 0=Fixed pips 1=ATR multiple +extern double TakeProfitPips = 10.0; // Fixed TP (pips) mode=0 +extern double TakeProfitATRMult = 1.2; // ATR multiplier for TP mode=1 + +//==================================================================== +// TRADE MANAGEMENT +//==================================================================== +extern bool UseBreakEven = true; +extern double BreakEvenTriggerR = 0.8; // Move SL to BE after 0.8R profit +extern double BreakEvenOffsetPips = 0.5; // Buffer pips beyond entry for BE +extern bool UseTrailingStop = false; +extern double TrailingStartR = 1.0; // Start trailing after 1R profit +extern double TrailingDistancePips = 5.0; // Trail distance in pips +extern bool UseTimeExit = true; +extern int MaxTradeMinutes = 20; // Close stalled trades after N min +extern bool CloseAtSessionEnd = true; // Close open trades at session end + +//==================================================================== +// DAILY / SESSION PROTECTION +//==================================================================== +extern int MaxTradesPerDay = 6; // Max new trades per session day +extern double MaxDailyLossPercent = 2.0; // Stop trading if daily loss >= X% +extern int MaxConsecutiveLosses = 3; // Pause after N consecutive losses +extern double MaxDrawdownPercent = 20.0; // Hard stop if equity DD >= X% +extern double MaxTotalOpenRiskPct = 1.0; // Cap on total open risk % + +//==================================================================== +// NEWS FILTER (auto-fetches ForexFactory calendar) +//==================================================================== +extern bool UseNewsFilter = true; // Enable automatic news filter +extern string NewsFilterCurrencies = "USD,EUR,GBP";// Block news for these currencies +extern int NewsBlockMinsBefore = 30; // Mins to block before event +extern int NewsBlockMinsAfter = 30; // Mins to block after event +extern int BrokerGMTOffset = 2; // Broker server GMT offset (check chart) + +//==================================================================== +// GLOBALS +//==================================================================== +double g_pip; // Value of 1 pip in price units +double g_point; // Broker point +int g_digits; // Symbol digits +int g_todayTrades; // Trades opened today +double g_todayStartEquity; // Equity at start of today +int g_consecutiveLosses; // Rolling loss streak count +int g_lastHistoryTotal; // History size snapshot (for tracking closed orders) +bool g_tradingHalted; // True when max DD hit (persists across days) +double g_peakEquity; // All-time equity high for DD calculation +datetime g_lastTradeDay; // Date of last counter reset + +// News filter globals +datetime g_newsEvents[]; +int g_newsEventCount = 0; +datetime g_lastNewsFetch = 0; +datetime g_lastNewsLogTime = 0; + +// GlobalVariable key names (set in OnInit) +string g_gvPeak; +string g_gvHalt; +string g_gvConsec; + +//+------------------------------------------------------------------+ +//| INIT | +//+------------------------------------------------------------------+ +int OnInit() +{ + g_digits = (int)MarketInfo(Symbol(), MODE_DIGITS); + + // Normalise pip for 4-digit and 5-digit brokers + if(g_digits == 5 || g_digits == 3) + g_pip = Point * 10; + else + g_pip = Point; + g_point = Point; + + // GlobalVariable keys unique to this symbol + magic number + string suffix = Symbol() + "_" + IntegerToString(MagicNumber); + g_gvPeak = "CScalp_Peak_" + suffix; + g_gvHalt = "CScalp_Halt_" + suffix; + g_gvConsec = "CScalp_Consec_" + suffix; + + g_todayTrades = 0; + g_todayStartEquity = AccountEquity(); + g_lastHistoryTotal = OrdersHistoryTotal(); + g_lastTradeDay = 0; + + // Restore persistent state so restarts don't reset DD protection + g_peakEquity = GlobalVariableCheck(g_gvPeak) ? GlobalVariableGet(g_gvPeak) : AccountEquity(); + g_tradingHalted = GlobalVariableCheck(g_gvHalt) && GlobalVariableGet(g_gvHalt) > 0; + g_consecutiveLosses = GlobalVariableCheck(g_gvConsec) ? (int)GlobalVariableGet(g_gvConsec) : 0; + + Log("Initialized | Symbol=" + Symbol() + + " Digits=" + IntegerToString(g_digits) + + " Pip=" + DoubleToString(g_pip, g_digits + 1)); + + return INIT_SUCCEEDED; +} + +//+------------------------------------------------------------------+ +//| DEINIT | +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) +{ + Log("Deinitialized | Reason=" + IntegerToString(reason)); +} + +//+------------------------------------------------------------------+ +//| TICK | +//+------------------------------------------------------------------+ +void OnTick() +{ + // 1. Reset daily counters if calendar date changed + ResetDailyIfNewDay(); + + // 2a. Refresh news calendar once per day + FetchNewsCalendar(); + + // 2. Track peak equity + if(AccountEquity() > g_peakEquity) + { + g_peakEquity = AccountEquity(); + GlobalVariableSet(g_gvPeak, g_peakEquity); + } + + // 3. Track closed order outcomes (update consecutive loss counter) + TrackClosedOrders(); + + // 4. Manage existing open trades (BE, trail, time/session exit) + ManageOpenTrades(); + + // 5. Evaluate new entry + if(!CanOpenNewTrade()) return; + + int signal = GetEntrySignal(); + if(signal != 0) ExecuteTrade(signal); +} + +//==================================================================== +// DAILY RESET +//==================================================================== +void ResetDailyIfNewDay() +{ + datetime today = StringToTime(TimeToStr(TimeCurrent(), TIME_DATE)); + if(today == g_lastTradeDay) return; + + g_lastTradeDay = today; + g_todayTrades = 0; + g_todayStartEquity = AccountEquity(); + + // Consecutive loss streak is NOT reset on new day — only a win resets it + + Log("New day reset | Equity=" + DoubleToString(AccountEquity(), 2)); +} + +//==================================================================== +// PRE-TRADE GATE CHECKS +//==================================================================== +bool CanOpenNewTrade() +{ + if(!AllowNewTrades) return false; + if(g_tradingHalted) return false; + + // Hard drawdown check + if(g_peakEquity > 0) + { + double dd = (g_peakEquity - AccountEquity()) / g_peakEquity * 100.0; + if(dd >= MaxDrawdownPercent) + { + Log("HARD HALT — max drawdown " + DoubleToString(dd, 2) + "% reached"); + g_tradingHalted = true; + GlobalVariableSet(g_gvHalt, 1.0); + return false; + } + } + + // Daily loss cap + if(g_todayStartEquity > 0) + { + double dailyLoss = (g_todayStartEquity - AccountEquity()) / g_todayStartEquity * 100.0; + if(dailyLoss >= MaxDailyLossPercent) return false; + } + + // Max trades today + if(g_todayTrades >= MaxTradesPerDay) return false; + + // Consecutive loss pause + if(g_consecutiveLosses >= MaxConsecutiveLosses) return false; + + // Day of week + if(!IsAllowedDay()) return false; + + // Session hours + if(!IsSessionTime()) return false; + + // Rollover block + if(IsRolloverTime()) return false; + + // News filter + if(IsNewsTime()) return false; + + // Spread + double spreadPips = MarketInfo(Symbol(), MODE_SPREAD) * g_point / g_pip; + if(spreadPips > MaxSpreadPips) return false; + + // ATR minimum (avoid dead market) + double atrPips = iATR(Symbol(), Period(), ATR_Period, 1) / g_pip; + if(atrPips < MinATR_Pips) return false; + + // One trade per symbol + if(OneTradePerSymbol && HasOpenTrade()) return false; + + // Total open risk cap + if(GetTotalOpenRiskPct() >= MaxTotalOpenRiskPct) return false; + + return true; +} + +//==================================================================== +// ENTRY SIGNAL +// Returns: 1 = BUY -1 = SELL 0 = NONE +//==================================================================== +int GetEntrySignal() +{ + // --- Higher timeframe bias (M15 EMA 50 vs 200) --- + double biasFast = iMA(Symbol(), BiasTimeframe, BiasFastEMA, 0, MODE_EMA, PRICE_CLOSE, 1); + double biasSlow = iMA(Symbol(), BiasTimeframe, BiasSlowEMA, 0, MODE_EMA, PRICE_CLOSE, 1); + double biasFastPrev = iMA(Symbol(), BiasTimeframe, BiasFastEMA, 0, MODE_EMA, PRICE_CLOSE, 2); + + bool bullBias = (biasFast > biasSlow) && (biasFast >= biasFastPrev); + bool bearBias = (biasFast < biasSlow) && (biasFast <= biasFastPrev); + + // --- Execution timeframe indicators --- + double execEMA = iMA(Symbol(), Period(), ExecEMA_Period, 0, MODE_EMA, PRICE_CLOSE, 1); + double rsiNow = iRSI(Symbol(), Period(), RSI_Period, PRICE_CLOSE, 1); + double rsiPrev = iRSI(Symbol(), Period(), RSI_Period, PRICE_CLOSE, 2); + + double prevHigh = iHigh(Symbol(), Period(), BreakoutBars + 1); + double prevLow = iLow(Symbol(), Period(), BreakoutBars + 1); + double closeNow = iClose(Symbol(), Period(), 1); + + // --- LONG --- + if(EnableLong && bullBias) + { + bool aboveEMA = (closeNow > execEMA); + bool rsiCross = (rsiNow >= RSI_LongLevel) && (rsiPrev < RSI_LongLevel); + bool breakout = (closeNow > prevHigh); + + if(aboveEMA && rsiCross && breakout) return 1; + } + + // --- SHORT --- + if(EnableShort && bearBias) + { + bool belowEMA = (closeNow < execEMA); + bool rsiCross = (rsiNow <= RSI_ShortLevel) && (rsiPrev > RSI_ShortLevel); + bool breakout = (closeNow < prevLow); + + if(belowEMA && rsiCross && breakout) return -1; + } + + return 0; +} + +//==================================================================== +// EXECUTE TRADE +//==================================================================== +void ExecuteTrade(int direction) +{ + double atr = iATR(Symbol(), Period(), ATR_Period, 1); + + // SL distance + double slDist = (StopLossMode == 0) + ? StopLossPips * g_pip + : StopLossATRMult * atr; + + // TP distance + double tpDist = (TakeProfitMode == 0) + ? TakeProfitPips * g_pip + : TakeProfitATRMult * atr; + + // Enforce broker minimum stop level + double minStop = MarketInfo(Symbol(), MODE_STOPLEVEL) * g_point; + if(slDist < minStop + g_pip) slDist = minStop + g_pip; + if(tpDist < minStop + g_pip) tpDist = minStop + g_pip; + + // Lot size + double lots = (LotSizingMode == 0) + ? FixedLot + : CalcLotByRisk(slDist); + lots = NormalizeLots(lots); + + if(lots <= 0) + { + Log("ERROR: Lot size <=0 — trade skipped"); + return; + } + + // Free margin check + double reqMargin = MarketInfo(Symbol(), MODE_MARGINREQUIRED) * lots; + if(AccountFreeMargin() < reqMargin) + { + Log("ERROR: Insufficient margin — trade skipped"); + return; + } + + double sl, tp; + int cmd; + double price; + color arrowCol; + + if(direction == 1) + { + cmd = OP_BUY; + price = Ask; + sl = NormalizeDouble(price - slDist, g_digits); + tp = NormalizeDouble(price + tpDist, g_digits); + arrowCol = clrDodgerBlue; + } + else + { + cmd = OP_SELL; + price = Bid; + sl = NormalizeDouble(price + slDist, g_digits); + tp = NormalizeDouble(price - tpDist, g_digits); + arrowCol = clrOrangeRed; + } + + int ticket = OrderSend(Symbol(), cmd, lots, price, MaxSlippagePts, + sl, tp, TradeComment, MagicNumber, 0, arrowCol); + + if(ticket < 0) + { + Log("ORDER FAILED | Error=" + IntegerToString(GetLastError()) + + " Dir=" + IntegerToString(direction)); + } + else + { + g_todayTrades++; + Log("ORDER OPEN | Ticket=" + IntegerToString(ticket) + + " Dir=" + IntegerToString(direction) + + " Lots=" + DoubleToString(lots, 2) + + " Price=" + DoubleToString(price, g_digits) + + " SL=" + DoubleToString(sl, g_digits) + + " TP=" + DoubleToString(tp, g_digits) + + " SLpips=" + DoubleToString(slDist / g_pip, 1)); + } +} + +//==================================================================== +// MANAGE OPEN TRADES (BE, trail, time/session exits) +//==================================================================== +void ManageOpenTrades() +{ + for(int i = OrdersTotal() - 1; i >= 0; i--) + { + if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; + if(OrderSymbol() != Symbol()) continue; + if(OrderMagicNumber() != MagicNumber) continue; + if(OrderType() > OP_SELL) continue; // skip pending + + int ticket = OrderTicket(); + int type = OrderType(); + double openPrice = OrderOpenPrice(); + double curSL = OrderStopLoss(); + double curTP = OrderTakeProfit(); + double slDist = MathAbs(openPrice - curSL); + + // --- Session-end close --- + if(CloseAtSessionEnd && !IsSessionTime()) + { + CloseOrder(ticket, type, "SessionEnd"); + continue; + } + + // --- Time-based exit --- + if(UseTimeExit) + { + int minsOpen = (int)((TimeCurrent() - OrderOpenTime()) / 60); + if(minsOpen >= MaxTradeMinutes) + { + CloseOrder(ticket, type, "TimeExit"); + continue; + } + } + + // Current P&L in price units + double profit = (type == OP_BUY) + ? Bid - openPrice + : openPrice - Ask; + double profitR = (slDist > 0) ? profit / slDist : 0; + + // --- Break-even --- + if(UseBreakEven && slDist > 0 && profitR >= BreakEvenTriggerR) + { + double beOffset = BreakEvenOffsetPips * g_pip; + if(type == OP_BUY) + { + double newSL = NormalizeDouble(openPrice + beOffset, g_digits); + if(newSL > curSL + g_point) + { + if(OrderModify(ticket, openPrice, newSL, curTP, 0, clrGold)) + Log("BE set | Ticket=" + IntegerToString(ticket) + + " NewSL=" + DoubleToString(newSL, g_digits)); + } + } + else + { + double newSL = NormalizeDouble(openPrice - beOffset, g_digits); + if(curSL == 0 || newSL < curSL - g_point) + { + if(OrderModify(ticket, openPrice, newSL, curTP, 0, clrGold)) + Log("BE set | Ticket=" + IntegerToString(ticket) + + " NewSL=" + DoubleToString(newSL, g_digits)); + } + } + } + + // --- Trailing stop --- + if(UseTrailingStop && slDist > 0 && profitR >= TrailingStartR) + { + double trailDist = TrailingDistancePips * g_pip; + if(type == OP_BUY) + { + double newSL = NormalizeDouble(Bid - trailDist, g_digits); + if(newSL > curSL + g_point) + { + if(OrderModify(ticket, openPrice, newSL, curTP, 0, clrAqua)) + Log("Trail updated | Ticket=" + IntegerToString(ticket) + + " NewSL=" + DoubleToString(newSL, g_digits)); + } + } + else + { + double newSL = NormalizeDouble(Ask + trailDist, g_digits); + if(curSL == 0 || newSL < curSL - g_point) + { + if(OrderModify(ticket, openPrice, newSL, curTP, 0, clrAqua)) + Log("Trail updated | Ticket=" + IntegerToString(ticket) + + " NewSL=" + DoubleToString(newSL, g_digits)); + } + } + } + } +} + +//==================================================================== +// TRACK CLOSED ORDERS (update consecutive loss counter) +//==================================================================== +void TrackClosedOrders() +{ + int histTotal = OrdersHistoryTotal(); + if(histTotal <= g_lastHistoryTotal) return; + + for(int i = g_lastHistoryTotal; i < histTotal; i++) + { + if(!OrderSelect(i, SELECT_BY_POS, MODE_HISTORY)) continue; + if(OrderSymbol() != Symbol()) continue; + if(OrderMagicNumber() != MagicNumber) continue; + if(OrderType() > OP_SELL) continue; + + double netPnl = OrderProfit() + OrderSwap() + OrderCommission(); + + if(netPnl < 0) + { + g_consecutiveLosses++; + GlobalVariableSet(g_gvConsec, (double)g_consecutiveLosses); + Log("LOSS | Ticket=" + IntegerToString(OrderTicket()) + + " PnL=" + DoubleToString(netPnl, 2) + + " ConsecLosses=" + IntegerToString(g_consecutiveLosses)); + } + else + { + if(g_consecutiveLosses > 0) + Log("WIN — loss streak reset from " + IntegerToString(g_consecutiveLosses)); + g_consecutiveLosses = 0; + GlobalVariableSet(g_gvConsec, 0.0); + } + } + g_lastHistoryTotal = histTotal; +} + +//==================================================================== +// CLOSE ORDER HELPER +//==================================================================== +void CloseOrder(int ticket, int type, string reason) +{ + double price = (type == OP_BUY) ? Bid : Ask; + bool ok = OrderClose(ticket, OrderLots(), price, MaxSlippagePts, clrWhite); + + if(ok) + Log("ORDER CLOSED | Ticket=" + IntegerToString(ticket) + " Reason=" + reason); + else + Log("CLOSE FAILED | Ticket=" + IntegerToString(ticket) + + " Error=" + IntegerToString(GetLastError())); +} + +//==================================================================== +// POSITION SIZING +//==================================================================== +double CalcLotByRisk(double slDist) +{ + double equity = AccountEquity(); + double riskAmt = equity * RiskPercent / 100.0; + double tickVal = MarketInfo(Symbol(), MODE_TICKVALUE); + double tickSize = MarketInfo(Symbol(), MODE_TICKSIZE); + + if(tickVal <= 0 || tickSize <= 0 || slDist <= 0) return FixedLot; + + double slTicks = slDist / tickSize; + return riskAmt / (slTicks * tickVal); +} + +double NormalizeLots(double lots) +{ + double minLot = MarketInfo(Symbol(), MODE_MINLOT); + double maxLot = MarketInfo(Symbol(), MODE_MAXLOT); + double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP); + + if(lotStep > 0) + lots = MathFloor(lots / lotStep) * lotStep; + + return NormalizeDouble(MathMax(minLot, MathMin(maxLot, lots)), 2); +} + +//==================================================================== +// UTILITY FUNCTIONS +//==================================================================== +bool HasOpenTrade() +{ + for(int i = 0; i < OrdersTotal(); i++) + { + if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; + if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber) + return true; + } + return false; +} + +double GetTotalOpenRiskPct() +{ + double totalRisk = 0; + double equity = AccountEquity(); + if(equity <= 0) return 0; + + for(int i = 0; i < OrdersTotal(); i++) + { + if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; + if(OrderMagicNumber() != MagicNumber) continue; + if(OrderType() > OP_SELL) continue; + + double sl = OrderStopLoss(); + if(sl == 0) continue; + + double slDist = MathAbs(OrderOpenPrice() - sl); + double tickVal = MarketInfo(OrderSymbol(), MODE_TICKVALUE); + double tickSize = MarketInfo(OrderSymbol(), MODE_TICKSIZE); + if(tickSize <= 0) continue; + + totalRisk += (slDist / tickSize) * tickVal * OrderLots() / equity * 100.0; + } + return totalRisk; +} + +bool IsSessionTime() +{ + int h = TimeHour(TimeCurrent()); + return (h >= SessionStartHour && h < SessionEndHour); +} + +bool IsRolloverTime() +{ + int h = TimeHour(TimeCurrent()); + int m = TimeMinute(TimeCurrent()); + int totalMin = h * 60 + m; + + // Minutes until next midnight + int beforeMid = 1440 - totalMin; + // Minutes since last midnight + int afterMid = totalMin; + + return (beforeMid <= RolloverBlockBefore || afterMid <= RolloverBlockAfter); +} + +bool IsAllowedDay() +{ + int dow = TimeDayOfWeek(TimeCurrent()); + switch(dow) + { + case 1: return AllowMonday; + case 2: return AllowTuesday; + case 3: return AllowWednesday; + case 4: return AllowThursday; + case 5: return AllowFriday; + default: return false; + } +} + +//==================================================================== +// NEWS FILTER — auto-fetches ForexFactory high-impact calendar +// Requires: MT4 Tools → Options → Expert Advisors → +// Allow WebRequest for: https://nfs.faireconomy.media +//==================================================================== +void FetchNewsCalendar() +{ + if(!UseNewsFilter) return; + + datetime today = StringToTime(TimeToStr(TimeCurrent(), TIME_DATE)); + if(g_lastNewsFetch == today) return; // Already fetched today + + string url = "https://nfs.faireconomy.media/ff_calendar_thisweek.json"; + string headers = "User-Agent: Mozilla/5.0\r\n"; + char post[]; + char result[]; + string resultHeaders; + + ResetLastError(); + int httpCode = WebRequest("GET", url, headers, 10000, post, result, resultHeaders); + + if(httpCode != 200) + { + Log("NEWS: Fetch failed. HTTP=" + IntegerToString(httpCode) + + " Error=" + IntegerToString(GetLastError()) + + " — Check WebRequest whitelist in MT4 options"); + return; + } + + string json = CharArrayToString(result); + ParseNewsJSON(json); + g_lastNewsFetch = today; + Log("NEWS: Calendar updated. High-impact events found=" + IntegerToString(g_newsEventCount)); +} + +void ParseNewsJSON(string json) +{ + g_newsEventCount = 0; + ArrayResize(g_newsEvents, 200); + + int pos = 0; + int jsonLen = StringLen(json); + + while(pos < jsonLen) + { + // Find next JSON object + int objStart = StringFind(json, "{", pos); + if(objStart < 0) break; + int objEnd = StringFind(json, "}", objStart); + if(objEnd < 0) break; + + string obj = StringSubstr(json, objStart, objEnd - objStart + 1); + + // Only process High impact events + if(StringFind(obj, "\"impact\":\"High\"") >= 0) + { + // Check currency filter + string country = ExtractJSONString(obj, "country"); + if(StringFind(NewsFilterCurrencies, country) >= 0) + { + // Parse date + string dateStr = ExtractJSONString(obj, "date"); + datetime eventTime = ParseISODate(dateStr); + if(eventTime > 0 && g_newsEventCount < 200) + { + g_newsEvents[g_newsEventCount] = eventTime; + g_newsEventCount++; + string title = ExtractJSONString(obj, "title"); + Log("NEWS: Loaded | " + country + " " + title + + " @ " + TimeToStr(eventTime, TIME_DATE | TIME_MINUTES)); + } + } + } + pos = objEnd + 1; + } + ArrayResize(g_newsEvents, g_newsEventCount); +} + +string ExtractJSONString(string obj, string key) +{ + string search = "\"" + key + "\":\""; + int start = StringFind(obj, search); + if(start < 0) return ""; + start += StringLen(search); + int end = StringFind(obj, "\"", start); + if(end < 0) return ""; + return StringSubstr(obj, start, end - start); +} + +datetime ParseISODate(string iso) +{ + // Format: "2026-04-04T08:30:00-0400" + if(StringLen(iso) < 19) return 0; + + int year = (int)StringToInteger(StringSubstr(iso, 0, 4)); + int month = (int)StringToInteger(StringSubstr(iso, 5, 2)); + int day = (int)StringToInteger(StringSubstr(iso, 8, 2)); + int hour = (int)StringToInteger(StringSubstr(iso, 11, 2)); + int min = (int)StringToInteger(StringSubstr(iso, 14, 2)); + + // Parse timezone offset (e.g. -0400 or +0000) + int tzOffsetSecs = 0; + int tzPos = StringFind(iso, "+", 19); + int tzSign = 1; + if(tzPos < 0) { tzPos = StringFind(iso, "-", 19); tzSign = -1; } + if(tzPos >= 0) + { + int tzH = (int)StringToInteger(StringSubstr(iso, tzPos + 1, 2)); + int tzM = (int)StringToInteger(StringSubstr(iso, tzPos + 3, 2)); + tzOffsetSecs = tzSign * (tzH * 3600 + tzM * 60); + } + + // Build UTC datetime + string dtStr = StringFormat("%04d.%02d.%02d %02d:%02d", year, month, day, hour, min); + datetime utc = StringToTime(dtStr) - tzOffsetSecs; + + // Convert UTC → broker server time + datetime serverTime = utc + BrokerGMTOffset * 3600; + return serverTime; +} + +bool IsNewsTime() +{ + if(!UseNewsFilter || g_newsEventCount == 0) return false; + + datetime now = TimeCurrent(); + int blockBefore = NewsBlockMinsBefore * 60; + int blockAfter = NewsBlockMinsAfter * 60; + + for(int i = 0; i < g_newsEventCount; i++) + { + if(now >= g_newsEvents[i] - blockBefore && + now <= g_newsEvents[i] + blockAfter) + { + if(g_newsEvents[i] != g_lastNewsLogTime) + { + Log("NEWS: Trading blocked near event @ " + + TimeToStr(g_newsEvents[i], TIME_DATE | TIME_MINUTES)); + g_lastNewsLogTime = g_newsEvents[i]; + } + return true; + } + } + return false; +} + +void Log(string msg) +{ + Print("[CScalp] " + TimeToStr(TimeCurrent(), TIME_DATE | TIME_MINUTES) + " | " + msg); +} +//+------------------------------------------------------------------+ diff --git a/18-Conservative-Scalper/Conservative-Scalper.pdf b/18-Conservative-Scalper/Conservative-Scalper.pdf new file mode 100644 index 0000000..1fb262a Binary files /dev/null and b/18-Conservative-Scalper/Conservative-Scalper.pdf differ diff --git a/19-XAUUSD-ATR-Scalper/XAUUSD-ATR-Scalper.mq4 b/19-XAUUSD-ATR-Scalper/XAUUSD-ATR-Scalper.mq4 new file mode 100644 index 0000000..cf3a875 --- /dev/null +++ b/19-XAUUSD-ATR-Scalper/XAUUSD-ATR-Scalper.mq4 @@ -0,0 +1,103 @@ +//+------------------------------------------------------------------+ +//| XAUUSD / USDJPY ATR Scalper EA | +//| Author: GIMS_Dev | +//| Platform: MetaTrader 4 | +//| Strategy: ATR + Trend + Price Action | +//+------------------------------------------------------------------+ +#property strict + +// ================= INPUTS ================= +input double Lots = 0.01; +input int ATR_Period = 14; +input double ATR_Multiplier_SL = 1.2; +input double ATR_Multiplier_TP = 1.5; +input int EMA_Fast = 50; +input int EMA_Slow = 200; +input int Slippage = 3; +input int BreakevenPips = 20; +input int TrailingStopPips = 15; +input int MaxTradesPerSymbol = 3; +input int MagicNumber = 123456; + +// ================= SYMBOLS ================= +string Symbols[] = {"XAUUSD", "USDJPY"}; + +//+------------------------------------------------------------------+ +int OnInit() +{ + CreateDashboard(); + Print("ATR Scalper EA initialized"); + return(INIT_SUCCEEDED); +} +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) +{ + ObjectsDeleteAll(0, OBJ_LABEL); +} +//+------------------------------------------------------------------+ +void OnTick() +{ + for(int i=0; i slow || fast < slow); +} + +// ================= PRICE ACTION ================= +bool EngulfingPattern(string sym) +{ + double o1 = iOpen(sym, PERIOD_M5, 1); + double c1 = iClose(sym, PERIOD_M5, 1); + double o2 = iOpen(sym, PERIOD_M5, 2); + double c2 = iClose(sym, PERIOD_M5, 2); + + bool bullish = c2 < o2 && c1 > o1 && c1 > o2; + bool bearish = c2 > o2 && c1 < o1 && c1 < o2; + + return (bullish || bearish); +} + +// ================= ORDER PLACEMENT ================= +void PlaceMarketOrder(string sym) +{ + double atr = iATR(sym, PERIOD_M5, ATR_Period, 0); + if(atr <= 0) return; + + double point = MarketInfo(sym, MODE_POINT); + int digits = (int)MarketInfo(sym, MODE_DIGITS); + + double ask = MarketInfo(sym, MODE_ASK); + double bid = MarketInfo(sym, MODE_BID); + + double sl, tp; + int type; + +} + diff --git a/19-XAUUSD-ATR-Scalper/XAUUSD-ATR-Scalper.pdf b/19-XAUUSD-ATR-Scalper/XAUUSD-ATR-Scalper.pdf new file mode 100644 index 0000000..4c68645 Binary files /dev/null and b/19-XAUUSD-ATR-Scalper/XAUUSD-ATR-Scalper.pdf differ diff --git a/20-ThreeBarPlay/ThreeBarPlay.mq4 b/20-ThreeBarPlay/ThreeBarPlay.mq4 new file mode 100644 index 0000000..680b14e --- /dev/null +++ b/20-ThreeBarPlay/ThreeBarPlay.mq4 @@ -0,0 +1,409 @@ +//+------------------------------------------------------------------+ +//| ThreeBarPlay.mq4 | +//| Copyright 2020, MetaQuotes Software Corp. | +//| https://www.mql5.com | +//+------------------------------------------------------------------+ +#property copyright "Copyright 2020, MetaQuotes Software Corp." +#property link "https://www.mql5.com" +#property version "1.00" +#property strict +//+------------------------------------------------------------------+ +//| Expert initialization function | +//+------------------------------------------------------------------+ +int OnInit() + { +//--- + +//--- + return(INIT_SUCCEEDED); + } +//+------------------------------------------------------------------+ +//| Expert deinitialization function | +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) + { +//--- + + } +//+------------------------------------------------------------------+ +//| trading mechanics is the same as BUYSTOCK/SELLSTOCK scripts | +//+------------------------------------------------------------------+ +int numOfTrades=0; +int takeprofit=100; +int stoploss=100; +void trade(bool buy, int sl) + { + numOfTrades++; + if(buy) + { + int pips=sl; + double risk=AccountBalance()*0.02/1.3; + double shares = (int)(risk / pips * 100); + double maxShares = (int)(AccountFreeMargin()/1.3 * 14.8 / (Ask)); + if((MarketInfo(_Symbol,17))<0.01)//if forex + { + maxShares/=100; + shares/=100; + } + if(shares>(MarketInfo(_Symbol,MODE_MAXLOT))) + { + shares=MarketInfo(_Symbol,MODE_MAXLOT); + } + if(maxShares < shares) + { + shares = maxShares; + pips=(int)(risk/shares*100); + } + + Alert("Buy ",shares," shares of ", _Symbol); + double x=Ask; + double y=shares*0.67; + double z=shares*0.33; + int pipstostoploss=pips; + int takeprofit2= pips; + int takeprofit1= (int)(pips/2); + int order0=OrderSend( + _Symbol,//currencyPair + OP_BUY,//buy + y,//howmuch*SYMBOL_VOLUME_MIN + x,//price + 3,//tolerance + x-pipstostoploss*_Point, //stoploss + x+takeprofit1*_Point,//takeprofit + NULL,//comment + 0,//magic number + 0,//expiration + CLR_NONE//color of arrow + ); + int order1=OrderSend( + _Symbol,//currencyPair + OP_BUY,//buy + z,//howmuch*SYMBOL_VOLUME_MIN + x,//price + 3,//tolerance + x-pipstostoploss*_Point, //stoploss + x+takeprofit2*_Point,//takeprofit + NULL,//comment + 0,//magic number + 0,//expiration + CLR_NONE//color of arrow + ); + } + else + if(!buy) + { + int pips=sl; + double risk=AccountBalance()*0.02/1.3; + double shares = (int)(risk / pips * 100); + double maxShares = (int)(AccountFreeMargin()/1.3 * 14.8 / (Bid)); + if((MarketInfo(_Symbol,17))<0.01)//if forex + { + maxShares/=100; + shares/=100; + } + if(shares>(MarketInfo(_Symbol,MODE_MAXLOT))) + { + shares=MarketInfo(_Symbol,MODE_MAXLOT); + } + if(maxShares < shares) + { + shares = maxShares; + pips=(int)(risk/shares*100); + } + + Alert("Sell ",shares," shares of ", _Symbol); + double x=Bid; + double y=shares*0.67; + double z=shares*0.33; + int pipstostoploss=pips; + int takeprofit2= pips; + int takeprofit1= (int)(pips/2); + + int order0=OrderSend( + _Symbol,//currencyPair + OP_SELL,//sell + y,//howmuch*SYMBOL_VOLUME_MIN + x,//price + 3,//tolerance + x+pipstostoploss*_Point, //stoploss + x-takeprofit1*_Point,//takeprofit + NULL,//comment + 0,//magic number + 0,//expiration + CLR_NONE//color of arrow + ); + int order1=OrderSend( + _Symbol,//currencyPair + OP_SELL,//sell + z,//howmuch*SYMBOL_VOLUME_MIN + x,//price + 3,//tolerance + x+pipstostoploss*_Point, //stoploss + x-takeprofit2*_Point,//takeprofit + NULL,//comment + 0,//magic number + 0,//expiration + CLR_NONE//color of arrow + ); + } + } + +//+------------------------------------------------------------------+ +//| Expert tick function | +//+------------------------------------------------------------------+ +double height; +bool traded=false; +void OnTick() + { +//find average size of candlesticks + int numOfCandles=200; + double total=0; + for(int i=0; i0) + { + top=high-close; + bot=open-low; + } + else + if(body<0) + { + top=high-open; + bot=close-low; + } + if(body!=0) + { + topratio=top/(MathAbs(body)); + botratio=bot/(MathAbs(body)); + } + else + { + //if body is 0 + topratio=0; + botratio=0; + } + double dojiratio=1; + if((botratio>=dojiratio)&&(topratio>=dojiratio)&&(botratio>=(dojiratio+2))&&(topratio>=(dojiratio+2))) + { + doji=true; + if((botratio/topratio)>=2) + { + doji=false; + hammer=true; + } + else + if((topratio/botratio)>=2) + { + doji=false; + star=true; + } + } + else + if((botratio>=dojiratio)&&(botratio>=(dojiratio+2))) + { + hammer=true; + } + else + if((topratio>=(dojiratio+2))&&(topratio>=dojiratio)) + { + star=true; + } + /* + calculations: + if spread<=16, then look for plays + ignition is any bar [a certain number] times or more than spread + correction is if second bar is less than 1 quarter ignition in op direction + enter trade when bid passes the ignition close + stoploss will be at ignition's open+spread + */ +//init variables + double MFI=iMFI(_Symbol,_Period,14,0); + int spread=SYMBOL_SPREAD; + double goldenNum=0.8; + double goldenMax=10; + double goldenNum2=2.5; + bool bull=false; + bool bear=false; + bool ignition=false; + bool correction=false; + bool confirmation=false; + bool rejection=false; + int height2=(int)((Close[2]-Open[2])/_Point); + int height1=(int)((Close[1]-Open[1])/_Point); +//one trade per 3barcombo + if(height!=Close[1]) + { + traded=false; + } +//ignition : any bar [a certain number] times or more than average candle + if((height2>=(goldenNum*average))&&(height2<=(goldenMax*average))) + { + ignition=true; + //bull + bull=true; + bear=false; + } + if((height2<=(goldenNum*-1*average))&&(height2>=(goldenMax*-1*average))) + { + ignition=true; + bear=true; + bull=false; + height2*=-1; + } +//correction : second bar is less than [a second certain number] times of ignition in op direction + + if(bull && (height1<-2) && (height1>=((-1)*height2/goldenNum2))) + { + if(!doji && !star) + { + correction=true; + } + } + else + if((bear &&(height1>2) && (height1<=(height2/goldenNum2)))) + { + if(!doji && !hammer) + { + correction=true; + } + } +//confirmation : when the price passes the ignition close + if(((bull)&&(Ask>=(Close[2]+(spread*_Point))))||((bear)&&(Bid<=(Close[2]-(spread*_Point))))) + { + if(correction) + { + confirmation=true; + } + } + else + { + if(((bull)&&(Bid<=(Close[1]-(1.5*spread*_Point))))||((bear)&&(Ask>=(Close[1]+(1.5*spread*_Point))))) + { + if((bull==true)&&(bear==false)&&(star||doji)) + { + rejection=true; + bear=true; + bull=false; + } + else + if((bear==true)&&(bull==false)&&(hammer||doji)) + { + rejection=true; + bull=true; + bear=false; + } + } + } + +//make trade if all true +//only trade with higher than average relative volume +bool withVol=false; +withVol=((Volume[1]/averageVol)>=1); +//trading format "trade(bool buy,int pipstostoploss));" + + if(ignition && correction && confirmation && bull && !traded && withVol) + { + //buy after confirmation + stoploss=height2+(2*spread); + takeprofit=stoploss/2; + trade(true,stoploss); + traded=true; + } + else + if(ignition && correction && confirmation && bear && !traded && withVol) + { + //sell after confirmation + stoploss=height2+(2*spread); + takeprofit=stoploss/2; + trade(false,stoploss); + traded=true; + } + else + if(!traded && ignition && bull && rejection && withVol) + { + //buy after rejection + stoploss=height2+spread; + takeprofit=stoploss/2; + trade(true,stoploss); + traded=true; + } + else + if(!traded && ignition && bear && rejection && withVol) + { + //sell after rejection + stoploss=height2+spread; + takeprofit=stoploss/2; + trade(false,stoploss); + traded=true; + } + Comment( + "balance : ",AccountBalance(),"\n", + "ignition : ",ignition,"\n", + "correction : ",correction,"\n", + "confirmation: ",confirmation,"\n", + "doji: ",doji,"\n", + "hammer: ",hammer,"\n", + "star: ",star,"\n", + numOfTrades," trades " + ); + height=Close[1]; +//if there is a trade open, make a trailing stop while profitable by 20+ pips + if(OrdersTotal()>0) + { + for(int i=OrdersTotal(); i>=0; i--) + { + int pips=(int)(takeprofit); + //select an order + if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES)) + { + //make sure its the right currency pair + if(OrderSymbol()==_Symbol) + { + //check if buy or sell + if(OrderType()==OP_BUY) + { + if((Bid>(OrderOpenPrice()+pips*_Point))&&(OrderStopLoss()OrderOpenPrice()) && (OrderStopLoss()>(Bid+pips*_Point))) + { + bool evensell=OrderModify(OrderTicket(),OrderOpenPrice(),Bid+pips*_Point,OrderTakeProfit(),0); + + } + } + + } + } + } + } + } +//+------------------------------------------------------------------+ + +//+------------------------------------------------------------------+ diff --git a/20-ThreeBarPlay/ThreeBarPlay.pdf b/20-ThreeBarPlay/ThreeBarPlay.pdf new file mode 100644 index 0000000..e39df66 Binary files /dev/null and b/20-ThreeBarPlay/ThreeBarPlay.pdf differ diff --git a/21-RSI-EA/RSI-EA.mq5 b/21-RSI-EA/RSI-EA.mq5 new file mode 100644 index 0000000..df2f833 --- /dev/null +++ b/21-RSI-EA/RSI-EA.mq5 @@ -0,0 +1,764 @@ +//-PROPERTIES-// +// Properties help the software look better when you load it in MT5. +// They provide more information and details +// This is what you see in the About tab when you attach the expert advisor to a chart. +#property link "https://www.earnforex.com/metatrader-expert-advisors/rsi-expert-advisor/" +#property version "1.00" +#property copyright "EarnForex.com - 2024" +#property description "A basic RSI EA created using an EA template." +#property description "" +#property description "WARNING: There is no guarantee that this expert advisor will work as intended. Use at your own risk." +#property description "" +#property description "Find more on www.EarnForex.com" +#property icon "\\Files\\EF-Icon-64x64px.ico" + +//-INCLUDES-// +// '#include' allows to import code from other files. +// In the following instance the file has to be placed in the MQL5\Include folder. +#include // This file is required to easily manage orders and positions. +#include // This file contains useful descriptions for errors. +#include // This file contains some useful functions. + +//-COMMENTS-// +// This is a single line comment and you can do it by placing // at the start of the comment, this text is ignored when compiling. + +/* +This is a multi-line comment. +It starts with /* and it finishes with the * and / like below +*/ + +enum ENUM_RISK_BASE +{ + RISK_BASE_EQUITY = 1, // EQUITY + RISK_BASE_BALANCE = 2, // BALANCE + RISK_BASE_FREEMARGIN = 3, // FREE MARGIN +}; + +enum ENUM_RISK_DEFAULT_SIZE +{ + RISK_DEFAULT_FIXED = 1, // FIXED SIZE + RISK_DEFAULT_AUTO = 2, // AUTOMATIC SIZE BASED ON RISK +}; + +enum ENUM_MODE_SL +{ + SL_FIXED = 0, // FIXED STOP LOSS + SL_AUTO = 1, // AUTOMATIC STOP LOSS +}; + +enum ENUM_MODE_TP +{ + TP_FIXED = 0, // FIXED TAKE PROFIT + TP_AUTO = 1, // AUTOMATIC TAKE PROFIT +}; + +// EA Parameters +input string Comment_0 = "=========="; // EA-Specific Parameters +// !! Declare parameters specific to your EA here. +// For example, a moving average period, an RSI level, or anything else your EA needs to know to implement its trading strategy. +// All input parameters start with 'input' keyword. +// input int example = 10; // This is an example input parameter +input int RSIPeriod = 14; // RSI period +input double RSIOverbought = 80; // RSI overbought level +input double RSIOversold = 20; // RSI oversold level +input ENUM_APPLIED_PRICE RSIPrice = PRICE_CLOSE; // RSI applied price + +input string Comment_1 = "=========="; // Trading Hours Settings +input bool UseTradingHours = false; // Limit trading hours +input ENUM_HOUR TradingHourStart = h07; // Trading start hour (Broker server hour) +input ENUM_HOUR TradingHourEnd = h19; // Trading end hour (Broker server hour) + +input string Comment_2 = "=========="; // ATR Settings +input int ATRPeriod = 100; // ATR period +input ENUM_TIMEFRAMES ATRTimeFrame = PERIOD_CURRENT; // ATR timeframe +input double ATRMultiplierSL = 2; // ATR multiplier for stop-loss +input double ATRMultiplierTP = 3; // ATR multiplier for take-profit + +// General input parameters +input string Comment_a = "=========="; // Risk Management Settings +input ENUM_RISK_DEFAULT_SIZE RiskDefaultSize = RISK_DEFAULT_FIXED; // Position size mode +input double DefaultLotSize = 0.1; // Position size (if fixed or if no stop loss defined) +input ENUM_RISK_BASE RiskBase = RISK_BASE_BALANCE; // Risk base +input int MaxRiskPerTrade = 2; // Percentage to risk each trade +input double MinLotSize = 0.01; // Minimum position size allowed +input double MaxLotSize = 100; // Maximum position size allowed +input int MaxPositions = 1; // Maximum number of positions for this EA + +input string Comment_b = "=========="; // Stop-Loss and Take-Profit Settings +input ENUM_MODE_SL StopLossMode = SL_FIXED; // Stop-loss mode +input int DefaultStopLoss = 0; // Default stop-loss in points (0 = no stop-loss) +input int MinStopLoss = 0; // Minimum allowed stop-loss in points +input int MaxStopLoss = 5000; // Maximum allowed stop-loss in points +input ENUM_MODE_TP TakeProfitMode = TP_FIXED; // Take-profit mode +input int DefaultTakeProfit = 0; // Default take-profit in points (0 = no take-profit) +input int MinTakeProfit = 0; // Minimum allowed take-profit in points +input int MaxTakeProfit = 5000; // Maximum allowed take-profit in points + +input string Comment_c = "=========="; // Partial Close Settings +input bool UsePartialClose = false; // Use partial close +input double PartialClosePerc = 50; // Partial close percentage +input double ATRMultiplierPC = 1; // ATR multiplier for partial close + +input string Comment_d = "=========="; // Additional Settings +input int MagicNumber = 0; // Magic number +input string OrderNote = ""; // Comment for orders +input int Slippage = 5; // Slippage in points +input int MaxSpread = 50; // Maximum allowed spread to trade, in points + + +// Global Variables +CTrade Trade; // Trade object. +int ATRHandle; // Indicator handle for ATR. +int IndicatorHandle = -1; // Global indicator handle for the EA's main signal indicator. +double ATR_current, ATR_previous; // ATR values. +double Indicator_current, Indicator_previous; // Indicator values. + +// Here go all the event handling functions. They all run on specific events generated for the expert advisor. +// All event handlers are optional and can be removed if you don't need to process that specific event. + +//+-------------------------------------------------------------------+ +//| Expert initialization handler | +//| Here goes the code that runs just once each time you load the EA. | +//+-------------------------------------------------------------------+ +int OnInit() +{ + // EventSetTimer(60); // Starting a 60-second timer. + // EventSetMillisecondTimer(500); // Starting a 500-millisecond timer. + + if (!Prechecks()) // Check if everything is OK with input parameters. + { + return INIT_FAILED; // Don't initialize the EA if checks fail. + } + + if (!InitializeHandles()) // Initialize indicator handles. + { + PrintFormat("Error initializing indicator handles - %s - %d", GetLastErrorText(GetLastError()), GetLastError()); + return INIT_FAILED; + } + + SetTradeObject(); + + return INIT_SUCCEEDED; // Successful initialization. +} + +//+---------------------------------------------------------------------+ +//| Expert deinitialization handler | +//| Here goes the code that runs just once each time you unload the EA. | +//+---------------------------------------------------------------------+ +void OnDeinit(const int reason) +{ + // Normally, there isn't much stuff you need to do on deinitialization. +} + +//+------------------------------------------------------------------+ +//| Expert tick handler | +//| Here goes the code that runs every tick. | +//+------------------------------------------------------------------+ +void OnTick() +{ + ProcessTick(); // Calling the EA's main processing function here. It's defined farther below. +} + +//+------------------------------------------------------------------+ +//| Timer event handler | +//| Here goes the code that runs on timer. | +//+------------------------------------------------------------------+ +void OnTimer() +{ + // For example, you can update a display timer here if you have one in your EA. +} + +//+------------------------------------------------------------------------------+ +//| Trade event handler | +//| Here goes the code that runs each time something related to trading happens. | +//+------------------------------------------------------------------------------+ +void OnTrade() +{ + // For example, if you want to do something when a pending order gets triggered, you can do it here without overloading the OnTick() handler too much. +} + +//+--------------------------------------------------------------------------------+ +//| Backtest end handler | +//| Here goes the code that runs each time a backtest in Strategy Tester finishes. | +//| The goal is to calculate the value of a custom optimization criterion. | +//+--------------------------------------------------------------------------------+ +double OnTester() +{ + double NetProfit = TesterStatistics(STAT_PROFIT); + double InitialDeposit = TesterStatistics(STAT_INITIAL_DEPOSIT); + double MaxDrawDownPerc = TesterStatistics(STAT_EQUITYDD_PERCENT); + double TotalTrades = TesterStatistics(STAT_TRADES); + if (InitialDeposit == 0) return 0; // Avoiding division by zero. + if (TotalTrades == 0) return -100; // Discard a backtest with zero trades. + if ((TotalTrades > 0) && (MaxDrawDownPerc == 0)) MaxDrawDownPerc = 0.01; // Avoiding division by zero. + + double NetProfitPerc = NetProfit / InitialDeposit * 100; + + double Max = 0; + if (NetProfitPerc > 0) Max = NetProfitPerc / MaxDrawDownPerc; // Adjust net profit by maximum drawdown. + if (NetProfitPerc < 0) Max = NetProfitPerc; + + return Max; // Return the value as a custom optimization criterion. +} + + +// Here go all custom functions. They all are called either from the above-defined event handlers or from other custom functions. + +// Entry and exit processing +void ProcessTick() +{ + if (!GetIndicatorsData()) return; + + if (CountPositions()) + { + // There is a position open. Manage SL, TP, or close if necessary. + if (UsePartialClose) PartialCloseAll(); + CheckExitSignal(); + } + + // A block of code that lets the subsequent code execute only when a new bar appears on the chart. + // This means that the entry signals will be checked only twice per bar. + static datetime current_bar_time = WRONG_VALUE; + datetime previous_bar_time = current_bar_time; + current_bar_time = iTime(Symbol(), Period(), 0); + static int ticks_of_new_bar = 0; // Process two ticks of each new bar to allow indicator buffers to refresh. + if (current_bar_time == previous_bar_time) + { + ticks_of_new_bar++; + if (ticks_of_new_bar > 1) return; // Skip after two ticks. + } + else ticks_of_new_bar = 0; + + // The number is recalculated after the first call because some trades could have been gotten closed. + if (CountPositions() < MaxPositions) CheckEntrySignal(); // Check entry signals only if there aren't too many positions already. +} + +int CountPositions() +{ + int count = 0; + int TotalPositions = PositionsTotal(); + for (int i = 0; i < TotalPositions; i++) + { + string Instrument = PositionGetSymbol(i); + if (Instrument == "") + { + PrintFormat(__FUNCTION__, ": ERROR - Unable to select the position - %s - %d.", GetLastErrorText(GetLastError()), GetLastError()); + } + else + { + // Skip positions in other symbols. + if (Instrument != Symbol()) continue; + // Skip counting positions with a different Magic number if the EA has non-zero Magic number set. + if ((MagicNumber != 0) && (PositionGetInteger(POSITION_MAGIC) != MagicNumber)) continue; + count++; + } + } + return count; +} + +// Initialize handles. Indicator handles have to be initialized at the beginning of the EA's operation. +bool InitializeHandles() +{ + // Indicator handle is the main handle for the signal generating indicator. + IndicatorHandle = iRSI(Symbol(), Period(), RSIPeriod, RSIPrice); + if (IndicatorHandle == INVALID_HANDLE) + { + PrintFormat("Unable to create main indicator handle - %s - %d.", GetLastErrorText(GetLastError()), GetLastError()); + return false; + } + // ATR handle for stop-loss and take-profit. + ATRHandle = iATR(Symbol(), ATRTimeFrame, ATRPeriod); + if (ATRHandle == INVALID_HANDLE) + { + PrintFormat("Unable to create ATR handle - %s - %d.", GetLastErrorText(GetLastError()), GetLastError()); + return false; + } + return true; +} + +// Trading functions + +// Set the basic parameters of the Trade object. +void SetTradeObject() +{ + // All future trade operations will take into account these parameters - Magic number and deviation/slippage. + Trade.SetExpertMagicNumber(MagicNumber); + Trade.SetDeviationInPoints(Slippage); +} + +// Open a position with a buy order. +bool OpenBuy() +{ + double Ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK); + double Bid = SymbolInfoDouble(Symbol(), SYMBOL_BID); + double OpenPrice = Ask; // Buy at Ask. + double StopLossPrice = StopLoss(ORDER_TYPE_BUY, OpenPrice); // Calculate SL based on direction, price, and SL rules. + double TakeProfitPrice = TakeProfit(ORDER_TYPE_BUY, OpenPrice); // Calculate TP based on direction, price, and TP rules. + double Size = LotSize(StopLossPrice, OpenPrice); // Calculate position size based on the SL, price, and the given rules. + // Use the standard Trade object to open the position with calculated parameters. + if (!Trade.Buy(Size, Symbol(), OpenPrice, StopLossPrice, TakeProfitPrice)) + { + PrintFormat("Unable to open BUY: %s - %d", Trade.ResultRetcodeDescription(), Trade.ResultRetcode()); + return false; + } + return true; +} + +// Open a position with a sell order. +bool OpenSell() +{ + double Ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK); + double Bid = SymbolInfoDouble(Symbol(), SYMBOL_BID); + double OpenPrice = Bid; // Sell at Bid. + double StopLossPrice = StopLoss(ORDER_TYPE_SELL, OpenPrice); // Calculate SL based on direction, price, and SL rules. + double TakeProfitPrice = TakeProfit(ORDER_TYPE_SELL, OpenPrice); // Calculate TP based on direction, price, and TP rules. + double Size = LotSize(StopLossPrice, OpenPrice); // Calculate position size based on the SL, price, and the given rules. + // Use the standard Trade object to open the position with calculated parameters. + if (!Trade.Sell(Size, Symbol(), OpenPrice, StopLossPrice, TakeProfitPrice)) + { + PrintFormat("Unable to open SELL: %s - %d", Trade.ResultRetcodeDescription(), Trade.ResultRetcode()); + return false; + } + return true; +} + +// Close the specified position completely. +//!! Unused. Can be uncommented and used to close specific positions. +/* bool ClosePosition(ulong ticket) +{ + if (!Trade.PositionClose(ticket)) + { + PrintFormat(__FUNCTION__, ": ERROR - Unable to close position: %s - %d", Trade.ResultRetcodeDescription(), Trade.ResultRetcode()); + return false; + } + return true; +}*/ + +void CloseAllSell() +{ + int total = PositionsTotal(); + + // Start a loop to scan all the positions. + // The loop starts from the last, otherwise it could skip positions. + for (int i = total - 1; i >= 0; i--) + { + // If the position cannot be selected log an error. + if (PositionGetSymbol(i) == "") + { + PrintFormat(__FUNCTION__, ": ERROR - Unable to select the position - %s - %d.", GetLastErrorText(GetLastError()), GetLastError()); + continue; + } + if (PositionGetString(POSITION_SYMBOL) != Symbol()) continue; // Only close current symbol trades. + if (PositionGetInteger(POSITION_TYPE) != POSITION_TYPE_SELL) continue; // Only close Sell positions. + if (PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue; // Only close own positions. + + for (int try = 0; try < 10; try++) + { + bool result = Trade.PositionClose(PositionGetInteger(POSITION_TICKET)); + if (!result) + { + PrintFormat(__FUNCTION__, ": ERROR - Unable to close position: %s - %d", Trade.ResultRetcodeDescription(), Trade.ResultRetcode()); + } + else break; + } + } +} + +void CloseAllBuy() +{ + int total = PositionsTotal(); + + // Start a loop to scan all the positions. + // The loop starts from the last, otherwise it could skip positions. + for (int i = total - 1; i >= 0; i--) + { + // If the position cannot be selected log an error. + if (PositionGetSymbol(i) == "") + { + PrintFormat(__FUNCTION__, ": ERROR - Unable to select the position - %s - %d.", GetLastErrorText(GetLastError()), GetLastError()); + continue; + } + if (PositionGetString(POSITION_SYMBOL) != Symbol()) continue; // Only close current symbol trades. + if (PositionGetInteger(POSITION_TYPE) != POSITION_TYPE_BUY) continue; // Only close Buy positions. + if (PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue; // Only close own positions. + + for (int try = 0; try < 10; try++) + { + bool result = Trade.PositionClose(PositionGetInteger(POSITION_TICKET)); + if (!result) + { + PrintFormat(__FUNCTION__, ": ERROR - Unable to close position: %s - %d", Trade.ResultRetcodeDescription(), Trade.ResultRetcode()); + } + else break; + } + } +} + +// Close all positions opened by this EA. +void CloseAllPositions() +{ + int total = PositionsTotal(); + + // Start a loop to scan all the positions. + // The loop starts from the last, otherwise it could skip positions. + for (int i = total - 1; i >= 0; i--) + { + // If the position cannot be selected log an error. + if (PositionGetSymbol(i) == "") + { + PrintFormat(__FUNCTION__, ": ERROR - Unable to select the position - %s - %d.", GetLastErrorText(GetLastError()), GetLastError()); + continue; + } + if (PositionGetString(POSITION_SYMBOL) != Symbol()) continue; // Only close current symbol trades. + if (PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue; // Only close own positions. + + for (int try = 0; try < 10; try++) + { + bool result = Trade.PositionClose(PositionGetInteger(POSITION_TICKET)); + if (!result) + { + PrintFormat(__FUNCTION__, ": ERROR - Unable to close position: %s - %d", Trade.ResultRetcodeDescription(), Trade.ResultRetcode()); + } + else break; + } + } +} + +// Partially close a position with a given ticket. +bool PartialClose(ulong ticket, double percentage) +{ + if (!PositionSelectByTicket(ticket)) + { + PrintFormat("ERROR - Unable to select position by ticket #%d: %s - %d", ticket, GetLastErrorText(GetLastError()), GetLastError()); + return false; + } + double OriginalSize = PositionGetDouble(POSITION_VOLUME); + double Size = OriginalSize * percentage / 100; + double LotStep = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_STEP); + double MaxLot = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MAX); + double MinLot = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN); + Size = MathFloor(Size / LotStep) * LotStep; + if (Size < MinLot) return false; + if (!Trade.PositionClosePartial(ticket, Size)) + { + PrintFormat("ERROR - Unable to partially close position #%d: %s - %d", ticket, Trade.ResultRetcodeDescription(), Trade.ResultRetcode()); + return false; + } + return true; +} + +// Calculate a stop-loss price for an order. +double StopLoss(ENUM_ORDER_TYPE order_type, double open_price) +{ + double StopLossPrice = 0; + if (StopLossMode == SL_FIXED) // Easy way. + { + if (DefaultStopLoss == 0) return 0; + if (order_type == ORDER_TYPE_BUY) + { + StopLossPrice = open_price - DefaultStopLoss * SymbolInfoDouble(Symbol(), SYMBOL_POINT); + } + if (order_type == ORDER_TYPE_SELL) + { + StopLossPrice = open_price + DefaultStopLoss * SymbolInfoDouble(Symbol(), SYMBOL_POINT); + } + } + else // Special cases. + { + StopLossPrice = DynamicStopLossPrice(order_type, open_price); + } + return NormalizeDouble(StopLossPrice, (int)SymbolInfoInteger(Symbol(), SYMBOL_DIGITS)); +} + +// Calculate the take-profit price for an order. +double TakeProfit(ENUM_ORDER_TYPE order_type, double open_price) +{ + double TakeProfitPrice = 0; + if (TakeProfitMode == TP_FIXED) // Easy way. + { + if (DefaultTakeProfit == 0) return 0; + if (order_type == ORDER_TYPE_BUY) + { + TakeProfitPrice = open_price + DefaultTakeProfit * SymbolInfoDouble(Symbol(), SYMBOL_POINT); + } + if (order_type == ORDER_TYPE_SELL) + { + TakeProfitPrice = open_price - DefaultTakeProfit * SymbolInfoDouble(Symbol(), SYMBOL_POINT); + } + } + else // Special cases. + { + TakeProfitPrice = DynamicTakeProfitPrice(order_type, open_price); + } + return NormalizeDouble(TakeProfitPrice, (int)SymbolInfoInteger(Symbol(), SYMBOL_DIGITS)); +} + +// Calculate the position size for an order. +double LotSize(double stop_loss, double open_price) +{ + double Size = DefaultLotSize; + if (RiskDefaultSize == RISK_DEFAULT_AUTO) // If the position size is dynamic. + { + if (stop_loss != 0) // Calculate position size only if SL is non-zero, otherwise there will be a division by zero error. + { + double RiskBaseAmount = 0; + // TickValue is the value of the individual price increment for 1 lot of the instrument expressed in the account currency. + double TickValue = SymbolInfoDouble(Symbol(), SYMBOL_TRADE_TICK_VALUE); + // Define the base for the risk calculation depending on the parameter chosen + if (RiskBase == RISK_BASE_BALANCE) RiskBaseAmount = AccountBalance(); + else if (RiskBase == RISK_BASE_EQUITY) RiskBaseAmount = AccountEquity(); + else if (RiskBase == RISK_BASE_FREEMARGIN) RiskBaseAmount = AccountFreeMargin(); + double SL = MathAbs(open_price - stop_loss) / SymbolInfoDouble(Symbol(), SYMBOL_POINT); // SL as a number of points. + // Calculate the Position Size. + Size = (RiskBaseAmount * MaxRiskPerTrade / 100) / (SL * TickValue); + } + // If the stop loss is zero, then use the default size. + if (stop_loss == 0) + { + Size = DefaultLotSize; + } + } + + // Normalize the Lot Size to satisfy the allowed lot increment and minimum and maximum position size. + double LotStep = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_STEP); + double MaxLot = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MAX); + double MinLot = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN); + Size = MathFloor(Size / LotStep) * LotStep; + // Limit the lot size in case it is greater than the maximum allowed by the user. + if (Size > MaxLotSize) Size = MaxLotSize; + // Limit the lot size in case it is greater than the maximum allowed by the broker. + if (Size > MaxLot) Size = MaxLot; + // If the lot size is too small, then set it to 0 and don't trade. + if ((Size < MinLotSize) || (Size < MinLot)) Size = 0; + + return Size; +} + +// Utility functions + +// Checks to run at initialization to complete it. +bool Prechecks() +{ + // An example of a check to run here. + if (MaxLotSize < MinLotSize) + { + Print("MaxLotSize cannot be less than MinLotSize"); + return false; + } + return true; +} + +// Retrieve indicator data necessary for entry, update, and exit. +// Boolean type, so it can return true if all the data is available or false if it is not. +// Other advantage of this function is to move part of repetitive code into one location to make it leaner. +bool GetIndicatorsData() +{ + double buf[2]; // Needed for CopyBuffer(). + int count; // Will store the number of array elements returned by CopyBuffer(). + bool AllDataAvailable = false; + int MaxAttemptsForData = 5; + int DelayBetweenAttempts = 200; // Milliseconds. + int Attempt = 0; + + while ((!AllDataAvailable) && (Attempt < MaxAttemptsForData)) + { + AllDataAvailable = true; + + count = CopyBuffer(ATRHandle, 0, 0, 2, buf); // Copy using ATR indicator handle 2 latest values from 0th buffer to the buf array. + if ((count < 2) || (buf[0] == NULL) || (buf[0] == EMPTY_VALUE)) + { + Print("Unable to get ATR values."); + AllDataAvailable = false; + } + else + { + ATR_current = buf[1]; + ATR_previous = buf[0]; + } + + // This is where the main indicator data is read. + count = CopyBuffer(IndicatorHandle, 0, 1, 2, buf); // Copying using main indicator handle 2 latest completed candles (hence starting from the 1st, and not 0th, candle) from 0th buffer to the buf array. + if (count < 2) + { + Print("Main indicator buffer not ready yet."); + AllDataAvailable = false; + } + else + { + Indicator_current = buf[1]; + Indicator_previous = buf[0]; + } + + Attempt++; + Sleep(DelayBetweenAttempts); + } + + if (!AllDataAvailable) + { + Print("Unable to get some data for the entry signal, skipping candle."); + return false; + } + + return true; +} + +// Entry signal +void CheckEntrySignal() +{ + if ((UseTradingHours) && (!IsCurrentTimeInInterval(TradingHourStart, TradingHourEnd))) return; // Trading hours restrictions for entry. + + bool BuySignal = false; + bool SellSignal = false; + + // Buy signal conditions + + // This is where you should insert your entry signal for BUY orders. + // Include a condition to open a buy order, the condition will have to set BuySignal to true or false. + + //!! Uncomment and modify this buy entry signal check line: + if ((Indicator_current > RSIOversold) && (Indicator_previous <= RSIOversold)) BuySignal = true; // Check if the RSI crossed the oversold level from below. + + if (BuySignal) + { + OpenBuy(); + } + + // Sell signal conditions + + // This is where you should insert your entry signal for SELL orders. + // Include a condition to open a sell order, the condition will have to set SellSignal to true or false. + + //!! Uncomment and modify this sell entry signal check line: + if ((Indicator_current < RSIOverbought) && (Indicator_previous >= RSIOverbought)) SellSignal = true; // Check if the RSI crossed the overbought level from above. + + if (SellSignal) + { + OpenSell(); + } +} + +// Exit signal +void CheckExitSignal() +{ + //!! if ((UseTradingHours) && (!IsCurrentTimeInInterval(TradingHourStart, TradingHourEnd))) return; // Trading hours restrictions for exit. Normally, you don't want to restrict exit by hours. Still, it's a possibility. + + bool SignalExitLong = false; + bool SignalExitShort = false; + + //!! Uncomment and modify these exit signal checks: + if ((Indicator_current > RSIOversold) && (Indicator_previous <= RSIOversold)) SignalExitShort = true; // Check if the RSI crossed the oversold level from below. + else if ((Indicator_current < RSIOverbought) && (Indicator_previous >= RSIOverbought)) SignalExitLong = true; // Check if the RSI crossed the overbought level from above. + + if (SignalExitLong) CloseAllBuy(); + if (SignalExitShort) CloseAllSell(); +} + +// Dynamic stop-loss calculation +double DynamicStopLossPrice(ENUM_ORDER_TYPE type, double open_price) +{ + double StopLossPrice = 0; + if (type == ORDER_TYPE_BUY) + { + StopLossPrice = open_price - ATR_previous * ATRMultiplierSL; + } + else if (type == ORDER_TYPE_SELL) + { + StopLossPrice = open_price + ATR_previous * ATRMultiplierSL; + } + return NormalizeDouble(StopLossPrice, (int)SymbolInfoInteger(Symbol(), SYMBOL_DIGITS)); +} + +// Dynamic take-profit calculation +double DynamicTakeProfitPrice(ENUM_ORDER_TYPE type, double open_price) +{ + double TakeProfitPrice = 0; + if (type == ORDER_TYPE_BUY) + { + TakeProfitPrice = open_price + ATR_previous * ATRMultiplierTP; + } + else if (type == ORDER_TYPE_SELL) + { + TakeProfitPrice = open_price - ATR_previous * ATRMultiplierTP; + } + return NormalizeDouble(TakeProfitPrice, (int)SymbolInfoInteger(Symbol(), SYMBOL_DIGITS)); +} + +// Partially close all positions opened by this EA. +void PartialCloseAll() +{ + int total = PositionsTotal(); + + // Start a loop to scan all the positions. + // The loop starts from the last, otherwise it could skip positions. + for (int i = total - 1; i >= 0; i--) + { + // If the position cannot be selected log an error. + if (PositionGetSymbol(i) == "") + { + Print(__FUNCTION__, ": ERROR - Unable to select the position - ", GetLastError()); + continue; + } + if (PositionGetString(POSITION_SYMBOL) != Symbol()) continue; // Only close current symbol trades. + if (PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue; // Only close own positions. + + int position_ticket = (int)PositionGetInteger(POSITION_TICKET); + + // Retrieve the history of deals and orders for that position to check if it hasn't been already partially closed. + if (!HistorySelectByPosition(PositionGetInteger(POSITION_IDENTIFIER))) + { + PrintFormat("ERROR - Unable to get position history for %d - %s - %d", position_ticket, GetLastErrorText(GetLastError()), GetLastError()); + continue; + } + + bool need_partial_close = true; + + // Process partial close for a long position. + if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) + { + for (int j = HistoryDealsTotal() - 1; j >= 0; j--) + { + long deal_ticket = (int)HistoryDealGetTicket(j); + if (!deal_ticket) + { + PrintFormat("Unable to get deal for %d - %s - %d", position_ticket, GetLastErrorText(GetLastError()), GetLastError()); + break; + } + if (HistoryDealGetInteger(deal_ticket, DEAL_TYPE) == DEAL_TYPE_SELL) // Looks like this long position has already been partially closed at least once. + { + need_partial_close = false; + break; // No need to partially close this position. + } + } + // Condition for partial close of a long position. + if ((need_partial_close) && (SymbolInfoDouble(Symbol(), SYMBOL_BID) - PositionGetDouble(POSITION_PRICE_OPEN) > ATR_previous * ATRMultiplierPC)) + { + PartialClose(position_ticket, PartialClosePerc); + } + } + // Process partial close for a short position. + else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) + { + for (int j = HistoryDealsTotal() - 1; j >= 0; j--) + { + long deal_ticket = (int)HistoryDealGetTicket(j); + if (!deal_ticket) + { + PrintFormat("Unable to get deal for %d - %s - %d", position_ticket, GetLastErrorText(GetLastError()), GetLastError()); + return; + } + if (HistoryDealGetInteger(deal_ticket, DEAL_TYPE) == DEAL_TYPE_BUY) // Looks like this short position has already been partially closed at least once. + { + need_partial_close = false; + break; // No need to partially close this position. + } + } + // Condition for partial close of a short position. + if ((need_partial_close) && (PositionGetDouble(POSITION_PRICE_OPEN) - SymbolInfoDouble(Symbol(), SYMBOL_ASK) > ATR_previous * ATRMultiplierPC)) + { + PartialClose(position_ticket, PartialClosePerc); + } + return; + } + } +} +//+------------------------------------------------------------------+ \ No newline at end of file diff --git a/21-RSI-EA/RSI-EA.pdf b/21-RSI-EA/RSI-EA.pdf new file mode 100644 index 0000000..445b28e Binary files /dev/null and b/21-RSI-EA/RSI-EA.pdf differ diff --git a/22-Stochastic-EA/Stochastic-EA.mq5 b/22-Stochastic-EA/Stochastic-EA.mq5 new file mode 100644 index 0000000..371a237 --- /dev/null +++ b/22-Stochastic-EA/Stochastic-EA.mq5 @@ -0,0 +1,75 @@ +/** + * @copyright 2019, pipbolt.io + * @license https://github.com/pipbolt/experts/blob/master/LICENSE + */ + +#include + +#define NAME "Stochastic Oscillator EA" +#define VERSION "0.022" + +#property copyright COPYRIGHT +#property link LINK +#property icon ICON +#property description DESCRIPTION +#property version VERSION + +#include + +input group "Entry Strategy"; + +input group "Exit Strategy"; +input bool UseExitStrategy = false; // Use Exit Strategy + +input group "Stochastic Oscillator"; +input int StochKPeriod = 5; // %K Period +input int StochDPeriod = 3; // %D Period +input int StochSlowing = 3; // Slowing +input ENUM_MA_METHOD StochMethod = MODE_SMA; // Method +input ENUM_STO_PRICE StochPrice = STO_LOWHIGH; // Price +input int StochBuyLevel = 20; // Buy Level +input int StochSellLevel = 80; // Sell Level + +#include + +CiStochastic Stoch; + +int OnInit(void) +{ + if (ONINIT() != INIT_SUCCEEDED) + return INIT_FAILED; + + Stoch.Init(NULL, NULL, StochKPeriod, StochDPeriod, StochSlowing, StochMethod, StochPrice); + + return INIT_SUCCEEDED; +} + +void OnTick(void) { ONTICK(); } +void OnDeinit(const int reason) { ONDEINIT(reason); } +void OnTimer() { ONTIMER(); } + +void CheckForOpen(bool &openBuy, bool &openSell) +{ + // Buy Entry Strategy + if (Stoch.Signal(1) <= StochBuyLevel && Stoch.Main(1) <= Stoch.Signal(1) && + Stoch.Main(0) <= StochBuyLevel && Stoch.Signal(0) <= Stoch.Main(0)) + openBuy = true; + + // Sell Entry Strategy + else if (Stoch.Signal(1) >= StochSellLevel && Stoch.Main(1) >= Stoch.Signal(1) && + Stoch.Main(0) >= StochSellLevel && Stoch.Signal(0) >= Stoch.Main(0)) + openSell = true; + + // Apply MA Filter + openBuy = openBuy && MAFilter.Check(DIR_BUY); + openSell = openSell && MAFilter.Check(DIR_SELL); +} + +void CheckForClose(bool &closeBuy, bool &closeSell) +{ + // Buy Exit Strategy + closeBuy = Stoch.Main(0) >= StochSellLevel; + + // Sell Exit Strategy + closeSell = Stoch.Main(0) <= StochBuyLevel; +} \ No newline at end of file diff --git a/22-Stochastic-EA/Stochastic-EA.pdf b/22-Stochastic-EA/Stochastic-EA.pdf new file mode 100644 index 0000000..563edbc Binary files /dev/null and b/22-Stochastic-EA/Stochastic-EA.pdf differ diff --git a/23-Parabolic-SAR-EA/Parabolic-SAR-EA.mq5 b/23-Parabolic-SAR-EA/Parabolic-SAR-EA.mq5 new file mode 100644 index 0000000..bd6961a --- /dev/null +++ b/23-Parabolic-SAR-EA/Parabolic-SAR-EA.mq5 @@ -0,0 +1,76 @@ +/** + * @copyright 2019, pipbolt.io + * @license https://github.com/pipbolt/experts/blob/master/LICENSE + */ + +#include + +#define NAME "Parabolic SAR EA" +#define VERSION "0.022" + +#property copyright COPYRIGHT +#property link LINK +#property icon ICON +#property description DESCRIPTION +#property version VERSION + +#include + +input group "Entry Strategy"; + +input group "Exit Strategy"; +input bool UseExitStrategy = false; // Use Exit Strategy + +input group "Parabolic SAR"; +input double PSAR_Step = 0.02; // Step +input double PSAR_Maximum = 0.2; // Maximum + +#include + +CiSAR PSAR; + +int OnInit(void) +{ + if (ONINIT() != INIT_SUCCEEDED) + return INIT_FAILED; + + PSAR.Init(NULL, NULL, PSAR_Step, PSAR_Maximum); + + return INIT_SUCCEEDED; +} + +void OnTick(void) { ONTICK(); } +void OnDeinit(const int reason) { ONDEINIT(reason); } +void OnTimer() { ONTIMER(); } + +void CheckForOpen(bool &openBuy, bool &openSell) +{ + // Close variables + double close0 = iClose(NULL, NULL, _indicatorShift); + double close1 = iClose(NULL, NULL, _indicatorShift + 1); + + // Buy Entry Strategy + if (PSAR.Main(1) > close1 && PSAR.Main(0) < close0) + openBuy = true; + + // Sell Entry Strategy + else if (PSAR.Main(1) < close1 && PSAR.Main(0) > close0) + openSell = true; + + // Apply MA Filter + openBuy = openBuy && MAFilter.Check(DIR_BUY); + openSell = openSell && MAFilter.Check(DIR_SELL); +} + +void CheckForClose(bool &closeBuy, bool &closeSell) +{ + // Close variables + double close0 = iClose(NULL, NULL, _indicatorShift); + double close1 = iClose(NULL, NULL, _indicatorShift + 1); + + // Buy Exit Strategy + closeBuy = PSAR.Main(0) > close0 && PSAR.Main(1) < close1; + + // Sell Exit Strategy + closeSell = PSAR.Main(0) < close0 && PSAR.Main(1) > close1; +} diff --git a/23-Parabolic-SAR-EA/Parabolic-SAR-EA.pdf b/23-Parabolic-SAR-EA/Parabolic-SAR-EA.pdf new file mode 100644 index 0000000..cb43367 Binary files /dev/null and b/23-Parabolic-SAR-EA/Parabolic-SAR-EA.pdf differ diff --git a/24-Daily-Range-Breakout/Daily-Range-Breakout.mq5 b/24-Daily-Range-Breakout/Daily-Range-Breakout.mq5 new file mode 100644 index 0000000..55a13b8 --- /dev/null +++ b/24-Daily-Range-Breakout/Daily-Range-Breakout.mq5 @@ -0,0 +1,253 @@ +//+------------------------------------------------------------------+ +//| DAILY RANGE BREAKOUT EA.mq5 | +//| Copyright 2024, MetaQuotes Ltd. | +//| https://www.mql5.com | +//+------------------------------------------------------------------+ +#property copyright "Copyright 2024, MetaQuotes Ltd." +#property link "https://www.mql5.com" +#property version "1.00" + +double maximum_price = -DBL_MAX; +double minimum_price = +DBL_MAX; +datetime maximum_time, minimum_time; + +bool isHaveDailyRange_Prices = false; +bool isHaveDailyRange_Break = false; + +#define RECTANGLE_PREFIX "RANGE RECTANGLE " +#define UPPER_LINE_PREFIX "UPPER LINE" +#define LOWER_LINE_PREFIX "LOWER LINE" + +//+------------------------------------------------------------------+ +//| Expert initialization function | +//+------------------------------------------------------------------+ +int OnInit(){ +//--- + +//--- + return(INIT_SUCCEEDED); +} +//+------------------------------------------------------------------+ +//| Expert deinitialization function | +//+------------------------------------------------------------------+ +void OnDeinit(const int reason){ +//--- + +} +//+------------------------------------------------------------------+ +//| Expert tick function | +//+------------------------------------------------------------------+ +void OnTick(){ +//--- + + static datetime midnight = iTime(_Symbol,PERIOD_D1,0); + static datetime sixAM = midnight + 6 * 3600; + static datetime scanBarTime = sixAM + 1 * PeriodSeconds(_Period); // next bar + + static datetime validBreakTime_start = scanBarTime; + static datetime validBreakTime_end = midnight + (6+5) * 3600; // 11 AM + + //Print("END TIME = ",validBreakTime_end); + + //Print("Midnight T = ",midnight,", 6 AM = ",sixAM,", SCAN BAT T = ",scanBarTime); + + if (isNewDay()){ + + midnight = iTime(_Symbol,PERIOD_D1,0); + sixAM = midnight + 6 * 3600; + scanBarTime = sixAM + 1 * PeriodSeconds(_Period); // next bar + + validBreakTime_start = scanBarTime; + validBreakTime_end = midnight + (6+5) * 3600; // 11 AM + + maximum_price = -DBL_MAX; + minimum_price = +DBL_MAX; + + isHaveDailyRange_Prices = false; + isHaveDailyRange_Break = false; + } + + if (isNewBar()){ + datetime currentBarTime = iTime(_Symbol,_Period,0); + + if (currentBarTime == scanBarTime && !isHaveDailyRange_Prices){ + Print("WE HAVE ENOUGH BARS DATA FOR DOCUMENTATION. MAKE THE DATA EXTRACTION NOW"); + int total_bars = int((sixAM - midnight)/PeriodSeconds(_Period))+1; + Print("Total bars for scan = ",total_bars); + int highest_price_bar_index = -1; + int lowest_price_bar_index = -1; + + for (int i=1; i<=total_bars; i++){ + double open_i = open(i); + double close_i = close(i); + + double highest_price_i = (open_i > close_i) ? open_i : close_i; + double lowest_price_i = (open_i < close_i) ? open_i : close_i; + + if (highest_price_i > maximum_price){ + maximum_price = highest_price_i; + highest_price_bar_index = i; + maximum_time = time(i); + } + if (lowest_price_i < minimum_price){ + minimum_price = lowest_price_i; + lowest_price_bar_index = i; + minimum_time = time(i); + } + } + Print("Maximum Price = ",maximum_price,", Bar Index = ",highest_price_bar_index,", Time = ",maximum_time); + Print("Minimum Price = ",minimum_price,", Bar Index = ",lowest_price_bar_index,", Time = ",minimum_time); + + create_Rectangle(RECTANGLE_PREFIX+TimeToString(maximum_time),maximum_time,maximum_price,minimum_time,minimum_price,clrBlue); + create_Line(UPPER_LINE_PREFIX+TimeToString(midnight),midnight,maximum_price,sixAM,maximum_price,3,clrBlack,DoubleToString(maximum_price,_Digits)); + create_Line(LOWER_LINE_PREFIX+TimeToString(midnight),midnight,minimum_price,sixAM,minimum_price,3,clrRed,DoubleToString(minimum_price,_Digits)); + + isHaveDailyRange_Prices = true; + } + } + + double barClose = close(1); + datetime barTime = time(1); + + if (barClose > maximum_price && isHaveDailyRange_Prices && !isHaveDailyRange_Break + && barTime >= validBreakTime_start && barTime <= validBreakTime_end + ){ + Print("CLOSE Price broke the HIGH range. ",barClose," > ",maximum_price); + isHaveDailyRange_Break = true; + drawBreakPoint(TimeToString(barTime),barTime,barClose,234,clrBlack,-1); + } + else if (barClose < minimum_price && isHaveDailyRange_Prices && !isHaveDailyRange_Break + && barTime >= validBreakTime_start && barTime <= validBreakTime_end + ){ + Print("CLOSE Price broke the LOW range. ",barClose," < ",minimum_price); + isHaveDailyRange_Break = true; + drawBreakPoint(TimeToString(barTime),barTime,barClose,233,clrBlue,+1); + } + + +} +//+------------------------------------------------------------------+ + +double open(int index){return (iOpen(_Symbol,_Period,index));} +double high(int index){return (iHigh(_Symbol,_Period,index));} +double low(int index){return (iLow(_Symbol,_Period,index));} +double close(int index){return (iClose(_Symbol,_Period,index));} +datetime time(int index){return (iTime(_Symbol,_Period,index));} + + + + +bool isNewBar(){ + static int prevbars = 0; + int currbars = iBars(_Symbol,_Period); + if (prevbars == currbars) return (false); + prevbars = currbars; + return (true); +} + +void create_Rectangle(string objName,datetime time1,double price1, + datetime time2,double price2,color clr){ + if (ObjectFind(0,objName) < 0){ + ObjectCreate(0,objName,OBJ_RECTANGLE,0,time1,price1,time2,price2); + + ObjectSetInteger(0,objName,OBJPROP_TIME,0,time1); + ObjectSetDouble(0,objName,OBJPROP_PRICE,0,price1); + ObjectSetInteger(0,objName,OBJPROP_TIME,1,time2); + ObjectSetDouble(0,objName,OBJPROP_PRICE,1,price2); + + ObjectSetInteger(0,objName,OBJPROP_FILL,true); + ObjectSetInteger(0,objName,OBJPROP_COLOR,clr); + ObjectSetInteger(0,objName,OBJPROP_BACK,false); + + ChartRedraw(0); + } +} + +bool isNewDay(){ + bool newDay = false; + + MqlDateTime str_datetime; + TimeToStruct(TimeCurrent(), str_datetime); + + static int prevday = 0; + int currday = str_datetime.day; + + if (prevday == currday){// we are still in current day + newDay = false; + } + else if (prevday != currday){// we have a new day + Print("WE HAVE A NEW DAY WITH DATE ",currday); + prevday = currday; + newDay = true; + } + return (newDay); +} + +void create_Line(string objName,datetime time1,double price1, + datetime time2,double price2,int width,color clr,string text){ + if (ObjectFind(0,objName) < 0){ + ObjectCreate(0,objName,OBJ_TREND,0,time1,price1,time2,price2); + + ObjectSetInteger(0,objName,OBJPROP_TIME,0,time1); + ObjectSetDouble(0,objName,OBJPROP_PRICE,0,price1); + ObjectSetInteger(0,objName,OBJPROP_TIME,1,time2); + ObjectSetDouble(0,objName,OBJPROP_PRICE,1,price2); + + ObjectSetInteger(0,objName,OBJPROP_WIDTH,width); + ObjectSetInteger(0,objName,OBJPROP_COLOR,clr); + ObjectSetInteger(0,objName,OBJPROP_BACK,false); + + long scale = 0; + if (!ChartGetInteger(0,CHART_SCALE,0,scale)){ + Print("UNABLE TO GET THE CHART SCALE. DEFAULT VALUE OF ",scale," IS CONSIDERED."); + } + + int fontsize = 11; + // 0=minimized, 5 = maximized + if (scale==0){fontsize=5;} + else if (scale==1){fontsize=6;} + else if (scale==2){fontsize=7;} + else if (scale==3){fontsize=9;} + else if (scale==4){fontsize=11;} + else if (scale==5){fontsize=13;} + + string txt = " Right Price"; + string objNameDescr = objName + txt; + ObjectCreate(0,objNameDescr,OBJ_TEXT,0,time2,price2); + ObjectSetInteger(0,objNameDescr,OBJPROP_COLOR,clr); + ObjectSetInteger(0,objNameDescr,OBJPROP_FONTSIZE,fontsize); + ObjectSetInteger(0,objNameDescr,OBJPROP_ANCHOR,ANCHOR_LEFT); + ObjectSetString(0,objNameDescr,OBJPROP_TEXT, " " + text); + ObjectSetString(0,objNameDescr,OBJPROP_FONT,"Calibri"); + + ChartRedraw(0); + } +} + +void drawBreakPoint(string objName,datetime time,double price,int arrCode, + color clr,int direction){ + + if (ObjectFind(0,objName) < 0){ + ObjectCreate(0,objName,OBJ_ARROW,0,time,price); + ObjectSetInteger(0,objName,OBJPROP_ARROWCODE,arrCode); + ObjectSetInteger(0,objName,OBJPROP_COLOR,clr); + ObjectSetInteger(0,objName,OBJPROP_FONTSIZE,12); + if (direction > 0) ObjectSetInteger(0,objName,OBJPROP_ANCHOR,ANCHOR_TOP); + if (direction < 0) ObjectSetInteger(0,objName,OBJPROP_ANCHOR,ANCHOR_BOTTOM); + + string txt = " Breakout"; + string objNameDescr = objName + txt; + ObjectCreate(0,objNameDescr,OBJ_TEXT,0,time,price); + ObjectSetInteger(0,objNameDescr,OBJPROP_COLOR,clr); + ObjectSetInteger(0,objNameDescr,OBJPROP_FONTSIZE,12); + if (direction > 0) { + ObjectSetInteger(0,objNameDescr,OBJPROP_ANCHOR,ANCHOR_LEFT_UPPER); + ObjectSetString(0,objNameDescr,OBJPROP_TEXT, " " + txt); + } + if (direction < 0) { + ObjectSetInteger(0,objNameDescr,OBJPROP_ANCHOR,ANCHOR_LEFT_LOWER); + ObjectSetString(0,objNameDescr,OBJPROP_TEXT, " " + txt); + } + } + ChartRedraw(0); +} diff --git a/24-Daily-Range-Breakout/Daily-Range-Breakout.pdf b/24-Daily-Range-Breakout/Daily-Range-Breakout.pdf new file mode 100644 index 0000000..92d7703 Binary files /dev/null and b/24-Daily-Range-Breakout/Daily-Range-Breakout.pdf differ diff --git a/25-JamesORB/JamesORB.mq4 b/25-JamesORB/JamesORB.mq4 new file mode 100644 index 0000000..bcd1b9a --- /dev/null +++ b/25-JamesORB/JamesORB.mq4 @@ -0,0 +1,206 @@ +//+------------------------------------------------------------------+ +//| JamesOBR.mq4 | +//| Copyright 2012,Clifford H. James | +//| | +//+------------------------------------------------------------------+ + +#include + +#property copyright "Copyright 2012,Clifford H. James" +#property link "" + + +// CONSTANTS +extern double OBR_PIP_OFFSET = 0.0002; +extern int EET_START = 10; +extern double OBR_RATIO = 1.9; +extern double ATR_PERIOD = 72; + + + +//+------------------------------------------------------------------+ +//| expert initialization function | +//+------------------------------------------------------------------+ +int init() + { +//--- + + +//---- + return(0); + } +//+------------------------------------------------------------------+ +//| expert deinitialization function | +//+------------------------------------------------------------------+ +int deinit() + { +//---- + +//---- + return(0); + } + + +//--- +// calculates the ORB +//--- +double CalcCurrORB() +{ + // Get the ATR of the 10EET Bar...we run on the + double currATR = iATR(NULL, 0, ATR_PERIOD, 1); + //Print("Curr ATR(72): ", currATR); + return (currATR + OBR_PIP_OFFSET); +} + + + +//--- +// Generate Daily pending orders based on the specified ORB value +// This will generate both a BUY_STOP and SELL_STOP pending order. +//--- +void generateDailyPendingOrders(double orbval) +{ + + double tenEETHi = High[1]; //Goes back 1 bar to compute 10EET bar high + double tenEETLo = Low[1]; // Goes back 1 bar to compute 10EET bar low + int slippage = 2; + + double buyEntry = tenEETHi + orbval; + double SL = buyEntry - (1.65 * orbval); + double TP = buyEntry + orbval; + double SL_Dist = RelDistToPoints(SL); + double TP_Dist = RelDistToPoints(TP); + int lotSize = 1; + + Alert("Current Price: ", Bid,"/",Ask); + + // buy side + PlacePendingStopOrder( + OP_BUYSTOP, + Symbol(), + buyEntry, + lotSize, + SL_Dist, + TP_Dist + ); + + double sellEntry = tenEETLo - orbval; + SL = sellEntry + (1.65 * orbval); + TP = sellEntry - orbval; + SL_Dist = RelDistToPoints(SL); + TP_Dist = RelDistToPoints(TP); + + // sell side + PlacePendingStopOrder( + OP_SELLSTOP, + Symbol(), + sellEntry, + lotSize, + SL_Dist, + TP_Dist + ); + + +} + +//--- +// determine if we are at the close of the day or not +//--- +bool AtCloseOfDay() { + int currHour=TimeHour(TimeCurrent()); + int currMin=TimeMinute(TimeCurrent()); + return(currHour == 17 && currMin == 30); + +} + + +//------------------------------------------------------------------------------- +// Calculate trade volume (lot size) for the current symbol based on: +// - Current free margin in your account +// - SL dist in points +// - Desired risk % (0-100) +// - Tick Value of current symbol +// +// Additional MIN/MAX lot constraints for the current symbold are applied +// +// - If you are requesting a volume < minLots for the current symbol +// then -1 is returned, this indicates that the current trade cannot be made +// +// - If you are requesting a volume > maxLots for the current symbol +// then your trade volume is effectively "capped" at maxLotSize +//------------------------------------------------------------------------------- +double calcTradeVolume(double risk, double stopLossPoints) +{ + double minLotAllowed = MarketInfo(Symbol(), MODE_MINLOT); + double maxLotAllowed = MarketInfo(Symbol(), MODE_MAXLOT); + + double vol = (AccountFreeMargin() * (risk/100)) / + ( stopLossPoints * MarketInfo(Symbol(), MODE_TICKVALUE) ); + + if(vol < minLotAllowed) + vol = -1.0; + if(vol > maxLotAllowed) + vol = maxLotAllowed; + + return(vol); +} + +double calcSLDist(double entryPrice, double stopLossPrice) +{ + return(-1.0); +} + + + + +//+------------------------------------------------------------------+ +//| expert start function | +//+------------------------------------------------------------------+ +int start() + { + //---- + // TODO: Check some start conditions ... + + // little magic to detect new bars ... + static datetime Time0; + static bool processedClose; + + + //int currMin = TimeMinute(Time[0]); + + if(AtCloseOfDay()) { + if(!processedClose) { + //Alert("Got Close of Day @: ", TimeToStr(Time[0],TIME_DATE|TIME_MINUTES)); + //printInfo(); + CloseAllOutstandingOrders(); + processedClose = true; + } + return(0); + } + processedClose = false; + + // check for first bar of the hour ... + if (Time0 == Time[0]) return; + Time0 = Time[0]; + int currHour = TimeHour(Time[0]); + + //Alert("Got a new bar at time: ", TimeToStr(Time[0],TIME_DATE|TIME_MINUTES)); + double currOrb = 0; + if(currHour == 11) { + currOrb = CalcCurrORB(); + + //Alert("ORB value on: ", TimeToStr(Time[0],TIME_DATE|TIME_MINUTES), " is: ", currOrb); + + // generate daily pending orders for buy/sell + generateDailyPendingOrders(currOrb); + } + + + +//---- + return(0); + } +//+------------------------------------------------------------------+ + + + diff --git a/25-JamesORB/JamesORB.pdf b/25-JamesORB/JamesORB.pdf new file mode 100644 index 0000000..9ef5d4e Binary files /dev/null and b/25-JamesORB/JamesORB.pdf differ diff --git a/26-Asian-Breakout/Asian-Breakout.mq4 b/26-Asian-Breakout/Asian-Breakout.mq4 new file mode 100644 index 0000000..b734071 --- /dev/null +++ b/26-Asian-Breakout/Asian-Breakout.mq4 @@ -0,0 +1,328 @@ +/* +============================================================ +Demo File: Asia_Session_Breakout_EA - Signal Logic Showcase +Category: Session Breakout +Platform: MetaTrader 4 (MQL4) +Version: 1.0 +Author: Giacomo Cipolat Bares +Portfolio: MQL4 Expert Advisors Portfolio +============================================================ + +Description: +This is a simplified public demo derived from the full +Asia Session Breakout EA. + +Included in this demo: +- Asia session high/low detection +- session range calculation +- breakout trigger calculation +- Asia range filter +- late breakout filter +- basic on-chart signal output + +Excluded from this demo: +- order execution +- pending order management +- risk management engine +- break-even / trailing stop +- retry logic +- broker protection handling +- full production trade framework +- chart visualization layer +============================================================ +*/ + +#property strict +#property version "1.00" + +//========================= INPUTS ================================== +input string __01_SessionSettings = "01 ======== Session Settings ========"; +input int AsiaStartHour = 0; +input int AsiaEndHour = 6; +input int LondonStartHour = 7; +input int LondonEndHour = 12; +input int SessionCalculationTimeframe = PERIOD_M1; + +input string __02_BreakoutSettings = "02 ======== Breakout Settings ========"; +input double BreakoutBufferPips = 1.0; +input bool UseAsiaRangeFilter = true; +input bool NoLateBreakout = true; +input double MinAsiaRangePips = 5.0; +input double MaxAsiaRangePips = 30.0; + +//======================= GLOBALS =================================== +double g_point; +double g_pip; +int g_digits; + +double g_asiaHigh = -1.0; +double g_asiaLow = -1.0; +bool g_asiaRangeFinalized = false; +datetime g_lastSessionDay = -1; +datetime g_lastBarTime = 0; + +//+------------------------------------------------------------------+ +//| Expert initialization | +//+------------------------------------------------------------------+ +int OnInit() +{ + g_point = Point; + g_digits = Digits; + + if(g_digits == 5 || g_digits == 3) + g_pip = g_point * 10.0; + else + g_pip = g_point; + + Print("Asia Session Breakout demo initialized"); + return(INIT_SUCCEEDED); +} + +//+------------------------------------------------------------------+ +//| Detects a new bar | +//+------------------------------------------------------------------+ +bool IsNewBar() +{ + datetime currentBarTime = iTime(NULL, 0, 0); + + if(currentBarTime != g_lastBarTime) + { + g_lastBarTime = currentBarTime; + return true; + } + + return false; +} + +//+------------------------------------------------------------------+ +//| Current hour helper | +//+------------------------------------------------------------------+ +int CurrentHour() +{ + return TimeHour(TimeCurrent()); +} + +//+------------------------------------------------------------------+ +//| Session checks | +//+------------------------------------------------------------------+ +bool IsInsideAsiaSession() +{ + int hour = TimeHour(TimeCurrent()); + return (hour >= AsiaStartHour && hour < AsiaEndHour); +} + +bool IsLondonSession() +{ + int hour = CurrentHour(); + return (hour >= LondonStartHour && hour < LondonEndHour); +} + +//+------------------------------------------------------------------+ +//| Date helper | +//+------------------------------------------------------------------+ +datetime DateOfDay(datetime t) +{ + return t - (t % 86400); +} + +//+------------------------------------------------------------------+ +//| Session time builders | +//+------------------------------------------------------------------+ +datetime GetSessionStart(datetime dayStart, int startHour) +{ + return dayStart + startHour * 3600; +} + +datetime GetSessionEnd(datetime dayStart, int endHour) +{ + return dayStart + endHour * 3600; +} + +//+------------------------------------------------------------------+ +//| Calculates Asia session high/low | +//+------------------------------------------------------------------+ +bool CalculateAsiaRange(double &asiaHigh, double &asiaLow) +{ + datetime today = DateOfDay(TimeCurrent()); + + datetime asiaStart = GetSessionStart(today, AsiaStartHour); + datetime asiaEnd = GetSessionEnd(today, AsiaEndHour); + + int tf = SessionCalculationTimeframe; + + int startShift = iBarShift(Symbol(), tf, asiaEnd - 1, false); + int endShift = iBarShift(Symbol(), tf, asiaStart, false); + + if(startShift < 0 || endShift < 0) + return false; + + int count = endShift - startShift + 1; + + if(count <= 0) + return false; + + int highestShift = iHighest(Symbol(), tf, MODE_HIGH, count, startShift); + int lowestShift = iLowest(Symbol(), tf, MODE_LOW, count, startShift); + + if(highestShift < 0 || lowestShift < 0) + return false; + + asiaHigh = iHigh(Symbol(), tf, highestShift); + asiaLow = iLow(Symbol(), tf, lowestShift); + + return (asiaHigh > 0 && asiaLow > 0); +} + +//+------------------------------------------------------------------+ +//| Updates tracked Asia range | +//+------------------------------------------------------------------+ +void UpdateAsiaRange() +{ + if(g_asiaRangeFinalized) + return; + + double asiaHigh = -1.0; + double asiaLow = -1.0; + + if(CalculateAsiaRange(asiaHigh, asiaLow)) + { + g_asiaHigh = asiaHigh; + g_asiaLow = asiaLow; + } + + datetime today = DateOfDay(TimeCurrent()); + datetime asiaEnd = GetSessionEnd(today, AsiaEndHour); + + if(TimeCurrent() >= asiaEnd) + g_asiaRangeFinalized = true; +} + +//+------------------------------------------------------------------+ +//| Reset session state daily | +//+------------------------------------------------------------------+ +void ResetSessionState() +{ + datetime today = DateOfDay(TimeCurrent()); + + if(today != g_lastSessionDay) + { + g_lastSessionDay = today; + g_asiaHigh = -1.0; + g_asiaLow = -1.0; + g_asiaRangeFinalized = false; + } +} + +//+------------------------------------------------------------------+ +//| Range and trigger helpers | +//+------------------------------------------------------------------+ +double GetAsiaRangePips() +{ + if(g_asiaHigh <= 0 || g_asiaLow <= 0) + return 0.0; + + return (g_asiaHigh - g_asiaLow) / g_pip; +} + +bool AsiaRangeFilterPassed() +{ + if(!UseAsiaRangeFilter) + return true; + + double asiaRangePips = GetAsiaRangePips(); + + if(asiaRangePips < MinAsiaRangePips) + return false; + + if(asiaRangePips > MaxAsiaRangePips) + return false; + + return true; +} + +double GetBuyTrigger() +{ + return g_asiaHigh + BreakoutBufferPips * g_pip; +} + +double GetSellTrigger() +{ + return g_asiaLow - BreakoutBufferPips * g_pip; +} + +//+------------------------------------------------------------------+ +//| Late breakout filter | +//+------------------------------------------------------------------+ +bool LateBreakout() +{ + if(!NoLateBreakout) + return true; + + if(Ask > GetBuyTrigger() || Bid < GetSellTrigger()) + return false; + + return true; +} + +//+------------------------------------------------------------------+ +//| Demo breakout signal wrappers | +//+------------------------------------------------------------------+ +bool BuySignal() +{ + if(!IsLondonSession()) + return false; + + if(!AsiaRangeFilterPassed()) + return false; + + if(!LateBreakout()) + return false; + + if(g_asiaHigh <= 0 || g_asiaLow <= 0) + return false; + + return (Ask > GetBuyTrigger()); +} + +bool SellSignal() +{ + if(!IsLondonSession()) + return false; + + if(!AsiaRangeFilterPassed()) + return false; + + if(!LateBreakout()) + return false; + + if(g_asiaHigh <= 0 || g_asiaLow <= 0) + return false; + + return (Bid < GetSellTrigger()); +} + +//+------------------------------------------------------------------+ +//| Expert tick | +//+------------------------------------------------------------------+ +void OnTick() +{ + if(!IsNewBar()) + return; + + ResetSessionState(); + UpdateAsiaRange(); + + if(BuySignal()) + { + Comment("Demo Signal: BUY Asia breakout detected"); + return; + } + + if(SellSignal()) + { + Comment("Demo Signal: SELL Asia breakout detected"); + return; + } + + Comment("Demo Signal: No valid Asia breakout"); +} \ No newline at end of file diff --git a/26-Asian-Breakout/Asian-Breakout.pdf b/26-Asian-Breakout/Asian-Breakout.pdf new file mode 100644 index 0000000..f968da3 Binary files /dev/null and b/26-Asian-Breakout/Asian-Breakout.pdf differ diff --git a/27-Trendline-Breakout/Trendline-Breakout.mq5 b/27-Trendline-Breakout/Trendline-Breakout.mq5 new file mode 100644 index 0000000..801677d --- /dev/null +++ b/27-Trendline-Breakout/Trendline-Breakout.mq5 @@ -0,0 +1,647 @@ +//+------------------------------------------------------------------+ +//| Trendline Breakout Trader EA.mq5 | +//| Copyright 2025, Allan Munene Mutiiria. | +//| https://t.me/Forex_Algo_Trader | +//+------------------------------------------------------------------+ +#property copyright "Copyright 2025, Allan Munene Mutiiria." +#property link "https://t.me/Forex_Algo_Trader" +#property version "1.00" +#property strict + +#include //--- Include Trade library for trading operations +CTrade obj_Trade; //--- Instantiate trade object +//+------------------------------------------------------------------+ +//| Breakout definition enumeration | +//+------------------------------------------------------------------+ +enum ENUM_BREAKOUT_TYPE { + BREAKOUT_CLOSE = 0, // Breakout on close above/below line + BREAKOUT_CANDLE = 1 // Breakout on entire candle above/below line +}; +//+------------------------------------------------------------------+ +//| Swing point structure | +//+------------------------------------------------------------------+ +struct Swing { //--- Define swing point structure + datetime time; //--- Store swing time + double price; //--- Store swing price +}; +//+------------------------------------------------------------------+ +//| Starting point structure | +//+------------------------------------------------------------------+ +struct StartingPoint { //--- Define starting point structure + datetime time; //--- Store starting point time + double price; //--- Store starting point price + bool is_support; //--- Indicate support/resistance flag +}; +//+------------------------------------------------------------------+ +//| Trendline storage structure | +//+------------------------------------------------------------------+ +struct TrendlineInfo { //--- Define trendline info structure + string name; //--- Store trendline name + datetime start_time; //--- Store start time + datetime end_time; //--- Store end time + double start_price; //--- Store start price + double end_price; //--- Store end price + double slope; //--- Store slope + bool is_support; //--- Indicate support/resistance flag + int touch_count; //--- Store number of touches + datetime creation_time; //--- Store creation time + int touch_indices[]; //--- Store touch indices array + bool is_signaled; //--- Indicate signal flag +}; +//+------------------------------------------------------------------+ +//| Forward declarations | +//+------------------------------------------------------------------+ +void DetectSwings(); //--- Declare swing detection function +void SortSwings(Swing &swings[], int count); //--- Declare swing sorting function +double CalculateAngle(datetime time1, double price1, datetime time2, double price2); //--- Declare angle calculation function +bool ValidateTrendline(bool isSupport, datetime start_time, datetime ref_time, double ref_price, double slope, double tolerance_pen); //--- Declare trendline validation function +void FindAndDrawTrendlines(bool isSupport); //--- Declare trendline finding/drawing function +void UpdateTrendlines(); //--- Declare trendline update function +void RemoveTrendlineFromStorage(int index); //--- Declare trendline removal function +bool IsStartingPointUsed(datetime time, double price, bool is_support); //--- Declare starting point usage check function +double CalculateRSquared(const datetime ×[], const double &prices[], int n, double slope, double intercept); //--- Declare R-squared calculation function +//+------------------------------------------------------------------+ +//| Inputs | +//+------------------------------------------------------------------+ +input ENUM_BREAKOUT_TYPE BreakoutType = BREAKOUT_CLOSE; // Breakout Definition +input int LookbackBars = 200; // Set bars for swing detection lookback +input double TouchTolerance = 10.0; // Set tolerance for touch points (points) +input int MinTouches = 3; // Set minimum touch points for valid trendline +input double PenetrationTolerance = 5.0; // Set allowance for bar penetration (points) +input int ExtensionBars = 100; // Set bars to extend trendline right +input int MinBarSpacing = 10; // Set minimum bar spacing between touches +input double inpLot = 0.01; // Set lot size +input double inpSLPoints = 100.0; // Set stop loss (points) +input double inpRRRatio = 1.1; // Set risk:reward ratio +input double MinAngle = 1.0; // Set minimum inclination angle (degrees) +input double MaxAngle = 89.0; // Set maximum inclination angle (degrees) +input double MinRSquared = 0.8; // Minimum R-squared for trendline acceptance +input bool DeleteExpiredObjects = false; // Enable deletion of expired/broken objects +input bool EnableTradingSignals = true; // Enable buy/sell signals and trades +input bool DrawTouchArrows = true; // Enable drawing arrows at touch points +input bool DrawLabels = true; // Enable drawing trendline/point labels +input color SupportLineColor = clrGreen; // Set color for support trendlines +input color ResistanceLineColor = clrRed; // Set color for resistance trendlines +//+------------------------------------------------------------------+ +//| Global variables | +//+------------------------------------------------------------------+ +Swing swingLows[]; //--- Store swing lows +int numLows = 0; //--- Track number of swing lows +Swing swingHighs[]; //--- Store swing highs +int numHighs = 0; //--- Track number of swing highs +TrendlineInfo trendlines[]; //--- Store trendlines +int numTrendlines = 0; //--- Track number of trendlines +StartingPoint startingPoints[]; //--- Store used starting points +int numStartingPoints = 0; //--- Track number of starting points +//+------------------------------------------------------------------+ +//| Expert initialization function | +//+------------------------------------------------------------------+ +int OnInit() { + ArrayResize(trendlines, 0); //--- Resize trendlines array + numTrendlines = 0; //--- Reset trendlines count + ArrayResize(startingPoints, 0); //--- Resize starting points array + numStartingPoints = 0; //--- Reset starting points count + return(INIT_SUCCEEDED); //--- Return success +} +//+------------------------------------------------------------------+ +//| Expert deinitialization function | +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) { + ArrayResize(trendlines, 0); //--- Resize trendlines array + numTrendlines = 0; //--- Reset trendlines count + ArrayResize(startingPoints, 0); //--- Resize starting points array + numStartingPoints = 0; //--- Reset starting points count +} +//+------------------------------------------------------------------+ +//| Expert tick function | +//+------------------------------------------------------------------+ +void OnTick() { + if (!IsNewBar()) return; //--- Exit if not new bar + DetectSwings(); //--- Detect swings + UpdateTrendlines(); //--- Update trendlines + FindAndDrawTrendlines(true); //--- Find/draw support trendlines + FindAndDrawTrendlines(false); //--- Find/draw resistance trendlines +} +//+------------------------------------------------------------------+ +//| Check for new bar | +//+------------------------------------------------------------------+ +bool IsNewBar() { + static datetime lastTime = 0; //--- Store last bar time + datetime currentTime = iTime(_Symbol, _Period, 0); //--- Get current bar time + if (lastTime != currentTime) { //--- Check for new bar + lastTime = currentTime; //--- Update last time + return true; //--- Indicate new bar + } + return false; //--- Indicate no new bar +} +//+------------------------------------------------------------------+ +//| Sort swings by time (ascending, oldest first) | +//+------------------------------------------------------------------+ +void SortSwings(Swing &swings[], int count) { + for (int i = 0; i < count - 1; i++) { //--- Iterate through swings + for (int j = 0; j < count - i - 1; j++) { //--- Compare adjacent swings + if (swings[j].time > swings[j + 1].time) { //--- Check time order + Swing temp = swings[j]; //--- Store temporary swing + swings[j] = swings[j + 1]; //--- Swap swings + swings[j + 1] = temp; //--- Complete swap + } + } + } +} +//+------------------------------------------------------------------+ +//| Detect swing highs and lows | +//+------------------------------------------------------------------+ +void DetectSwings() { + numLows = 0; //--- Reset lows count + ArrayResize(swingLows, 0); //--- Resize lows array + numHighs = 0; //--- Reset highs count + ArrayResize(swingHighs, 0); //--- Resize highs array + int totalBars = iBars(_Symbol, _Period); //--- Get total bars + int effectiveLookback = MathMin(LookbackBars, totalBars); //--- Calculate effective lookback + if (effectiveLookback < 5) { //--- Check sufficient bars + Print("Not enough bars for swing detection."); //--- Log insufficient bars + return; //--- Exit function + } + for (int i = 2; i < effectiveLookback - 2; i++) { //--- Iterate through bars + double low_i = iLow(_Symbol, _Period, i); //--- Get current low + double low_im1 = iLow(_Symbol, _Period, i - 1); //--- Get previous low + double low_im2 = iLow(_Symbol, _Period, i - 2); //--- Get two bars prior low + double low_ip1 = iLow(_Symbol, _Period, i + 1); //--- Get next low + double low_ip2 = iLow(_Symbol, _Period, i + 2); //--- Get two bars next low + if (low_i < low_im1 && low_i < low_im2 && low_i < low_ip1 && low_i < low_ip2) { //--- Check for swing low + Swing s; //--- Create swing struct + s.time = iTime(_Symbol, _Period, i); //--- Set swing time + s.price = low_i; //--- Set swing price + ArrayResize(swingLows, numLows + 1); //--- Resize lows array + swingLows[numLows] = s; //--- Add swing low + numLows++; //--- Increment lows count + } + double high_i = iHigh(_Symbol, _Period, i); //--- Get current high + double high_im1 = iHigh(_Symbol, _Period, i - 1); //--- Get previous high + double high_im2 = iHigh(_Symbol, _Period, i - 2); //--- Get two bars prior high + double high_ip1 = iHigh(_Symbol, _Period, i + 1); //--- Get next high + double high_ip2 = iHigh(_Symbol, _Period, i + 2); //--- Get two bars next high + if (high_i > high_im1 && high_i > high_im2 && high_i > high_ip1 && high_i > high_ip2) { //--- Check for swing high + Swing s; //--- Create swing struct + s.time = iTime(_Symbol, _Period, i); //--- Set swing time + s.price = high_i; //--- Set swing price + ArrayResize(swingHighs, numHighs + 1); //--- Resize highs array + swingHighs[numHighs] = s; //--- Add swing high + numHighs++; //--- Increment highs count + } + } + if (numLows > 0) SortSwings(swingLows, numLows); //--- Sort swing lows + if (numHighs > 0) SortSwings(swingHighs, numHighs); //--- Sort swing highs +} +//+------------------------------------------------------------------+ +//| Calculate visual inclination angle | +//+------------------------------------------------------------------+ +double CalculateAngle(datetime time1, double price1, datetime time2, double price2) { + int x1, y1, x2, y2; //--- Declare coordinate variables + if (!ChartTimePriceToXY(0, 0, time1, price1, x1, y1)) return 0.0; //--- Convert time1/price1 to XY + if (!ChartTimePriceToXY(0, 0, time2, price2, x2, y2)) return 0.0; //--- Convert time2/price2 to XY + double dx = (double)(x2 - x1); //--- Calculate x difference + double dy = (double)(y2 - y1); //--- Calculate y difference + if (dx == 0.0) return (dy > 0.0 ? -90.0 : 90.0); //--- Handle vertical line case + double angle = MathArctan(-dy / dx) * 180.0 / M_PI; //--- Calculate angle in degrees + return angle; //--- Return angle +} +//+------------------------------------------------------------------+ +//| Validate trendline | +//+------------------------------------------------------------------+ +bool ValidateTrendline(bool isSupport, datetime start_time, datetime ref_time, double ref_price, double slope, double tolerance_pen) { + int bar_start = iBarShift(_Symbol, _Period, start_time); //--- Get start bar index + if (bar_start < 0) return false; //--- Check invalid bar index + for (int bar = bar_start; bar >= 0; bar--) { //--- Iterate through bars + datetime bar_time = iTime(_Symbol, _Period, bar); //--- Get bar time + double dk = (double)(bar_time - ref_time); //--- Calculate time difference + double line_price = ref_price + slope * dk; //--- Calculate line price + if (isSupport) { //--- Check support case + double low = iLow(_Symbol, _Period, bar); //--- Get bar low + if (low < line_price - tolerance_pen) return false;//--- Check if broken + } else { //--- Handle resistance case + double high = iHigh(_Symbol, _Period, bar); //--- Get bar high + if (high > line_price + tolerance_pen) return false;//--- Check if broken + } + } + return true; //--- Return valid +} +//+------------------------------------------------------------------+ +//| Calculate R-squared for goodness of fit | +//+------------------------------------------------------------------+ +double CalculateRSquared(const datetime ×[], const double &prices[], int n, double slope, double intercept) { + double sum_y = 0.0; //--- Initialize sum of y + for (int k = 0; k < n; k++) { //--- Iterate through points + sum_y += prices[k]; //--- Accumulate y + } + double mean_y = sum_y / n; //--- Calculate mean y + double ss_tot = 0.0, ss_res = 0.0; //--- Initialize sums of squares + for (int k = 0; k < n; k++) { //--- Iterate through points + double x = (double)times[k]; //--- Get x (time) + double y_pred = intercept + slope * x; //--- Calculate predicted y + double y = prices[k]; //--- Get actual y + ss_res += (y - y_pred) * (y - y_pred); //--- Accumulate residual sum + ss_tot += (y - mean_y) * (y - mean_y); //--- Accumulate total sum + } + if (ss_tot == 0.0) return 1.0; //--- Handle constant y case + return 1.0 - ss_res / ss_tot; //--- Calculate and return R-squared +} +//+------------------------------------------------------------------+ +//| Check if starting point is already used | +//+------------------------------------------------------------------+ +bool IsStartingPointUsed(datetime time, double price, bool is_support) { + for (int i = 0; i < numStartingPoints; i++) { //--- Iterate through starting points + if (startingPoints[i].time == time && MathAbs(startingPoints[i].price - price) < TouchTolerance * _Point && startingPoints[i].is_support == is_support) { //--- Check match + return true; //--- Return used + } + } + return false; //--- Return not used +} +//+------------------------------------------------------------------+ +//| Remove trendline from storage and optionally chart objects | +//+------------------------------------------------------------------+ +void RemoveTrendlineFromStorage(int index) { + if (index < 0 || index >= numTrendlines) return; //--- Check valid index + Print("Removing trendline from storage: ", trendlines[index].name); //--- Log removal + if (DeleteExpiredObjects) { //--- Check deletion flag + ObjectDelete(0, trendlines[index].name); //--- Delete trendline object + for (int m = 0; m < trendlines[index].touch_count; m++) { //--- Iterate touches + string arrow_name = trendlines[index].name + "_touch" + IntegerToString(m); //--- Generate arrow name + ObjectDelete(0, arrow_name); //--- Delete touch arrow + string text_name = trendlines[index].name + "_point_label" + IntegerToString(m); //--- Generate text name + ObjectDelete(0, text_name); //--- Delete point label + } + string label_name = trendlines[index].name + "_label"; //--- Generate label name + ObjectDelete(0, label_name); //--- Delete trendline label + string signal_arrow = trendlines[index].name + "_signal_arrow"; //--- Generate signal arrow name + ObjectDelete(0, signal_arrow); //--- Delete signal arrow + string signal_text = trendlines[index].name + "_signal_text"; //--- Generate signal text name + ObjectDelete(0, signal_text); //--- Delete signal text + } + for (int i = index; i < numTrendlines - 1; i++) { //--- Shift array + trendlines[i] = trendlines[i + 1]; //--- Copy next trendline + } + ArrayResize(trendlines, numTrendlines - 1); //--- Resize trendlines array + numTrendlines--; //--- Decrement trendlines count +} +//+------------------------------------------------------------------+ +//| Update trendlines and check for signals | +//+------------------------------------------------------------------+ +void UpdateTrendlines() { + datetime current_time = iTime(_Symbol, _Period, 0); //--- Get current time + double pointValue = _Point; //--- Get point value + double pen_tolerance = PenetrationTolerance * pointValue; //--- Calculate penetration tolerance + double touch_tolerance = TouchTolerance * pointValue; //--- Calculate touch tolerance + for (int i = numTrendlines - 1; i >= 0; i--) { //--- Iterate trendlines backward + string type = trendlines[i].is_support ? "Support" : "Resistance"; //--- Determine trendline type + string name = trendlines[i].name; //--- Get trendline name + if (current_time > trendlines[i].end_time) { //--- Check if expired + PrintFormat("%s trendline %s is no longer valid (expired). End time: %s, Current time: %s.", type, name, TimeToString(trendlines[i].end_time), TimeToString(current_time)); //--- Log expiration + RemoveTrendlineFromStorage(i); //--- Remove trendline + continue; //--- Skip to next + } + datetime prev_bar_time = iTime(_Symbol, _Period, 1); //--- Get previous bar time + double dk = (double)(prev_bar_time - trendlines[i].start_time); //--- Calculate time difference + double line_price = trendlines[i].start_price + trendlines[i].slope * dk; //--- Calculate line price + double prev_close = iClose(_Symbol, _Period, 1); //--- Get previous bar close + double prev_low = iLow(_Symbol, _Period, 1); //--- Get previous bar low + double prev_high = iHigh(_Symbol, _Period, 1); //--- Get previous bar high + bool broken = false; //--- Initialize broken flag + if (BreakoutType == BREAKOUT_CLOSE) { //--- Check breakout on close + if (trendlines[i].is_support && prev_close < line_price) { //--- Support break by close + PrintFormat("%s trendline %s is no longer valid (broken by close). Line price: %.5f, Prev close: %.5f.", type, name, line_price, prev_close); //--- Log break + broken = true; //--- Set broken flag + } else if (!trendlines[i].is_support && prev_close > line_price) { //--- Resistance break by close + PrintFormat("%s trendline %s is no longer valid (broken by close). Line price: %.5f, Prev close: %.5f.", type, name, line_price, prev_close); //--- Log break + broken = true; //--- Set broken flag + } + } else if (BreakoutType == BREAKOUT_CANDLE) { //--- Check breakout on entire candle + if (trendlines[i].is_support && prev_high < line_price) { //--- Entire candle below support + PrintFormat("%s trendline %s is no longer valid (entire candle below). Line price: %.5f, Prev high: %.5f.", type, name, line_price, prev_high); //--- Log break + broken = true; //--- Set broken flag + } else if (!trendlines[i].is_support && prev_low > line_price) { //--- Entire candle above resistance + PrintFormat("%s trendline %s is no longer valid (entire candle above). Line price: %.5f, Prev low: %.5f.", type, name, line_price, prev_low); //--- Log break + broken = true; //--- Set broken flag + } + } + if (broken && EnableTradingSignals && !trendlines[i].is_signaled) { //--- Check for breakout signal + bool signaled = false; //--- Initialize signaled flag + string signal_type = ""; //--- Initialize signal type + color signal_color = clrNONE; //--- Initialize signal color + int arrow_code = 0; //--- Initialize arrow code + int anchor = 0; //--- Initialize anchor + double text_angle = 0.0; //--- Initialize text angle + double text_offset = 0.0; //--- Initialize text offset + double text_price = 0.0; //--- Initialize text price + int text_anchor = 0; //--- Initialize text anchor + if (trendlines[i].is_support) { //--- Support break: SELL + signaled = true; //--- Set signaled flag + signal_type = "SELL BREAK"; //--- Set sell break signal + signal_color = clrRed; //--- Set red color + arrow_code = 218; //--- Set down arrow + anchor = ANCHOR_BOTTOM; //--- Set bottom anchor + text_angle = 90.0; //--- Set vertical downward + text_offset = 20 * pointValue; //--- Set text offset + text_price = line_price + text_offset; //--- Calculate text price + text_anchor = ANCHOR_BOTTOM; //--- Set bottom anchor + double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits); //--- Get bid price + double SL = NormalizeDouble(line_price + inpSLPoints * _Point, _Digits); //--- SL above the line + double risk = SL - Bid; //--- Calculate risk + double TP = NormalizeDouble(Bid - risk * inpRRRatio, _Digits); //--- Calculate take profit + obj_Trade.Sell(inpLot, _Symbol, Bid, SL, TP); //--- Execute sell trade + } else { //--- Resistance break: BUY + signaled = true; //--- Set signaled flag + signal_type = "BUY BREAK"; //--- Set buy break signal + signal_color = clrBlue; //--- Set blue color + arrow_code = 217; //--- Set up arrow + anchor = ANCHOR_TOP; //--- Set top anchor + text_angle = -90.0; //--- Set vertical upward + text_offset = -20 * pointValue; //--- Set text offset + text_price = line_price + text_offset; //--- Calculate text price + text_anchor = ANCHOR_LEFT; //--- Set left anchor + double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits); //--- Get ask price + double SL = NormalizeDouble(line_price - inpSLPoints * _Point, _Digits); //--- SL below the line + double risk = Ask - SL; //--- Calculate risk + double TP = NormalizeDouble(Ask + risk * inpRRRatio, _Digits); //--- Calculate take profit + obj_Trade.Buy(inpLot, _Symbol, Ask, SL, TP); //--- Execute buy trade + } + if (signaled) { //--- Check if signaled + PrintFormat("Breakout signal generated for %s trendline %s: %s at price %.5f, time %s.", type, name, signal_type, line_price, TimeToString(current_time)); //--- Log signal + string arrow_name = name + "_signal_arrow"; //--- Generate signal arrow name + if (ObjectFind(0, arrow_name) < 0) { //--- Check if arrow exists + ObjectCreate(0, arrow_name, OBJ_ARROW, 0, prev_bar_time, line_price); //--- Create signal arrow + ObjectSetInteger(0, arrow_name, OBJPROP_ARROWCODE, arrow_code); //--- Set arrow code + ObjectSetInteger(0, arrow_name, OBJPROP_ANCHOR, anchor); //--- Set anchor + ObjectSetInteger(0, arrow_name, OBJPROP_COLOR, signal_color); //--- Set color + ObjectSetInteger(0, arrow_name, OBJPROP_WIDTH, 1); //--- Set width + ObjectSetInteger(0, arrow_name, OBJPROP_BACK, false); //--- Set to foreground + } + string text_name = name + "_signal_text"; //--- Generate signal text name + if (ObjectFind(0, text_name) < 0) { //--- Check if text exists + ObjectCreate(0, text_name, OBJ_TEXT, 0, prev_bar_time, text_price); //--- Create signal text + ObjectSetString(0, text_name, OBJPROP_TEXT, " " + signal_type); //--- Set text content + ObjectSetInteger(0, text_name, OBJPROP_COLOR, signal_color); //--- Set color + ObjectSetInteger(0, text_name, OBJPROP_FONTSIZE, 10); //--- Set font size + ObjectSetInteger(0, text_name, OBJPROP_ANCHOR, text_anchor); //--- Set anchor + ObjectSetDouble(0, text_name, OBJPROP_ANGLE, text_angle); //--- Set angle + ObjectSetInteger(0, text_name, OBJPROP_BACK, false); //--- Set to foreground + } + trendlines[i].is_signaled = true; //--- Set signaled flag + } + } + if (broken) { //--- Remove if broken + RemoveTrendlineFromStorage(i); //--- Remove trendline + } + } +} +//+------------------------------------------------------------------+ +//| Find and draw trendlines if no active one exists | +//+------------------------------------------------------------------+ +void FindAndDrawTrendlines(bool isSupport) { + bool has_active = false; //--- Initialize active flag + for (int i = 0; i < numTrendlines; i++) { //--- Iterate through trendlines + if (trendlines[i].is_support == isSupport) { //--- Check type match + has_active = true; //--- Set active flag + break; //--- Exit loop + } + } + if (has_active) return; //--- Exit if active trendline exists + Swing swings[]; //--- Initialize swings array + int numSwings; //--- Initialize swings count + color lineColor; //--- Initialize line color + string prefix; //--- Initialize prefix + if (isSupport) { //--- Handle support case + numSwings = numLows; //--- Set number of lows + ArrayResize(swings, numSwings); //--- Resize swings array + for (int i = 0; i < numSwings; i++) { //--- Iterate through lows + swings[i].time = swingLows[i].time; //--- Copy low time + swings[i].price = swingLows[i].price; //--- Copy low price + } + lineColor = SupportLineColor; //--- Set support line color + prefix = "Trendline_Support_"; //--- Set support prefix + } else { //--- Handle resistance case + numSwings = numHighs; //--- Set number of highs + ArrayResize(swings, numSwings); //--- Resize swings array + for (int i = 0; i < numSwings; i++) { //--- Iterate through highs + swings[i].time = swingHighs[i].time; //--- Copy high time + swings[i].price = swingHighs[i].price; //--- Copy high price + } + lineColor = ResistanceLineColor; //--- Set resistance line color + prefix = "Trendline_Resistance_"; //--- Set resistance prefix + } + if (numSwings < 2) return; //--- Exit if insufficient swings + double pointValue = _Point; //--- Get point value + double touch_tolerance = TouchTolerance * pointValue; //--- Calculate touch tolerance + double pen_tolerance = PenetrationTolerance * pointValue; //--- Calculate penetration tolerance + int best_j = -1; //--- Initialize best j index + int max_touches = 0; //--- Initialize max touches + double best_rsquared = -1.0; //--- Initialize best R-squared + int best_touch_indices[]; //--- Initialize best touch indices + double best_slope = 0.0; //--- Initialize best slope + double best_intercept = 0.0; //--- Initialize best intercept + datetime best_min_time = 0; //--- Initialize best min time + for (int i = 0; i < numSwings - 1; i++) { //--- Iterate through first points + for (int j = i + 1; j < numSwings; j++) { //--- Iterate through second points + datetime time1 = swings[i].time; //--- Get first time + double price1 = swings[i].price; //--- Get first price + datetime time2 = swings[j].time; //--- Get second time + double price2 = swings[j].price; //--- Get second price + double dt = (double)(time2 - time1); //--- Calculate time difference + if (dt <= 0) continue; //--- Skip invalid time difference + double initial_slope = (price2 - price1) / dt; //--- Calculate initial slope + int touch_indices[]; //--- Initialize touch indices + ArrayResize(touch_indices, 0); //--- Resize touch indices + int touches = 0; //--- Initialize touches count + ArrayResize(touch_indices, touches + 1); //--- Add first index + touch_indices[touches] = i; //--- Set first index + touches++; //--- Increment touches + ArrayResize(touch_indices, touches + 1); //--- Add second index + touch_indices[touches] = j; //--- Set second index + touches++; //--- Increment touches + for (int k = 0; k < numSwings; k++) { //--- Iterate through swings + if (k == i || k == j) continue; //--- Skip used indices + datetime tk = swings[k].time; //--- Get swing time + double dk = (double)(tk - time1); //--- Calculate time difference + double expected = price1 + initial_slope * dk; //--- Calculate expected price + double actual = swings[k].price; //--- Get actual price + if (MathAbs(expected - actual) <= touch_tolerance) { //--- Check touch within tolerance + ArrayResize(touch_indices, touches + 1); //--- Add index + touch_indices[touches] = k; //--- Set index + touches++; //--- Increment touches + } + } + if (touches >= MinTouches) { //--- Check minimum touches + ArraySort(touch_indices); //--- Sort touch indices + bool valid_spacing = true; //--- Initialize spacing flag + for (int m = 0; m < touches - 1; m++) { //--- Iterate through touches + int idx1 = touch_indices[m]; //--- Get first index + int idx2 = touch_indices[m + 1]; //--- Get second index + int bar1 = iBarShift(_Symbol, _Period, swings[idx1].time); //--- Get first bar + int bar2 = iBarShift(_Symbol, _Period, swings[idx2].time); //--- Get second bar + int diff = MathAbs(bar1 - bar2); //--- Calculate bar difference + if (diff < MinBarSpacing) { //--- Check minimum spacing + valid_spacing = false; //--- Mark invalid spacing + break; //--- Exit loop + } + } + if (valid_spacing) { //--- Check valid spacing + datetime touch_times[]; //--- Initialize touch times + double touch_prices[]; //--- Initialize touch prices + ArrayResize(touch_times, touches); //--- Resize times array + ArrayResize(touch_prices, touches); //--- Resize prices array + for (int m = 0; m < touches; m++) { //--- Iterate through touches + int idx = touch_indices[m]; //--- Get index + touch_times[m] = swings[idx].time; //--- Set time + touch_prices[m] = swings[idx].price; //--- Set price + } + double slope = initial_slope; //--- Use initial slope from two points + double intercept = price1 - slope * (double)time1; //--- Calculate intercept + double rsquared = CalculateRSquared(touch_times, touch_prices, touches, slope, intercept); //--- Calculate R-squared + if (rsquared >= MinRSquared) { //--- Check minimum R-squared + int adjusted_touch_indices[]; //--- Initialize adjusted indices + ArrayResize(adjusted_touch_indices, touches); //--- Resize to current touches + ArrayCopy(adjusted_touch_indices, touch_indices); //--- Copy indices + int adjusted_touches = touches; //--- Set adjusted touches + if (adjusted_touches >= MinTouches) { //--- Check minimum adjusted touches + datetime temp_min_time = swings[adjusted_touch_indices[0]].time; //--- Get min time + double temp_ref_price = intercept + slope * (double)temp_min_time; //--- Calculate ref price + if (ValidateTrendline(isSupport, temp_min_time, temp_min_time, temp_ref_price, slope, pen_tolerance)) { //--- Validate trendline + datetime temp_max_time = swings[adjusted_touch_indices[adjusted_touches - 1]].time; //--- Get max time + double temp_max_price = intercept + slope * (double)temp_max_time; //--- Calculate max price + double angle = CalculateAngle(temp_min_time, temp_ref_price, temp_max_time, temp_max_price); //--- Calculate angle + double abs_angle = MathAbs(angle); //--- Get absolute angle + if (abs_angle >= MinAngle && abs_angle <= MaxAngle) { //--- Check angle range + if (adjusted_touches > max_touches || (adjusted_touches == max_touches && rsquared > best_rsquared)) { //--- Check better trendline + max_touches = adjusted_touches; //--- Update max touches + best_rsquared = rsquared; //--- Update best R-squared + best_j = j; //--- Update best j + best_slope = slope; //--- Update best slope + best_intercept = intercept; //--- Update best intercept + best_min_time = temp_min_time; //--- Update best min time + ArrayResize(best_touch_indices, adjusted_touches); //--- Resize best indices + ArrayCopy(best_touch_indices, adjusted_touch_indices); //--- Copy indices + } + } + } + } + } + } + } + } + } + if (max_touches < MinTouches) { //--- Check insufficient touches + string type = isSupport ? "Support" : "Resistance"; //--- Set type string + return; //--- Exit function + } + int touch_indices[]; //--- Initialize touch indices + ArrayResize(touch_indices, max_touches); //--- Resize touch indices + ArrayCopy(touch_indices, best_touch_indices); //--- Copy best indices + int touches = max_touches; //--- Set touches count + datetime min_time = best_min_time; //--- Set min time + double price_min = best_intercept + best_slope * (double)min_time; //--- Calculate min price + datetime max_time = swings[touch_indices[touches - 1]].time; //--- Set max time + double price_max = best_intercept + best_slope * (double)max_time; //--- Calculate max price + datetime start_time_check = min_time; //--- Set start time check + double start_price_check = price_min; //--- Set start price check (approximate if not exact) + if (IsStartingPointUsed(start_time_check, start_price_check, isSupport)) { //--- Check used starting point + return; //--- Skip if used + } + datetime time_end = iTime(_Symbol, _Period, 0) + PeriodSeconds(_Period) * ExtensionBars; //--- Calculate end time + double dk_end = (double)(time_end - min_time); //--- Calculate end time difference + double price_end = price_min + best_slope * dk_end; //--- Calculate end price + string unique_name = prefix + TimeToString(TimeCurrent(), TIME_DATE|TIME_MINUTES|TIME_SECONDS); //--- Generate unique name + if (ObjectFind(0, unique_name) < 0) { //--- Check if trendline exists + ObjectCreate(0, unique_name, OBJ_TREND, 0, min_time, price_min, time_end, price_end); //--- Create trendline + ObjectSetInteger(0, unique_name, OBJPROP_COLOR, lineColor); //--- Set color + ObjectSetInteger(0, unique_name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style + ObjectSetInteger(0, unique_name, OBJPROP_WIDTH, 1); //--- Set width + ObjectSetInteger(0, unique_name, OBJPROP_RAY_RIGHT, false); //--- Disable right ray + ObjectSetInteger(0, unique_name, OBJPROP_RAY_LEFT, false); //--- Disable left ray + ObjectSetInteger(0, unique_name, OBJPROP_BACK, false); //--- Set to foreground + } + ArrayResize(trendlines, numTrendlines + 1); //--- Resize trendlines array + trendlines[numTrendlines].name = unique_name; //--- Set trendline name + trendlines[numTrendlines].start_time = min_time; //--- Set start time + trendlines[numTrendlines].end_time = time_end; //--- Set end time + trendlines[numTrendlines].start_price = price_min; //--- Set start price + trendlines[numTrendlines].end_price = price_end; //--- Set end price + trendlines[numTrendlines].slope = best_slope; //--- Set slope + trendlines[numTrendlines].is_support = isSupport; //--- Set type + trendlines[numTrendlines].touch_count = touches; //--- Set touch count + trendlines[numTrendlines].creation_time = TimeCurrent(); //--- Set creation time + trendlines[numTrendlines].is_signaled = false; //--- Set signaled flag + ArrayResize(trendlines[numTrendlines].touch_indices, touches); //--- Resize touch indices + ArrayCopy(trendlines[numTrendlines].touch_indices, touch_indices); //--- Copy touch indices + numTrendlines++; //--- Increment trendlines count + ArrayResize(startingPoints, numStartingPoints + 1); //--- Resize starting points array + startingPoints[numStartingPoints].time = start_time_check;//--- Set starting point time + startingPoints[numStartingPoints].price = start_price_check; //--- Set starting point price + startingPoints[numStartingPoints].is_support = isSupport; //--- Set starting point type + numStartingPoints++; //--- Increment starting points count + if (DrawTouchArrows) { //--- Check draw arrows + for (int m = 0; m < touches; m++) { //--- Iterate through touches + int idx = touch_indices[m]; //--- Get touch index + datetime tk_time = swings[idx].time; //--- Get touch time + double tk_price = swings[idx].price; //--- Get touch price + string arrow_name = unique_name + "_touch" + IntegerToString(m); //--- Generate arrow name + if (ObjectFind(0, arrow_name) < 0) { //--- Check if arrow exists + ObjectCreate(0, arrow_name, OBJ_ARROW, 0, tk_time, tk_price); //--- Create touch arrow + ObjectSetInteger(0, arrow_name, OBJPROP_ARROWCODE, 159); //--- Set arrow code + ObjectSetInteger(0, arrow_name, OBJPROP_ANCHOR, isSupport ? ANCHOR_TOP : ANCHOR_BOTTOM); //--- Set anchor + ObjectSetInteger(0, arrow_name, OBJPROP_COLOR, lineColor); //--- Set color + ObjectSetInteger(0, arrow_name, OBJPROP_WIDTH, 1); //--- Set width + ObjectSetInteger(0, arrow_name, OBJPROP_BACK, false); //--- Set to foreground + } + } + } + double angle = CalculateAngle(min_time, price_min, max_time, price_max); //--- Calculate angle + string type = isSupport ? "Support" : "Resistance"; //--- Set type string + Print(type + " Trendline " + unique_name + " drawn with " + IntegerToString(touches) + " touches. Inclination angle: " + DoubleToString(angle, 2) + " degrees."); //--- Log trendline + if (DrawLabels) { //--- Check draw labels + datetime mid_time = min_time + (max_time - min_time) / 2; //--- Calculate mid time + double dk_mid = (double)(mid_time - min_time); //--- Calculate mid time difference + double mid_price = price_min + best_slope * dk_mid; //--- Calculate mid price + double label_offset = 20 * _Point * (isSupport ? -1 : 1); //--- Calculate label offset + double label_price = mid_price + label_offset; //--- Calculate label price + int label_anchor = isSupport ? ANCHOR_TOP : ANCHOR_BOTTOM;//--- Set label anchor + string label_text = type + " Trendline"; //--- Set label text + string label_name = unique_name + "_label"; //--- Generate label name + if (ObjectFind(0, label_name) < 0) { //--- Check if label exists + ObjectCreate(0, label_name, OBJ_TEXT, 0, mid_time, label_price); //--- Create label + ObjectSetString(0, label_name, OBJPROP_TEXT, label_text); //--- Set text + ObjectSetInteger(0, label_name, OBJPROP_COLOR, clrBlack); //--- Set color + ObjectSetInteger(0, label_name, OBJPROP_FONTSIZE, 8); //--- Set font size + ObjectSetInteger(0, label_name, OBJPROP_ANCHOR, label_anchor); //--- Set anchor + ObjectSetDouble(0, label_name, OBJPROP_ANGLE, angle); //--- Set angle + ObjectSetInteger(0, label_name, OBJPROP_BACK, false); //--- Set to foreground + } + color point_label_color = isSupport ? clrSaddleBrown : clrDarkGoldenrod; //--- Set point label color + double point_text_offset = 20.0 * _Point; //--- Set point text offset + for (int m = 0; m < touches; m++) { //--- Iterate through touches + int idx = touch_indices[m]; //--- Get touch index + datetime tk_time = swings[idx].time; //--- Get touch time + double tk_price = swings[idx].price; //--- Get touch price + double text_price; //--- Initialize text price + int point_text_anchor; //--- Initialize text anchor + if (isSupport) { //--- Handle support + text_price = tk_price - point_text_offset; //--- Set text price below + point_text_anchor = ANCHOR_LEFT; //--- Set left anchor + } else { //--- Handle resistance + text_price = tk_price + point_text_offset; //--- Set text price above + point_text_anchor = ANCHOR_BOTTOM; //--- Set bottom anchor + } + string text_name = unique_name + "_point_label" + IntegerToString(m); //--- Generate text name + string point_text = "Pt " + IntegerToString(m + 1); //--- Set point text + if (ObjectFind(0, text_name) < 0) { //--- Check if text exists + ObjectCreate(0, text_name, OBJ_TEXT, 0, tk_time, text_price); //--- Create text + ObjectSetString(0, text_name, OBJPROP_TEXT, point_text); //--- Set text + ObjectSetInteger(0, text_name, OBJPROP_COLOR, point_label_color); //--- Set color + ObjectSetInteger(0, text_name, OBJPROP_FONTSIZE, 8); //--- Set font size + ObjectSetInteger(0, text_name, OBJPROP_ANCHOR, point_text_anchor); //--- Set anchor + ObjectSetDouble(0, text_name, OBJPROP_ANGLE, 0); //--- Set angle + ObjectSetInteger(0, text_name, OBJPROP_BACK, false); //--- Set to foreground + } + } + } +} +//+------------------------------------------------------------------+ \ No newline at end of file diff --git a/27-Trendline-Breakout/Trendline-Breakout.pdf b/27-Trendline-Breakout/Trendline-Breakout.pdf new file mode 100644 index 0000000..5f02116 Binary files /dev/null and b/27-Trendline-Breakout/Trendline-Breakout.pdf differ diff --git a/28-Range-Breakout/Range-Breakout.mq4 b/28-Range-Breakout/Range-Breakout.mq4 new file mode 100644 index 0000000..ce67e6d --- /dev/null +++ b/28-Range-Breakout/Range-Breakout.mq4 @@ -0,0 +1,258 @@ +/* +============================================================ +Demo File: Range_Breakout_EA - Signal Logic Showcase +Category: Breakout +Platform: MetaTrader 4 (MQL4) +Version: 1.0 +Author: Giacomo Cipolat Bares +Portfolio: MQL4 Expert Advisors Portfolio +============================================================ + +Description: +This is a simplified public demo derived from the full +Range Breakout EA. + +Included in this demo: +- range high/low detection +- breakout trigger calculation +- breakout buffer logic +- one-breakout-per-range logic +- breakout cooldown logic +- basic on-chart signal output + +Excluded from this demo: +- order execution +- risk management engine +- break-even / trailing stop +- retry logic +- broker protection handling +- full production trade framework +- chart visualization layer +============================================================ +*/ + +#property strict +#property version "1.00" + +//========================= INPUTS ================================== +input string __01_BreakoutSettings = "01 ======== Breakout Settings ========"; +input int BreakoutBars = 20; +input double BreakoutBufferPips = 0.5; +input bool UseCloseBreakout = false; +input bool OneBreakoutPerRange = true; +input bool ResetRangeAfterTrade = true; +input bool UseBreakoutCooldown = true; +input int BreakoutCooldownBars = 5; + +//======================= BREAKOUT GLOBALS ========================== +double g_lastRangeHigh = 0.0; +double g_lastRangeLow = 0.0; +bool g_rangeAlreadyTraded = false; +int g_lastTradeBarIndex = -1; + +//======================= GENERAL GLOBALS =========================== +double g_point; +double g_pip; +int g_digits; +datetime g_lastBarTime = 0; + +//+------------------------------------------------------------------+ +//| Expert initialization | +//+------------------------------------------------------------------+ +int OnInit() +{ + g_point = Point; + g_digits = Digits; + + if(g_digits == 5 || g_digits == 3) + g_pip = g_point * 10.0; + else + g_pip = g_point; + + Print("Range Breakout demo initialized"); + return(INIT_SUCCEEDED); +} + +//+------------------------------------------------------------------+ +//| Detects a new bar | +//+------------------------------------------------------------------+ +bool IsNewBar() +{ + datetime currentBarTime = iTime(NULL, 0, 0); + + if(currentBarTime != g_lastBarTime) + { + g_lastBarTime = currentBarTime; + return true; + } + + return false; +} + +//+------------------------------------------------------------------+ +//| Range high from previous N bars | +//+------------------------------------------------------------------+ +double GetBreakoutHigh() +{ + int highestIndex = iHighest(NULL, 0, MODE_HIGH, BreakoutBars, 1); + return High[highestIndex]; +} + +//+------------------------------------------------------------------+ +//| Range low from previous N bars | +//+------------------------------------------------------------------+ +double GetBreakoutLow() +{ + int lowestIndex = iLowest(NULL, 0, MODE_LOW, BreakoutBars, 1); + return Low[lowestIndex]; +} + +//+------------------------------------------------------------------+ +//| Bullish breakout condition | +//+------------------------------------------------------------------+ +bool IsBullishBreakout() +{ + double breakoutHigh = GetBreakoutHigh(); + double triggerPrice = breakoutHigh + BreakoutBufferPips * g_pip; + + if(UseCloseBreakout) + return (Close[1] > triggerPrice); + + return (Ask > triggerPrice); +} + +//+------------------------------------------------------------------+ +//| Bearish breakout condition | +//+------------------------------------------------------------------+ +bool IsBearishBreakout() +{ + double breakoutLow = GetBreakoutLow(); + double triggerPrice = breakoutLow - BreakoutBufferPips * g_pip; + + if(UseCloseBreakout) + return (Close[1] < triggerPrice); + + return (Bid < triggerPrice); +} + +//+------------------------------------------------------------------+ +//| Checks if current range is same as previous tracked range | +//+------------------------------------------------------------------+ +bool IsSameRange(double rangeHigh, double rangeLow) +{ + if(MathAbs(rangeHigh - g_lastRangeHigh) < (g_point * 0.5) && + MathAbs(rangeLow - g_lastRangeLow) < (g_point * 0.5)) + { + return true; + } + + return false; +} + +//+------------------------------------------------------------------+ +//| Updates tracked breakout range | +//+------------------------------------------------------------------+ +void UpdateRangeState() +{ + double currentRangeHigh = GetBreakoutHigh(); + double currentRangeLow = GetBreakoutLow(); + + if(!IsSameRange(currentRangeHigh, currentRangeLow)) + { + g_lastRangeHigh = currentRangeHigh; + g_lastRangeLow = currentRangeLow; + + if(ResetRangeAfterTrade) + g_rangeAlreadyTraded = false; + } +} + +//+------------------------------------------------------------------+ +//| One breakout per range filter | +//+------------------------------------------------------------------+ +bool CanTradeCurrentRange() +{ + if(!OneBreakoutPerRange) + return true; + + if(g_rangeAlreadyTraded) + return false; + + return true; +} + +//+------------------------------------------------------------------+ +//| Breakout cooldown filter | +//+------------------------------------------------------------------+ +bool BreakoutCooldownPassed() +{ + if(!UseBreakoutCooldown) + return true; + + if(g_lastTradeBarIndex < 0) + return true; + + int barsPassed = Bars - g_lastTradeBarIndex; + + if(barsPassed >= BreakoutCooldownBars) + return true; + + return false; +} + +//+------------------------------------------------------------------+ +//| Demo buy signal wrapper | +//+------------------------------------------------------------------+ +bool BuySignal() +{ + if(!CanTradeCurrentRange()) + return false; + + if(!BreakoutCooldownPassed()) + return false; + + return IsBullishBreakout(); +} + +//+------------------------------------------------------------------+ +//| Demo sell signal wrapper | +//+------------------------------------------------------------------+ +bool SellSignal() +{ + if(!CanTradeCurrentRange()) + return false; + + if(!BreakoutCooldownPassed()) + return false; + + return IsBearishBreakout(); +} + +//+------------------------------------------------------------------+ +//| Expert tick | +//+------------------------------------------------------------------+ +void OnTick() +{ + if(!IsNewBar()) + return; + + UpdateRangeState(); + + if(BuySignal()) + { + g_lastTradeBarIndex = Bars; + g_rangeAlreadyTraded = true; + Comment("Demo Signal: BUY breakout detected"); + return; + } + + if(SellSignal()) + { + g_lastTradeBarIndex = Bars; + g_rangeAlreadyTraded = true; + Comment("Demo Signal: SELL breakout detected"); + return; + } + + Comment("Demo Signal: No valid breakout"); +} \ No newline at end of file diff --git a/28-Range-Breakout/Range-Breakout.pdf b/28-Range-Breakout/Range-Breakout.pdf new file mode 100644 index 0000000..73865a1 Binary files /dev/null and b/28-Range-Breakout/Range-Breakout.pdf differ diff --git a/29-Support-Resistance/Support-Resistance.mq4 b/29-Support-Resistance/Support-Resistance.mq4 new file mode 100644 index 0000000..977f6ed --- /dev/null +++ b/29-Support-Resistance/Support-Resistance.mq4 @@ -0,0 +1,317 @@ +//+------------------------------------------------------------------+ +//| Support and Resistance EA.mq4 | +//| Copyright 2023,JBlanked | +//| https://www.jblanked.com | +//+------------------------------------------------------------------+ +#property copyright "Copyright 2023,JBlanked" +#property link "https://www.jblanked.com" +#property strict + +#include + + +input string orderSeng = "======= ORDER SETTINGS ======"; //---------------------------> +input double StopLoss = 10; // Stop Loss +input double TakeProfit = 600; // Take Profit +input bool usepercentrisk = true; // Use risk per trade? +input double percentrisk = 0.10; // Percent risk +input bool uselotsize = false; // Use lot size? +input double lotsizee = 0.10; // Lot size + +input string orderSeting = "======= TREND SETTINGS ======"; //---------------------------> +input int MA_Period = 160; // Period for moving average +input int RSI_Period = 8; // Period for RSI +input int rsibuylevel = 20; // RSI under which level to buy +input int rsiselllevel = 80; // RSI above which level to sell +input bool reverseorder = false; // Reverse trend? +input bool HODL = false; // HODL til opposite setup? + + +input string BreakEvenSettings = "--------TAKE PARTIAL SETTINGS-------"; //---------------------------> +input bool UseBreakEvenStop = true; //Use take partials? +input double BEclosePercent = 50.0; //Close how much percent? +input double breakstart = 200; // Take partials after how many pips in profit (1) +input double breakstart2 = 300; // Take partials after how many pips in profit (2) +input double breakstart3 = 400; // Take partials after how many pips in profit (3) +input double breakstart4 = 500; // Take partials after how many pips in profit (4) + +input double breakstop = 20; // Move stop loss in profit X pips + +input string BkEvnSettings = "======= MARTINGALE SETTINGS ======="; //---------------------------> +input bool useMartingale = false; // Use martingale? +input double martinPips = 78; // Pips in between martingales +input double martinMULTI = 5; // Martingale multiplier + + +input string timeSettings = "======= TIME SETTINGS ======"; //-------------------- +input bool UseTimer = false; // Custom trading hours (true/false) +input string StartTime1 = "16:30"; //1 Trading start time (hh:mm) +input string StopTime1 = "16:31"; //1 Trading stop time (hh:mm) + +input string DAILY_TARGETS = "======= Gain/Loss ======="; //--------------- +input double dailyTargetP = 10.0; // Daily Profit Target (%) +input double dailyLossP = 0.4; // Daily Max DD (%) + + +input string orderSettins = "======= OTHER SETTINGS ======"; //--------------- +input string orderComments = "Support/Resistance EA"; // Order Comment +input int magicnumb = 918119; // Magic Number + + +//+------------------------------------------------------------------+ +//| Expert initialization function | +//+------------------------------------------------------------------+ +int OnInit() + { +//--- + +JBlankedInitCEO(magicnumb,918119,"Support/Resistance EA"); +JBlankedBranding("Support/Resistance EA",magicnumb,string(expiryDateVIP)); + +//--- + return(INIT_SUCCEEDED); + } +//+------------------------------------------------------------------+ +//| Expert deinitialization function | +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) + { +//--- + JBlankedDeinit(); + + } +//+------------------------------------------------------------------+ +//| Expert tick function | +//+------------------------------------------------------------------+ +void OnTick() + { +//--- + +if(ApplyDailyTarget(dailyTargetP,dailyLossP,magicnumb)) return; + +if(useMartingale) { martingale(_Symbol,magicnumb,OrderStopLoss(),martinMULTI,martinPips); } + +//////////////////// Start Take Partials Tempalte + +if(UseBreakEvenStop)DoBreak2(magicnumb,breakstart,BEclosePercent,breakstop,breakstart2,breakstart3,breakstart4); + + + +//////////////////// End Take Partials Tempalte + + + + +//+------------------------------------------------------------------+ + + + double MA = iMA(NULL,0,MA_Period,0,MODE_SMA,PRICE_CLOSE,0); + double RSI = iRSI(NULL,0,RSI_Period,PRICE_CLOSE,0); + double currentPrice = Close[0]; + + if(allowTime(UseTimer,StartTime1,StopTime1)) + { + if(!CheckIfOpenOrdersByMagicNB(magicnumb,orderComments) && StopLoss != 0 && !HODL) + { + if(!reverseorder) + { + if (currentPrice > MA && RSI < rsibuylevel) + { + //price is above moving average and RSI is below 30, indicating oversold + //enter long position + int orderr= OrderSend(Symbol(),OP_BUY,GetRisk(usepercentrisk,uselotsize,percentrisk,StopLoss,lotsizee),Ask,3,Ask-StopLoss*GetPipValue(),Ask+TakeProfit*GetPipValue(),orderComments,magicnumb,0,Green); + + } + + + else if (currentPrice < MA && RSI > rsiselllevel) + { + //price is below moving average and RSI is above 70, indicating overbought + //enter short position + int orderr= OrderSend(Symbol(),OP_SELL,GetRisk(usepercentrisk,uselotsize,percentrisk,StopLoss,lotsizee),Bid,3,Bid+StopLoss*GetPipValue(),Bid-TakeProfit*GetPipValue(),orderComments,magicnumb,0,Red); + + } + } + + + if(reverseorder) + { + if (currentPrice > MA && RSI < rsibuylevel) + { + //price is above moving average and RSI is below 30, indicating oversold + //enter long position + int orderr= OrderSend(Symbol(),OP_SELL,GetRisk(usepercentrisk,uselotsize,percentrisk,StopLoss,lotsizee),Bid,3,Bid+StopLoss*GetPipValue(),Bid-TakeProfit*GetPipValue(),orderComments,magicnumb,0,Red); + + } + + + else if (currentPrice < MA && RSI > rsiselllevel) + { + //price is below moving average and RSI is above 70, indicating overbought + //enter short position + int orderr= OrderSend(Symbol(),OP_BUY,GetRisk(usepercentrisk,uselotsize,percentrisk,StopLoss,lotsizee),Ask,3,Ask-StopLoss*GetPipValue(),Ask+TakeProfit*GetPipValue(),orderComments,magicnumb,0,Green); + + + } + } + } + + + if(!CheckIfOpenOrdersByMagicNB(magicnumb,orderComments) && StopLoss == 0) + { + if(!reverseorder) + { + if (currentPrice > MA && RSI < rsibuylevel) + { + //price is above moving average and RSI is below 30, indicating oversold + //enter long position + int orderr= OrderSend(Symbol(),OP_BUY,GetRisk(usepercentrisk,uselotsize,percentrisk,StopLoss,lotsizee),Ask,3,0,Ask+TakeProfit*GetPipValue(),orderComments,magicnumb,0,Green); + + } + + + else if (currentPrice < MA && RSI > rsiselllevel) + { + //price is below moving average and RSI is above 70, indicating overbought + //enter short position + int orderr= OrderSend(Symbol(),OP_SELL,GetRisk(usepercentrisk,uselotsize,percentrisk,StopLoss,lotsizee),Bid,3,0,Bid-TakeProfit*GetPipValue(),orderComments,magicnumb,0,Red); + + } + } + + + if(reverseorder) + { + if (currentPrice > MA && RSI < rsibuylevel) + { + //price is above moving average and RSI is below 30, indicating oversold + //enter long position + int orderr= OrderSend(Symbol(),OP_SELL,GetRisk(usepercentrisk,uselotsize,percentrisk,StopLoss,lotsizee),Bid,3,0,Bid-TakeProfit*GetPipValue(),orderComments,magicnumb,0,Red); + + } + + + else if (currentPrice < MA && RSI > rsiselllevel) + { + //price is below moving average and RSI is above 70, indicating overbought + //enter short position + int orderr= OrderSend(Symbol(),OP_BUY,GetRisk(usepercentrisk,uselotsize,percentrisk,StopLoss,lotsizee),Ask,3,0,Ask+TakeProfit*GetPipValue(),orderComments,magicnumb,0,Green); + + + } + } + } + + + + + + + + + + + + + + + + + + + if(!CheckIfOpenOrdersByMagicNB(magicnumb,orderComments) && StopLoss != 0 && HODL) + { + if(!reverseorder) + { + if (currentPrice > MA && RSI < rsibuylevel) + { + //price is above moving average and RSI is below 30, indicating oversold + //enter long position + int orderr= OrderSend(Symbol(),OP_BUY,GetRisk(usepercentrisk,uselotsize,percentrisk,StopLoss,lotsizee),Ask,3,Ask-StopLoss*GetPipValue(),0,orderComments,magicnumb,0,Green); + + } + + + else if (currentPrice < MA && RSI > rsiselllevel) + { + //price is below moving average and RSI is above 70, indicating overbought + //enter short position + int orderr= OrderSend(Symbol(),OP_SELL,GetRisk(usepercentrisk,uselotsize,percentrisk,StopLoss,lotsizee),Bid,3,Bid+StopLoss*GetPipValue(),0,orderComments,magicnumb,0,Red); + + } + } + + + if(reverseorder) + { + if (currentPrice > MA && RSI < rsibuylevel) + { + //price is above moving average and RSI is below 30, indicating oversold + //enter long position + int orderr= OrderSend(Symbol(),OP_SELL,GetRisk(usepercentrisk,uselotsize,percentrisk,StopLoss,lotsizee),Bid,3,Bid+StopLoss*GetPipValue(),0,orderComments,magicnumb,0,Red); + + } + + + else if (currentPrice < MA && RSI > rsiselllevel) + { + //price is below moving average and RSI is above 70, indicating overbought + //enter short position + int orderr= OrderSend(Symbol(),OP_BUY,GetRisk(usepercentrisk,uselotsize,percentrisk,StopLoss,lotsizee),Ask,3,Ask-StopLoss*GetPipValue(),0,orderComments,magicnumb,0,Green); + + + } + } + } + + + if(!CheckIfOpenOrdersByMagicNB(magicnumb,orderComments) && StopLoss == 0) + { + if(!reverseorder) + { + if (currentPrice > MA && RSI < rsibuylevel) + { + //price is above moving average and RSI is below 30, indicating oversold + //enter long position + int orderr= OrderSend(Symbol(),OP_BUY,GetRisk(usepercentrisk,uselotsize,percentrisk,StopLoss,lotsizee),Ask,3,0,0,orderComments,magicnumb,0,Green); + + } + + + else if (currentPrice < MA && RSI > rsiselllevel) + { + //price is below moving average and RSI is above 70, indicating overbought + //enter short position + int orderr= OrderSend(Symbol(),OP_SELL,GetRisk(usepercentrisk,uselotsize,percentrisk,StopLoss,lotsizee),Bid,3,0,0,orderComments,magicnumb,0,Red); + + } + } + + + if(reverseorder) + { + if (currentPrice > MA && RSI < rsibuylevel) + { + //price is above moving average and RSI is below 30, indicating oversold + //enter long position + int orderr= OrderSend(Symbol(),OP_SELL,GetRisk(usepercentrisk,uselotsize,percentrisk,StopLoss,lotsizee),Bid,3,0,0,orderComments,magicnumb,0,Red); + + } + + + else if (currentPrice < MA && RSI > rsiselllevel) + { + //price is below moving average and RSI is above 70, indicating overbought + //enter short position + int orderr= OrderSend(Symbol(),OP_BUY,GetRisk(usepercentrisk,uselotsize,percentrisk,StopLoss,lotsizee),Ask,3,0,0,orderComments,magicnumb,0,Green); + + + } + } + } + + + } + + +} diff --git a/29-Support-Resistance/Support-Resistance.pdf b/29-Support-Resistance/Support-Resistance.pdf new file mode 100644 index 0000000..e101b73 Binary files /dev/null and b/29-Support-Resistance/Support-Resistance.pdf differ diff --git a/30-HedgeEA/HedgeEA.mq4 b/30-HedgeEA/HedgeEA.mq4 new file mode 100644 index 0000000..ec5fe0a --- /dev/null +++ b/30-HedgeEA/HedgeEA.mq4 @@ -0,0 +1,413 @@ +//+------------------------------------------------------------------+ +//| HedgeEA.mq4 | +//| Copyright 2024, MetaQuotes Software Corp. | +//| https://www.mql5.com | +//+------------------------------------------------------------------+ +#property copyright "Copyright 2024" +#property link "https://www.mql5.com" +#property version "1.00" +#property strict + +// Input Parameters +input double LotSize = 0.1; // Lot Size +input double HedgeLotSize = 0.1; // Hedge Lot Size +input int StopLoss = 50; // Stop Loss in pips +input int TakeProfit = 100; // Take Profit in pips +input int MagicNumber = 123456; // Magic Number +input int Slippage = 3; // Slippage in pips +input double MaxLossPercent = 1.0; // Maximum Loss Percent before Hedging +input double TargetProfit = 2.0; // Target Profit Percent to Close All +input int StartHour = 0; // Trading Start Hour +input int EndHour = 24; // Trading End Hour +input double MaxMarginLevel = 1000; // Maximum margin level before closing positions +input bool UseTrailingStop = true; // Use trailing stop +input int TrailingStop = 20; // Trailing stop in pips +input int TrailingStep = 5; // Trailing step in pips +input bool UseDynamicLotSize = false; // Use dynamic lot size +input double RiskPercent = 1.0; // Risk percent for dynamic lot size +input int MaxPositionAge = 24; // Maximum position age in hours +input bool UseNewsFilter = true; // Use news filter +input int NewsMinutesBefore = 30; // Minutes before news to avoid trading +input int NewsMinutesAfter = 30; // Minutes after news to avoid trading +input bool UseSessionFilter = true; // Use session filter +input string AsianSession = "00:00-08:00"; // Asian session +input string LondonSession = "08:00-16:00"; // London session +input string NewYorkSession = "13:00-21:00"; // New York session +input bool UseRiskManagement = true; // Use advanced risk management +input double DailyLossLimit = 2.0; // Daily loss limit in percent +input double WeeklyLossLimit = 5.0; // Weekly loss limit in percent + +// Global Variables +int ticket = 0; +bool hedgedPositions[]; // Array to track hedged positions +int totalPositions = 0; +datetime lastTradeTime = 0; +double dailyProfit = 0; +double weeklyProfit = 0; + +//+------------------------------------------------------------------+ +//| Expert initialization function | +//+------------------------------------------------------------------+ +int OnInit() +{ + ArrayResize(hedgedPositions, 100); // Initialize array for 100 positions + ArrayInitialize(hedgedPositions, false); + lastTradeTime = TimeCurrent(); + return(INIT_SUCCEEDED); +} + +//+------------------------------------------------------------------+ +//| Expert deinitialization function | +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) +{ + ArrayFree(hedgedPositions); +} + +//+------------------------------------------------------------------+ +//| Check if trading is allowed in current time | +//+------------------------------------------------------------------+ +bool IsTradingTime() +{ + int currentHour = TimeHour(TimeCurrent()); + return (currentHour >= StartHour && currentHour < EndHour); +} + +//+------------------------------------------------------------------+ +//| Check if current time is within a trading session | +//+------------------------------------------------------------------+ +bool IsInSession(string session) +{ + string times[]; + StringSplit(session, '-', times); + if(ArraySize(times) != 2) return false; + + string currentTime = TimeToString(TimeCurrent(), TIME_MINUTES); + return (currentTime >= times[0] && currentTime < times[1]); +} + +//+------------------------------------------------------------------+ +//| Check if trading is allowed based on sessions | +//+------------------------------------------------------------------+ +bool IsSessionAllowed() +{ + if(!UseSessionFilter) return true; + + return (IsInSession(AsianSession) || IsInSession(LondonSession) || IsInSession(NewYorkSession)); +} + +//+------------------------------------------------------------------+ +//| Check if there is important news coming | +//+------------------------------------------------------------------+ +bool IsNewsTime() +{ + if(!UseNewsFilter) return false; + + // Here you would implement your news checking logic + // This is a placeholder - you would need to integrate with a news API + return false; +} + +//+------------------------------------------------------------------+ +//| Calculate total profit of all positions | +//+------------------------------------------------------------------+ +double CalculateTotalProfit() +{ + double totalProfit = 0; + for(int i = 0; i < OrdersTotal(); i++) + { + if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) + { + if(OrderMagicNumber() == MagicNumber) + { + totalProfit += OrderProfit(); + } + } + } + return totalProfit; +} + +//+------------------------------------------------------------------+ +//| Calculate dynamic lot size based on risk | +//+------------------------------------------------------------------+ +double CalculateLotSize() +{ + if(!UseDynamicLotSize) return LotSize; + + double riskAmount = AccountBalance() * RiskPercent / 100; + double tickValue = MarketInfo(Symbol(), MODE_TICKVALUE); + double stopLossPoints = StopLoss * Point; + + return NormalizeDouble(riskAmount / (stopLossPoints * tickValue), 2); +} + +//+------------------------------------------------------------------+ +//| Check if position is too old | +//+------------------------------------------------------------------+ +bool IsPositionTooOld(int ticket) +{ + if(OrderSelect(ticket, SELECT_BY_TICKET)) + { + datetime positionAge = TimeCurrent() - OrderOpenTime(); + return (positionAge > MaxPositionAge * 3600); + } + return false; +} + +//+------------------------------------------------------------------+ +//| Check if daily or weekly loss limits are reached | +//+------------------------------------------------------------------+ +bool IsLossLimitReached() +{ + if(!UseRiskManagement) return false; + + datetime currentTime = TimeCurrent(); + if(TimeDay(currentTime) != TimeDay(lastTradeTime)) + { + dailyProfit = 0; + lastTradeTime = currentTime; + } + + if(TimeDayOfWeek(currentTime) == 0) // Sunday + { + weeklyProfit = 0; + } + + double currentProfit = CalculateTotalProfit(); + dailyProfit += currentProfit; + weeklyProfit += currentProfit; + + return (dailyProfit <= -AccountBalance() * DailyLossLimit / 100 || + weeklyProfit <= -AccountBalance() * WeeklyLossLimit / 100); +} + +//+------------------------------------------------------------------+ +//| Close all positions | +//+------------------------------------------------------------------+ +void CloseAllPositions() +{ + for(int i = OrdersTotal() - 1; i >= 0; i--) + { + if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) + { + if(OrderMagicNumber() == MagicNumber) + { + bool result = false; + if(OrderType() == OP_BUY) + result = OrderClose(OrderTicket(), OrderLots(), Bid, Slippage, clrRed); + else if(OrderType() == OP_SELL) + result = OrderClose(OrderTicket(), OrderLots(), Ask, Slippage, clrGreen); + + if(!result) + { + Print("Error closing order #", OrderTicket(), ": ", GetLastError()); + } + } + } + } +} + +//+------------------------------------------------------------------+ +//| Check if trading conditions are met | +//+------------------------------------------------------------------+ +bool CheckTradeConditions() +{ + if(!IsTradeAllowed()) return false; + if(!IsTradingTime()) return false; + if(!IsSessionAllowed()) return false; + if(IsNewsTime()) return false; + if(AccountMargin() > MaxMarginLevel) return false; + if(IsLossLimitReached()) return false; + return true; +} + +//+------------------------------------------------------------------+ +//| Print trade information | +//+------------------------------------------------------------------+ +void PrintTradeInfo() +{ + Print("Total Positions: ", OrdersTotal()); + Print("Total Profit: ", CalculateTotalProfit()); + Print("Account Balance: ", AccountBalance()); + Print("Daily Profit: ", dailyProfit); + Print("Weekly Profit: ", weeklyProfit); + Print("Current Session: ", IsInSession(AsianSession) ? "Asian" : + (IsInSession(LondonSession) ? "London" : + (IsInSession(NewYorkSession) ? "New York" : "No Session"))); +} + +//+------------------------------------------------------------------+ +//| Check if lot size is valid and can be opened | +//+------------------------------------------------------------------+ +bool IsValidLotSize(double lot) +{ + double minLot = MarketInfo(Symbol(), MODE_MINLOT); + double maxLot = MarketInfo(Symbol(), MODE_MAXLOT); + double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP); + + // بررسی محدوده حجم + if(lot < minLot || lot > maxLot) + { + Print("Invalid lot size: ", lot, " (Min: ", minLot, ", Max: ", maxLot, ")"); + return false; + } + + // بررسی گام حجم + if(MathAbs(MathMod(lot, lotStep)) > 0.00001) + { + Print("Lot size must be a multiple of ", lotStep); + return false; + } + + return true; +} + +//+------------------------------------------------------------------+ +//| Check if there is enough margin to open position | +//+------------------------------------------------------------------+ +bool HasEnoughMargin(int type, double lot) +{ + double margin = MarketInfo(Symbol(), MODE_MARGINREQUIRED) * lot; + double freeMargin = AccountFreeMargin(); + + if(freeMargin < margin) + { + Print("Not enough margin. Required: ", margin, ", Free: ", freeMargin); + return false; + } + + return true; +} + +//+------------------------------------------------------------------+ +//| Try to reduce lot size if original size cannot be opened | +//+------------------------------------------------------------------+ +double GetAdjustedLotSize(double originalLot) +{ + double minLot = MarketInfo(Symbol(), MODE_MINLOT); + double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP); + double adjustedLot = originalLot; + + while(adjustedLot > minLot) + { + if(IsValidLotSize(adjustedLot) && HasEnoughMargin(OP_BUY, adjustedLot)) + { + Print("Adjusted lot size from ", originalLot, " to ", adjustedLot); + return adjustedLot; + } + adjustedLot -= lotStep; + } + + return 0; // اگر هیچ حجمی نتواند باز شود +} + +//+------------------------------------------------------------------+ +//| Expert tick function | +//+------------------------------------------------------------------+ +void OnTick() +{ + if(!CheckTradeConditions()) return; + + // Check total profit and close all if target reached + double totalProfit = CalculateTotalProfit(); + if(totalProfit >= AccountBalance() * TargetProfit / 100) + { + CloseAllPositions(); + return; + } + + // Check if we have any open positions + if(OrdersTotal() > 0) + { + for(int i = 0; i < OrdersTotal(); i++) + { + if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) + { + if(OrderMagicNumber() == MagicNumber) + { + // Check if position is too old + if(IsPositionTooOld(OrderTicket())) + { + bool result = false; + if(OrderType() == OP_BUY) + result = OrderClose(OrderTicket(), OrderLots(), Bid, Slippage, clrRed); + else if(OrderType() == OP_SELL) + result = OrderClose(OrderTicket(), OrderLots(), Ask, Slippage, clrGreen); + continue; + } + + // Calculate loss percentage + double lossPercent = MathAbs(OrderProfit()) / AccountBalance() * 100; + + // If position is in loss and not hedged yet and loss exceeds threshold + if(OrderProfit() < 0 && !hedgedPositions[OrderTicket()] && lossPercent >= MaxLossPercent) + { + double hedgeLot = CalculateLotSize(); + + // بررسی و تنظیم حجم + if(!IsValidLotSize(hedgeLot) || !HasEnoughMargin(OrderType() == OP_BUY ? OP_SELL : OP_BUY, hedgeLot)) + { + hedgeLot = GetAdjustedLotSize(hedgeLot); + if(hedgeLot == 0) + { + Print("Cannot open hedge position - no valid lot size available"); + // اینجا می‌توانید تصمیم بگیرید چه کاری انجام شود + // مثلاً بستن پوزیشن اصلی یا ارسال هشدار + if(CloseOnHedgeFailure) // یک پارامتر جدید + { + OrderClose(OrderTicket(), OrderLots(), + OrderType() == OP_BUY ? Bid : Ask, + Slippage, clrRed); + Print("Closed original position due to hedge failure"); + } + continue; + } + } + + // باز کردن پوزیشن هدج با حجم تنظیم شده + if(OrderType() == OP_BUY) + { + ticket = OrderSend(Symbol(), OP_SELL, hedgeLot, Bid, Slippage, + OrderOpenPrice() + StopLoss * Point, + OrderOpenPrice() - TakeProfit * Point, + "Hedge", MagicNumber, 0, clrRed); + } + else if(OrderType() == OP_SELL) + { + ticket = OrderSend(Symbol(), OP_BUY, hedgeLot, Ask, Slippage, + OrderOpenPrice() - StopLoss * Point, + OrderOpenPrice() + TakeProfit * Point, + "Hedge", MagicNumber, 0, clrGreen); + } + + if(ticket > 0) + { + hedgedPositions[OrderTicket()] = true; + Print("Hedge position opened successfully for order #", OrderTicket(), + " with adjusted lot size: ", hedgeLot); + } + else + { + Print("Error opening hedge position: ", GetLastError()); + if(CloseOnHedgeFailure) + { + OrderClose(OrderTicket(), OrderLots(), + OrderType() == OP_BUY ? Bid : Ask, + Slippage, clrRed); + Print("Closed original position due to hedge failure"); + } + } + } + } + } + } + } + + // Print trade information periodically + static datetime lastPrintTime = 0; + if(TimeCurrent() - lastPrintTime >= 3600) // Print every hour + { + PrintTradeInfo(); + lastPrintTime = TimeCurrent(); + } +} diff --git a/30-HedgeEA/HedgeEA.pdf b/30-HedgeEA/HedgeEA.pdf new file mode 100644 index 0000000..9211d0c Binary files /dev/null and b/30-HedgeEA/HedgeEA.pdf differ diff --git a/31-HedgeGuard/HedgeGuard.mq4 b/31-HedgeGuard/HedgeGuard.mq4 new file mode 100644 index 0000000..18fcf54 --- /dev/null +++ b/31-HedgeGuard/HedgeGuard.mq4 @@ -0,0 +1,1203 @@ +//+------------------------------------------------------------------+ +//| HedgeGuard_EA_v5_2.mq4 | +//| Smart Hedge Bot for XAUUSD Grid/Averaging EA | +//| | +//| HYBRID TRIGGERS: | +//| 1. Drawdown+Grid Count: DD >= X% AND >= N open trades | +//| 2. Emergency Momentum: Spike >= P pts -- fires when grid has | +//| >= 3 positions AND drawdown >= 50% of trigger threshold | +//| 3. ATR Expansion: Strong trend confirmed (same guards as T2) | +//| (Note: internally numbered as Trigger 3 — S3 group) | +//| SAFETY: Will NOT hedge during blocked news/session hours | +//| Will NOT place any order when market is closed | +//| ORPHAN: Prioritises closing highest-lot orphan positions first | +//| RECOVERY: Adopts ALL hedge positions from before EA restart | +//| | +//| v5.2 ORPHAN FIXES: | +//| Bug 2 — Orphan block now runs while grid is active when hedges | +//| are detached (hedgeIsOpen=false); previously the | +//| tradeCount==0 gate blocked it entirely. | +//| Bug 3 — Funding close (Phase A) promoted to a dedicated pre- | +//| pass so it always targets bestProfitOrphanTicket, | +//| not a random position found by the reverse main loop. | +//| Bug 4 — Bank-pool assassination no longer requires tradeCount | +//| ==0; detached orphans can be assassinated while the | +//| grid is still active. | +//| Bug 5 — realizedHedgePL anchor uses g_OrphanCycleStartTime | +//| when hedges are detached + grid is active, preventing | +//| oldestGridTime drift from understating the pool. | +//| g_OrphanCycleStartTime is no longer reset while any | +//| hedge positions exist regardless of grid state. | +//| Bug 6 — CheckPartialOverlapClose skips same-direction hedge | +//| positions in detached mode; prevents false pairing of | +//| two losing same-side positions as an "overlap". | +//| | +//| v5.3 CRITICAL FIXES (2026.04.23): | +//| Bug 7 — OVERLAP CLOSE: Now REFUSES to close any position with | +//| a LOSS unless OnlyCloseInProfit=false. Previously the | +//| code closed grid trades at losses if hedge was profit. | +//| FIX: CheckPartialOverlapClose() now validates: | +//| - BOTH hedge & grid losses must be acceptable (loss OK | +//| only if hedge profit >= losing position's abs loss) | +//| - OR both must be in profit (strict, no net calc) | +//| - MinHedgeProfit applies to NET only when both profit | +//| Bug 8 — OnlyCloseInProfit enforcement: Now checked at EVERY | +//| close attempt in CheckPartialOverlapClose before | +//| proceeding (early return if position in loss). | +//| Bug 9 — Individual position close: Scaled profit check via | +//| BaseLotForProfitTarget to avoid closing micro-lots at | +//| tiny losses masked by profit requirements. | +//+------------------------------------------------------------------+ +#property copyright "HedgeGuard EA v5.3" +#property version "5.30" +#property strict + +//+------------------------------------------------------------------+ +//| INPUT PARAMETERS | +//+------------------------------------------------------------------+ + +input string S0 = "=== Core Identity ==="; +input string TradeSymbol = ""; // Leave BLANK to auto-use chart symbol (safe with any broker suffix/case) +input int HedgeMagicNumber = 88888; +input int GridMagicNumber = 0; // 0 = watch all non-hedge trades +input string TradeComment = "HedgeGuard"; +input int Slippage = 30; + +// Runtime resolved symbol — handles case differences & broker suffixes (XAUUSDm, xauusdm, XAUUSD., etc.) +string g_Symbol = ""; // UPPERCASE — used only for comparisons +string g_SymbolRaw = ""; // Original case — used for OrderSend, MarketInfo + +input string S1 = "=== Trigger 1: Drawdown + Grid Count (Both Required) ==="; +input bool UsePrimaryTrigger = true; +input double HedgeTriggerPct = 0.1; // Fire if floating loss >= this % of balance +input int MinGridTrades = 3; // AND grid has >= this many open trades + +input string S2 = "=== Trigger 2: Emergency Momentum (Independent) ==="; +input bool UseEmergencyMomentum = true; +input int MomentumBars = 3; // Bars to measure momentum over +input double MomentumPipsThresh = 150.0; // Fire immediately if price spikes > this many points against grid +input double MomentumReentryStep = 50.0; // Min points distance for next momentum scalp on the same spike +input int MomentumMinGridTrades = 1; // Minimum grid positions required before T2 can fire + +input string S3 = "=== Trigger 3: ATR Expansion (Strong Trend) ==="; +input bool UseATRTrigger = true; +input int ATRPeriod = 14; // ATR period +input double ATRMultiplier = 1.5; // Fire if current ATR > X * average ATR +input int ATRAvgPeriod = 50; // Bars to average ATR over +input int ATRMinGridTrades = 1; // Minimum grid positions required before T3 can fire +input double ATRTriggerPct = 0.1; // T3: min drawdown % required before ATR expansion can fire (independent of T1) + +input string S5 = "=== News / Session Block (Safety Filter) — Disabled by default: apply blocking to the grid EA, not the hedge EA ==="; +input bool UseSessionFilter = false; +input int BlockStartHour = 12; // Block hedge from this hour (server time) +input int BlockEndHour = 14; // Block hedge until this hour (covers NY open/news) +input bool BlockFriday = false; // Block hedging on Friday (illiquid close) + +input string S6 = "=== Hedge Lot & Exit ==="; +input double HedgeLotMultiplier = 2.0; // 2.0 = double hedge of net grid lots (for faster recovery) +input double MaxHedgeLot = 0.1; +input double MinHedgeLot = 0.01; +input double HedgeExitPct = 2.0; // Close hedge when drawdown recovers to this % +input bool OnlyCloseInProfit = true; // *** CRITICAL: Only close hedge if its net PNL is >= 0 *** +input bool ForceCloseOnFlip = false; // True=Close immediately, False=Detach to allow new hedge +input double MinHedgeProfit = 3.0; // Minimum net profit ($) for overlap or global closes +input bool EnableOverlapClose = true; // Use profitable trades to close losing trades on the other side +input bool EnableIndividualClose = false; // Allow closing individual active hedges early (steals overlap potential) +input bool EnableOrphanIndividualClose = true; // Allow individual close for orphan/detached hedges in profit +input double MinIndividualProfit = 2.0; // Minimum profit ($) required to close a hedge individually +input double BaseLotForProfitTarget = 0.01; // Base lot size used for scaling MinIndividualProfit + +input string S7 = "=== Orphan Hedge Recovery ==="; +input bool EnableOrphanRecovery = true; // Enable recovery grid for orphaned hedges +input double OrphanRecoveryStep = 2.0; // Distance in ATRs before opening recovery trade +input double OrphanRecoveryMult = 1.5; // Lot multiplier for recovery trades +input int MaxOrphanRecovery = 2; // Max recovery trades to add +input double OrphanProfitTarget = 3.0; // Target profit ($) for orphan group closure +input bool EnableMomentumFlip = true; // Open opposite hedge on momentum spike to recover orphan +input double MomentumFlipMult = 2.0; // Lot multiplier for counter-hedge (double recommended) + +input string S8 = "=== Alerts & Control ==="; +input bool EnableAlerts = false; +input bool EnablePushNotify = false; // Mobile push notifications +input int ManualBlockSeconds = 60; // Block auto-logic after manual close (buttons) + +input string S9 = "=== Dashboard Position ==="; +input bool DashboardRightAligned = true; // true = dashboard on RIGHT side of chart (recommended) +input int DashboardXOffset = 20; // Extra pixels to shift dashboard contents RIGHT (increase to move further right) + +//+------------------------------------------------------------------+ +//| GLOBALS | +//+------------------------------------------------------------------+ +bool hedgeIsOpen = false; +int hedgeTicket = -1; +int atrHandle = -1; + +// Tracks which trigger opened the current hedge — shown on dashboard while hedge is active +string g_LastTriggerSource = ""; + +// Latch to prevent instant close if entered at low DD +bool g_DDWasHigh = false; + +datetime g_OrphanCycleStartTime = 0; + +// Persistent banked-pool for orphan assassination. +// Accumulates the REALISED P&L of each position closed via bank-pool assassination. +// Unlike realizedHedgePL (recomputed each tick from history with a time-anchor that +// can drift or reset when a grid trade re-opens), this counter is never wiped by +// grid restarts, timestamp drift, or orphan-cycle transitions. +// Reset only when ALL hedges are fully cleared (CloseAllOnSymbol / CloseHedge success). +double g_BankedOrphanPool = 0.0; + +// Direction-flip confirmation +int g_FlipConfirmTicks = 0; +#define FLIP_CONFIRM_REQUIRED 10 + +// Recovery close guard +int g_TradesSeen = 0; +#define RECOVERY_MIN_TRADES_SEEN 30 + +// Dashboard +#define DB_PREFIX "HG_" +#define DB_X 15 +#define DB_Y 60 +#define DB_W 320 +#define DB_ROW_H 24 +#define DB_TITLE_H 30 +#define DB_FONT "Arial" +#define DB_FS 9 + +// Palette (unchanged) +#define C_BG C'10,10,10' +#define C_CELL C'25,25,25' +#define C_BRD C'50,50,50' +#define C_ACCENT C'0,102,204' +#define C_TITLE C'255,255,255' +#define C_LBL C'160,160,160' +#define C_VAL C'255,255,255' +#define C_GRN C'46,204,113' +#define C_RED C'231,76,60' +#define C_ORG C'230,126,34' +#define C_YLW C'241,196,15' + +int g_DBCorner = CORNER_LEFT_LOWER; + +//+------------------------------------------------------------------+ +//| Dashboard positioning helpers | +//+------------------------------------------------------------------+ +int GetPanelLeftX() +{ + return DashboardRightAligned ? (DB_W + 15 + DashboardXOffset) : DB_X; +} + +//+------------------------------------------------------------------+ +//| Rectangle helper | +//+------------------------------------------------------------------+ +void _Rect(string n, int x, int y, int w, int h, color bg, color brd, int brdW=1) +{ + if(ObjectFind(0, n) < 0) ObjectCreate(0, n, OBJ_RECTANGLE_LABEL, 0, 0, 0); + ObjectSetInteger(0, n, OBJPROP_CORNER, g_DBCorner); + ObjectSetInteger(0, n, OBJPROP_XDISTANCE, x); + ObjectSetInteger(0, n, OBJPROP_YDISTANCE, y); + ObjectSetInteger(0, n, OBJPROP_XSIZE, w); + ObjectSetInteger(0, n, OBJPROP_YSIZE, h); + ObjectSetInteger(0, n, OBJPROP_BGCOLOR, bg); + ObjectSetInteger(0, n, OBJPROP_COLOR, brd); + ObjectSetInteger(0, n, OBJPROP_WIDTH, brdW); + ObjectSetInteger(0, n, OBJPROP_BORDER_TYPE, BORDER_FLAT); + ObjectSetInteger(0, n, OBJPROP_BACK, false); + ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false); + ObjectSetInteger(0, n, OBJPROP_HIDDEN, true); + ObjectSetInteger(0, n, OBJPROP_ZORDER, 10); +} + +//+------------------------------------------------------------------+ +//| Label helper | +//+------------------------------------------------------------------+ +void _Lbl(string n, int x, int y, string txt, color clr, + int fs=DB_FS, string fnt=DB_FONT, int anchor=ANCHOR_LEFT) +{ + if(ObjectFind(0, n) < 0) + { + ObjectCreate(0, n, OBJ_LABEL, 0, 0, 0); + ObjectSetInteger(0, n, OBJPROP_BACK, false); + ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false); + ObjectSetInteger(0, n, OBJPROP_HIDDEN, true); + ObjectSetInteger(0, n, OBJPROP_ZORDER, 20); + } + // Always update corner — ensures stale objects from a prior session/corner + // setting are immediately corrected rather than retaining the old corner. + ObjectSetInteger(0, n, OBJPROP_CORNER, g_DBCorner); + ObjectSetInteger(0, n, OBJPROP_ANCHOR, anchor); + ObjectSetInteger(0, n, OBJPROP_XDISTANCE, x); + ObjectSetInteger(0, n, OBJPROP_YDISTANCE, y); + ObjectSetInteger(0, n, OBJPROP_COLOR, clr); + ObjectSetInteger(0, n, OBJPROP_FONTSIZE, fs); + ObjectSetString (0, n, OBJPROP_FONT, fnt); + ObjectSetString (0, n, OBJPROP_TEXT, txt); +} + +//+------------------------------------------------------------------+ +//| Button helper | +//+------------------------------------------------------------------+ +void _Btn(string n, int x, int y, int w, int h, string txt, color clr, color bg, int fs=9) +{ + if(ObjectFind(0, n) < 0) + { + ObjectCreate(0, n, OBJ_BUTTON, 0, 0, 0); + ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false); + ObjectSetInteger(0, n, OBJPROP_ZORDER, 30); + } + ObjectSetInteger(0, n, OBJPROP_CORNER, g_DBCorner); + ObjectSetInteger(0, n, OBJPROP_XDISTANCE, x); + ObjectSetInteger(0, n, OBJPROP_YDISTANCE, y); + ObjectSetInteger(0, n, OBJPROP_XSIZE, w); + ObjectSetInteger(0, n, OBJPROP_YSIZE, h); + ObjectSetInteger(0, n, OBJPROP_COLOR, clr); + ObjectSetInteger(0, n, OBJPROP_BGCOLOR, bg); + ObjectSetInteger(0, n, OBJPROP_FONTSIZE, fs); + ObjectSetString (0, n, OBJPROP_TEXT, txt); + ObjectSetInteger(0, n, OBJPROP_STATE, false); +} + +//+------------------------------------------------------------------+ +//| Row helper — fixed for right alignment | +//+------------------------------------------------------------------+ +void _Row(string id, int y, int h, string lbl, string val, color vClr, color bClr=C_CELL) +{ + int panelLeft = GetPanelLeftX(); + _Rect(DB_PREFIX+id+"_bg", panelLeft, y, DB_W, h, bClr, C_BRD, 1); + + int midY = y - h + 7; + int labelX, valueX; + if (DashboardRightAligned) + { + // CORNER_RIGHT_LOWER: X = distance from right edge. panelLeft=355 is the visual left edge. + // To place text inside the panel, subtract from panelLeft. + labelX = panelLeft - 10; // near visual left of panel + valueX = panelLeft - 170; // ~halfway across panel toward the right + } + else + { + labelX = panelLeft + 10; + valueX = panelLeft + 170; + } + + _Lbl(DB_PREFIX+id+"_l", labelX, midY, lbl, C_LBL, 9, "Arial"); + _Lbl(DB_PREFIX+id+"_v", valueX, midY, val, vClr, 9, "Courier New"); +} + +//+------------------------------------------------------------------+ +//| Clean dashboard | +//+------------------------------------------------------------------+ +void DBClean() +{ + ObjectsDeleteAll(0, DB_PREFIX); + ObjectsDeleteAll(0, "HG_DIA_"); + ObjectsDeleteAll(0, "HG_SIG_"); +} + +//+------------------------------------------------------------------+ +//| Profit probability (heuristic) | +//+------------------------------------------------------------------+ +int GetProfitProbability(double hedgeLots) +{ + if (hedgeLots <= 0) return 0; + + double entryPrice = 0; + int hType = -1; + for (int i = 0; i < OrdersTotal(); i++) + { + if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; + if (OrderMagicNumber() != HedgeMagicNumber) continue; + string _os = OrderSymbol(); StringToUpper(_os); + if (_os != g_Symbol) continue; + entryPrice = OrderOpenPrice(); + hType = OrderType(); + break; + } + + if (entryPrice <= 0) return 0; + + double curPrice = (hType == OP_BUY) ? MarketInfo(g_SymbolRaw, MODE_BID) : MarketInfo(g_SymbolRaw, MODE_ASK); + double pipsAway = MathAbs(curPrice - entryPrice) / MarketInfo(g_SymbolRaw, MODE_POINT) / 10.0; + double atr = iATR(g_SymbolRaw, PERIOD_M5, 14, 0) / MarketInfo(g_SymbolRaw, MODE_POINT) / 10.0; + double rsi = iRSI(g_SymbolRaw, PERIOD_M5, 14, PRICE_CLOSE, 0); + + int prob = 50; + + if (pipsAway < atr) prob += 20; + else if (pipsAway > atr * 3) prob -= 30; + + if (hType == OP_SELL) + { + if (rsi > 70) prob += 20; + if (rsi < 30) prob -= 20; + } + else + { + if (rsi < 30) prob += 20; + if (rsi > 70) prob -= 20; + } + + return (int)MathMax(5, MathMin(95, prob)); +} + +//+------------------------------------------------------------------+ +//| Draw dashboard — fully fixed right alignment | +//+------------------------------------------------------------------+ +void DrawDashboard( + double balance, double gridPL, double hedgePL, double realizedPL, double drawdownPct, + string gridDirLabel, double netLots, int tradeCount, + bool at1, bool at2, bool at3, bool blk, + string hedgeStatus, bool hedgeActive, double coveragePct, + int prob, string triggerSource, + double orphanPool = 0.0, double orphanFloat = 0.0) +{ + int rowsCount = 17; + int panelH = DB_TITLE_H + (rowsCount * DB_ROW_H) + (DB_ROW_H * 2) + 40; + + int panelTop = DB_Y + panelH; + int panelLeft = GetPanelLeftX(); + + _Rect(DB_PREFIX+"PANEL", panelLeft, panelTop, DB_W, panelH, C_BG, C_BRD, 1); + _Rect(DB_PREFIX+"TITLE_BG", panelLeft, panelTop, DB_W, DB_TITLE_H, C_ACCENT, C_ACCENT, 0); + + color dotClr = hedgeActive ? C_YLW : (blk ? C_ORG : C_GRN); + + int titleTxtX = DashboardRightAligned ? (panelLeft - 22) : (panelLeft + 22); + // Dot sits just to the LEFT of the title text, inside the panel left edge. + // Right-corner mode: X is distance from right edge; panelLeft is the visual left edge + // of the panel. To place the dot left-of-text we use a slightly larger X (further from + // right edge = further left visually). Text starts at panelLeft-22, dot sits at panelLeft-8 + // (which is 14px to the LEFT of the text anchor in screen space). + int titleDotX = DashboardRightAligned ? (panelLeft - 8) : (panelLeft + 8); + _Lbl(DB_PREFIX+"TITLE_DOT", titleDotX, panelTop - DB_TITLE_H + 9, "O", dotClr, 11, "Arial Bold"); + _Lbl(DB_PREFIX+"TITLE_TXT", titleTxtX, panelTop - DB_TITLE_H + 9, "HedgeGuard EA v5.3", C_TITLE, 10, "Arial Bold"); + + int curY = panelTop - DB_TITLE_H - 1; + + double openPL = gridPL + hedgePL; + double totalNetPL = openPL + realizedPL; + + _Row("r1", curY, DB_ROW_H, "Balance", StringFormat("$ %.2f", balance), C_VAL); curY -= DB_ROW_H; + _Row("r2", curY, DB_ROW_H, "Grid P/L", StringFormat("$ %.2f", gridPL), (gridPL >= 0 ? C_GRN : C_RED)); curY -= DB_ROW_H; + + if (hedgeActive || hedgePL != 0) + { + _Row("r2a", curY, DB_ROW_H, "Hedge P/L", StringFormat("$ %.2f", hedgePL), (hedgePL >= 0 ? C_GRN : C_RED)); curY -= DB_ROW_H; + } + else + { + _Row("r2a", curY, DB_ROW_H, "Hedge P/L", "$ 0.00", C_VAL); curY -= DB_ROW_H; + } + + _Row("r2b", curY, DB_ROW_H, "Open P/L", StringFormat("$ %.2f", openPL), (openPL >= 0 ? C_GRN : C_RED)); curY -= DB_ROW_H; + _Row("r2c", curY, DB_ROW_H, "Banked P/L", StringFormat("$ %.2f", realizedPL), (realizedPL >= 0 ? C_GRN : C_RED)); curY -= DB_ROW_H; + + // Orphan Pool row — only shown when grid is flat and orphan hedges are open. + // Shows the combined assassination pool: closed profits (g_BankedOrphanPool) + // + floating profits of profitable open orphans. This is the actual ammunition + // the EA uses to decide whether it can absorb a losing orphan's loss. + // Banked P/L (realizedHedgePL from MT4 history) cannot show this because it + // uses a time-anchored history scan that misses the floating component entirely. + if (tradeCount == 0 && hedgeActive) + { + double combinedOrphanPool = orphanPool + orphanFloat; + color poolClr = (combinedOrphanPool >= OrphanProfitTarget) ? C_GRN : C_YLW; + // Line 1: label + combined total + _Row("r2e", curY, DB_ROW_H, "Orphan Pool", + StringFormat("$ %.2f", combinedOrphanPool), + poolClr); + curY -= DB_ROW_H; + // Line 2: B/F breakdown (no label, indented value) + _Row("r2e2", curY, DB_ROW_H, "", + StringFormat("B:%.2f F:%.2f", orphanPool, orphanFloat), + poolClr); + curY -= DB_ROW_H; + } + else + { + // Clear both rows when not in orphan mode so they don't ghost + _Row("r2e", curY, DB_ROW_H, "", "", C_BG); curY -= DB_ROW_H; + _Row("r2e2", curY, DB_ROW_H, "", "", C_BG); curY -= DB_ROW_H; + } + _Row("r2d", curY, DB_ROW_H, "Cycle Net P/L", StringFormat("$ %.2f", totalNetPL), (totalNetPL >= 0 ? C_GRN : C_RED)); curY -= DB_ROW_H; + _Row("r3", curY, DB_ROW_H, "Drawdown %", StringFormat("%.2f %%", drawdownPct), (drawdownPct >= HedgeTriggerPct ? C_RED : C_VAL)); curY -= DB_ROW_H; + _Row("r3b", curY, DB_ROW_H, "Exit Target", StringFormat("%.2f %% DD", HedgeExitPct), C_YLW); curY -= DB_ROW_H; + _Row("r4", curY, DB_ROW_H, "Grid Dir", StringFormat("%s (%d trades)", gridDirLabel, tradeCount), C_VAL); curY -= DB_ROW_H; + _Row("r5", curY, DB_ROW_H, "Exposure", StringFormat("%.2f Lots", netLots), C_VAL); curY -= DB_ROW_H; + _Row("r6", curY, DB_ROW_H, "Hedge Coverage", StringFormat("%.1f %%", coveragePct), (coveragePct >= 100 ? C_GRN : C_RED)); curY -= DB_ROW_H; + + if (hedgeActive) + { + color pClr = (prob > 70 ? C_GRN : (prob > 40 ? C_YLW : C_RED)); + _Row("r7", curY, DB_ROW_H, "Profit Prob.", StringFormat("%d %%", prob), pClr); curY -= DB_ROW_H; + } + + int centerX = DashboardRightAligned ? (panelLeft - DB_W / 2) : (panelLeft + DB_W / 2); + + _Rect(DB_PREFIX+"trig_hdr", panelLeft, curY, DB_W, DB_ROW_H, C'40,40,40', C_BRD, 1); + _Lbl(DB_PREFIX+"trig_lbl", centerX, curY - DB_ROW_H + 8, "SMART TRIGGERS", C_ACCENT, 8, "Arial Bold", ANCHOR_CENTER); + curY -= DB_ROW_H; + + // Trigger statuses + // FIRED = trigger condition fully met right now (hedge should open) + // ACTIVE = this trigger was the one that opened the current hedge + // WAIT 0 && tradeCount < MinGridTrades) + { t1Status = StringFormat("WAIT<%d", MinGridTrades); t1Clr = C_ORG; } + else { t1Status = "OK"; t1Clr = C_GRN; } + + // T2: trade count guard fires first (when MomentumMinGridTrades > 0), then DD guard + string t2Status; color t2Clr; + if (at2) { t2Status = "FIRED"; t2Clr = C_RED; } + else if (UseEmergencyMomentum && MomentumMinGridTrades > 0 && tradeCount < MomentumMinGridTrades) + { t2Status = StringFormat("WAIT<%d", MomentumMinGridTrades); t2Clr = C_ORG; } + else if (UseEmergencyMomentum && MomentumMinGridTrades > 0 && drawdownPct < HedgeTriggerPct) + { t2Status = "WAIT DD"; t2Clr = C_ORG; } + else { t2Status = "OK"; t2Clr = C_GRN; } + + // T3: trade count guard fires first (when ATRMinGridTrades > 0), then DD guard + string t3Status; color t3Clr; + if (at3) { t3Status = "FIRED"; t3Clr = C_RED; } + else if (UseATRTrigger && ATRMinGridTrades > 0 && tradeCount < ATRMinGridTrades) + { t3Status = StringFormat("WAIT<%d", ATRMinGridTrades); t3Clr = C_ORG; } + else if (UseATRTrigger && ATRMinGridTrades > 0 && drawdownPct < ATRTriggerPct) + { t3Status = "WAIT DD"; t3Clr = C_ORG; } + else { t3Status = "OK"; t3Clr = C_GRN; } + + if (hedgeActive && triggerSource != "") + { + if (StringFind(triggerSource, "T1:") >= 0) { t1Status = "ACTIVE"; t1Clr = C_ORG; } + else if (StringFind(triggerSource, "T2 EMERGENCY") >= 0) { t2Status = "ACTIVE"; t2Clr = C_ORG; } + else if (StringFind(triggerSource, "ATR expansion") >= 0) { t3Status = "ACTIVE"; t3Clr = C_ORG; } + } + + _Row("t1", curY, DB_ROW_H, "T1: DD+Count", t1Status, t1Clr); curY -= DB_ROW_H; + _Row("t2", curY, DB_ROW_H, "T2: Emergency Mom", t2Status, t2Clr); curY -= DB_ROW_H; + _Row("t3", curY, DB_ROW_H, "T3: ATR Expansion", t3Status, t3Clr); curY -= DB_ROW_H; + _Row("t5", curY, DB_ROW_H, "Session Block", blk ? "YES" : "NO", blk ? C_ORG : C_GRN); + curY -= (DB_ROW_H + 5); + + string statusTxt = hedgeActive ? (coveragePct < 90 ? "UNDER-HEDGED" : "HEDGING") : (blk ? "SYSTEM BLOCKED" : "MONITORING..."); + color statusColor = hedgeActive ? (coveragePct < 90 ? C_RED : C_YLW) : (blk ? C_ORG : C_LBL); + + // In CORNER_RIGHT_LOWER, X is distance from the RIGHT edge of the chart. + // panelLeft is already the correct anchor. Use it directly for all footer elements. + int footX = panelLeft; + _Rect(DB_PREFIX+"FOOT_BG", footX, curY, DB_W-10, DB_ROW_H+4, C_CELL, C_ACCENT, 1); + _Lbl(DB_PREFIX+"FOOT_TXT", centerX, curY - (DB_ROW_H+4) + 9, statusTxt, statusColor, 9, "Arial Bold", ANCHOR_CENTER); + curY -= (DB_ROW_H + 8); + + int btnW = (DB_W - 15) / 2; + int btn1X = panelLeft; + int btn2X = DashboardRightAligned ? (btn1X - btnW - 5) : (panelLeft + btnW + 10); + + _Btn(DB_PREFIX+"CLOSE_HEDGE", btn1X, curY, btnW, DB_ROW_H+6, "CLOSE HEDGE", C_TITLE, C_ORG, 8); + _Btn(DB_PREFIX+"CLOSE_ALL", btn2X, curY, btnW, DB_ROW_H+6, "CLOSE ALL", C_TITLE, C_RED, 8); + + ChartRedraw(); +} + +//+------------------------------------------------------------------+ +//| ChartEvent | +//+------------------------------------------------------------------+ +datetime g_NextAllowedOrderTime = 0; +datetime g_ManualCloseBlockTime = 0; + +void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) +{ + if (id == CHARTEVENT_OBJECT_CLICK) + { + if (sparam == DB_PREFIX+"CLOSE_HEDGE") + { + Print("[HedgeGuard] Manual 'Close Hedge' requested."); + g_ManualCloseBlockTime = TimeCurrent() + ManualBlockSeconds; + CloseHedge("Manual button click"); + ObjectSetInteger(0, DB_PREFIX+"CLOSE_HEDGE", OBJPROP_STATE, false); + } + else if (sparam == DB_PREFIX+"CLOSE_ALL") + { + Print("[HedgeGuard] Manual 'Close All' requested."); + g_ManualCloseBlockTime = TimeCurrent() + ManualBlockSeconds; + CloseAllOnSymbol("Manual button click"); + ObjectSetInteger(0, DB_PREFIX+"CLOSE_ALL", OBJPROP_STATE, false); + } + ChartRedraw(); + } +} + +//+------------------------------------------------------------------+ +//| Market open guard | +//| Returns true only when the broker is currently accepting orders | +//| for this symbol. Checks MODE_TRADEALLOWED (server-side flag) | +//| and the chart symbol's trade-allowed status. | +//+------------------------------------------------------------------+ +bool IsMarketOpen() +{ + // Use chart symbol as fallback if g_SymbolRaw has not been resolved yet + string checkSym = (g_SymbolRaw != "" && g_SymbolRaw != NULL) ? g_SymbolRaw : Symbol(); + + // Broker/server has suspended trading on this symbol + if ((int)MarketInfo(checkSym, MODE_TRADEALLOWED) == 0) return false; + + // MT4 also exposes the global terminal trade-allowed flag + if (!IsTradeAllowed()) return false; + if (!IsConnected()) return false; + + // Additional weekend guard: Saturday = 6, Sunday = 0 + int dow = TimeDayOfWeek(TimeCurrent()); + if (dow == 0 || dow == 6) return false; + + return true; +} + +//+------------------------------------------------------------------+ +//| Safe OrderSend wrapper | +//+------------------------------------------------------------------+ +int SafeOrderSend(string symbol, int cmd, double volume, double price, int slippage, double stoploss, double takeprofit, string comment, int magic, datetime expiration, color arrow_color) +{ + static bool s_marketClosedLogged = false; + + // Never attempt an order when the market is closed + if (!IsMarketOpen()) + { + if (!s_marketClosedLogged) + { + Print("[HedgeGuard] ⚠️ Market is closed — order skipped (", comment, ")"); + s_marketClosedLogged = true; + } + return -1; + } + else + { + if (s_marketClosedLogged) + { + Print("[HedgeGuard] ✅ Market is now open — resuming operations."); + s_marketClosedLogged = false; + } + } + + if (TimeCurrent() < g_NextAllowedOrderTime) return -1; + + int ticket = OrderSend(symbol, cmd, volume, price, slippage, stoploss, takeprofit, comment, magic, expiration, arrow_color); + if (ticket < 0) + { + int err = GetLastError(); + if (err == 134) + { + g_NextAllowedOrderTime = TimeCurrent() + 60; + Print(StringFormat("[HedgeGuard] ❌ ERROR 134 (Not enough money) for %.2f lots. Retrying in 60s...", volume)); + } + else + { + g_NextAllowedOrderTime = TimeCurrent() + 5; + Print(StringFormat("[HedgeGuard] ❌ ERROR %d opening order. Retrying in 5s...", err)); + } + } + else + { + // Apply a 3-second cooldown on successful order placement to prevent + // rapid double-firing before the terminal updates its internal order pool + g_NextAllowedOrderTime = TimeCurrent() + 3; + } + return ticket; +} + +//+------------------------------------------------------------------+ +//| Normalize lot | +//+------------------------------------------------------------------+ +double NormalizeLot(double lot) +{ + double lstep = MarketInfo(g_SymbolRaw, MODE_LOTSTEP); + double lmin = MarketInfo(g_SymbolRaw, MODE_MINLOT); + double lmax = MarketInfo(g_SymbolRaw, MODE_MAXLOT); + lot = MathFloor(lot / lstep) * lstep; + lot = MathMax(lot, lmin); + lot = MathMin(lot, lmax); + lot = MathMin(lot, MaxHedgeLot); + lot = MathMax(lot, MinHedgeLot); + return NormalizeDouble(lot, 2); +} + +//+------------------------------------------------------------------+ +//| Scan grid positions | +//+------------------------------------------------------------------+ +void ScanGridPositions(double &netLots, double &floatingPL, + int &direction, int &tradeCount, datetime &oldestTime) +{ + double buyLots = 0, sellLots = 0; + floatingPL = 0; + tradeCount = 0; + oldestTime = 0; + + string filterSym = ""; + if (TradeSymbol != "" && TradeSymbol != NULL) + { + filterSym = TradeSymbol; + StringToUpper(filterSym); + } + + if (filterSym == "") + { + string topSym = ""; + int maxC = 0; + + string sNames[50]; + int sCounts[50]; + ArrayInitialize(sCounts, 0); + for(int i=0; i<50; i++) sNames[i]=""; + int sTotal = 0; + + for (int i = 0; i < OrdersTotal(); i++) + { + if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; + if (OrderMagicNumber() == HedgeMagicNumber) continue; + if (GridMagicNumber != 0 && OrderMagicNumber() != GridMagicNumber) continue; + + string s = OrderSymbol(); StringToUpper(s); + bool found = false; + for (int j = 0; j < sTotal; j++) + if (sNames[j] == s) { sCounts[j]++; found = true; break; } + + if (!found && sTotal < 50) + { + sNames[sTotal] = s; sCounts[sTotal] = 1; sTotal++; + } + } + + for (int k = 0; k < sTotal; k++) + if (sCounts[k] > maxC) { maxC = sCounts[k]; topSym = sNames[k]; } + + if (topSym != "") filterSym = topSym; + else filterSym = g_Symbol; + } + + if (filterSym != "" && filterSym != g_Symbol) + { + g_Symbol = filterSym; + g_SymbolRaw = Symbol(); + for (int i = 0; i < OrdersTotal(); i++) + { + if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; + string _raw = OrderSymbol(); string _up = _raw; StringToUpper(_up); + if (_up == g_Symbol) { g_SymbolRaw = _raw; break; } + } + Print("[HedgeGuard] Monitoring symbol: ", g_SymbolRaw); + } + + for (int i = 0; i < OrdersTotal(); i++) + { + if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; + if (OrderMagicNumber() == HedgeMagicNumber) continue; + if (GridMagicNumber != 0 && OrderMagicNumber() != GridMagicNumber) continue; + + string _oSym = OrderSymbol(); StringToUpper(_oSym); + if (_oSym != g_Symbol) continue; + + floatingPL += OrderProfit() + OrderSwap() + OrderCommission(); + tradeCount++; + + if (OrderType() == OP_BUY) buyLots += OrderLots(); + if (OrderType() == OP_SELL) sellLots += OrderLots(); + + if (oldestTime == 0 || OrderOpenTime() < oldestTime) + oldestTime = OrderOpenTime(); + } + + if (buyLots >= sellLots) { netLots = buyLots - sellLots; direction = OP_BUY; } + else { netLots = sellLots - buyLots; direction = OP_SELL; } +} + +//+------------------------------------------------------------------+ +//| Trigger 1: DD + Count | +//+------------------------------------------------------------------+ +// gridDrawdownPct = MathAbs(floatingPL [grid-only]) / balance * 100.0 +// This is intentionally grid-only (no hedge P/L) so T1 fires on raw +// grid stress, independent of whether a hedge is offsetting losses. +// Bug fix: previously recomputed pct internally from floatingPL, which +// could include swap/commission on brand-new positions and fire at near- +// zero real DD. Now the caller computes and passes gridDrawdownPct so +// the same value drives both T1 and the dashboard display. +bool CheckPrimaryTrigger(double floatingPL, double balance, int tradeCount, int gridDirection, string &reason, double gridDrawdownPct = -1.0) +{ + if (!UsePrimaryTrigger || balance <= 0) return false; + + // Use pre-computed gridDrawdownPct when provided; fall back to + // internal calculation (backwards-compatible default = -1.0). + double pct; + if (gridDrawdownPct >= 0.0) + pct = gridDrawdownPct; + else + { + // Fallback: only treat as loss when floatingPL is genuinely negative + // (not just commission-bleed). Require at least MinHedgeLot worth of + // real loss before computing pct. + if (floatingPL >= 0) return false; + pct = MathAbs(floatingPL) / balance * 100.0; + } + + if (pct < HedgeTriggerPct) return false; + if (tradeCount < MinGridTrades) return false; + + reason = StringFormat("T1: DD=%.2f%% >= %.1f%% | Grid=%d >= %d trades", + pct, HedgeTriggerPct, tradeCount, MinGridTrades); + return true; +} + +//+------------------------------------------------------------------+ +//| Trigger 2: Emergency Momentum | +//+------------------------------------------------------------------+ +bool CheckEmergencyMomentum(int gridDirection, string &reason, int tradeCount = 0, double drawdownPct = 0.0) +{ + if (!UseEmergencyMomentum) return false; + if (Bars < MomentumBars + 2) return false; + + if (MomentumMinGridTrades > 0) + { + // Require minimum active grid positions + if (tradeCount < MomentumMinGridTrades) return false; + + // Require a meaningful drawdown (using global HedgeTriggerPct) so we don't + // fire the momentum trigger on a healthy grid with no real stress + if (drawdownPct < HedgeTriggerPct) return false; + } + + double priceNow = Close[1]; + double priceBack = Close[MomentumBars + 1]; + double move = MathAbs(priceNow - priceBack) / Point; + + if (move >= MomentumPipsThresh) + { + bool movingDown = (priceNow < priceBack); + bool gridIsLong = (gridDirection == OP_BUY); + + if ((gridIsLong && movingDown) || (!gridIsLong && !movingDown)) + { + reason = StringFormat("T2 EMERGENCY: Spike=%.1f pts >= %.0f pts against grid in %d bars | Grid=%d >= %d positions | DD=%.2f%%", + move, MomentumPipsThresh, MomentumBars, tradeCount, MomentumMinGridTrades, drawdownPct); + return true; + } + } + return false; +} + +//+------------------------------------------------------------------+ +//| Orphan momentum trigger | +//+------------------------------------------------------------------+ +double g_LastMomScalpPrice = 0; +datetime g_LastMomScalpBar = 0; + +bool CheckMomentumTrigger(int hedgeDirection, bool &isSpikingAgainst, string &reason) +{ + if (Bars < MomentumBars + 2) return false; + + double priceNow = Close[1]; + double priceBack = Close[MomentumBars + 1]; + double move = MathAbs(priceNow - priceBack) / Point; + + if (move < MomentumPipsThresh) return false; + + if (Time[1] != g_LastMomScalpBar) + { + g_LastMomScalpPrice = 0; + g_LastMomScalpBar = Time[1]; + } + + if (g_LastMomScalpPrice != 0 && + MathAbs(priceNow - g_LastMomScalpPrice) / Point < MomentumReentryStep) + return false; + + bool movingDown = (priceNow < priceBack); + bool hedgeIsLong = (hedgeDirection == OP_BUY); + + isSpikingAgainst = ((hedgeIsLong && movingDown) || (!hedgeIsLong && !movingDown)); + + string dirStr = isSpikingAgainst ? "against" : "in favor of"; + reason = StringFormat("Orphan Mom=%.1f pts %s hedge in %d bars", move, dirStr, MomentumBars); + return true; +} + +//+------------------------------------------------------------------+ +//| Trigger 3: ATR Expansion | +//+------------------------------------------------------------------+ +bool CheckATRTrigger(string &reason, int gridDirection = -1, int tradeCount = 0, double drawdownPct = 0.0) +{ + if (!UseATRTrigger) return false; + if (Bars < ATRAvgPeriod + ATRPeriod + 5) return false; + + if (ATRMinGridTrades > 0) + { + // Require minimum active grid positions + if (tradeCount < ATRMinGridTrades) return false; + + // Bug fix: was using HedgeTriggerPct (T1's threshold), causing T3 to always + // co-fire with T1. Now uses its own ATRTriggerPct so T3 can be tuned + // independently (e.g. set lower to act as an earlier warning, or higher + // to ensure T3 only fires under severe stress). + if (drawdownPct < ATRTriggerPct) return false; + } + + // Optimized: cache ATR values once + double atrBuffer[]; + ArrayResize(atrBuffer, ATRAvgPeriod + 2); + for (int i = 1; i <= ATRAvgPeriod + 1; i++) + atrBuffer[i] = iATR(g_SymbolRaw, 0, ATRPeriod, i); + + double currentATR = atrBuffer[1]; + + double sumATR = 0; + for (int i = 2; i <= ATRAvgPeriod + 1; i++) + sumATR += atrBuffer[i]; + double avgATR = sumATR / ATRAvgPeriod; + + if (avgATR <= 0) return false; + + double ratio = currentATR / avgATR; + if (ratio >= ATRMultiplier) + { + if (gridDirection == OP_BUY || gridDirection == OP_SELL) + { + // Bug fix: was using MomentumBars (a T2-specific 3-bar lookback) for the + // directional check, causing T3 to fire on brief 3-bar counter-moves even + // when the ATR expansion was driven by a longer-term trend. Now uses + // ATRAvgPeriod as the lookback so the direction check is consistent with + // the window over which ATR expansion is measured. + int dirLookback = MathMin(ATRAvgPeriod, Bars - 2); + bool movingDown = (Close[1] < Close[dirLookback + 1]); + bool gridIsLong = (gridDirection == OP_BUY); + if ((gridIsLong && !movingDown) || (!gridIsLong && movingDown)) + return false; + } + reason = StringFormat("ATR expansion: %.5f = %.2fx avg (threshold %.1fx) against grid | Grid=%d >= %d positions | DD=%.2f%%", + currentATR, ratio, ATRMultiplier, tradeCount, ATRMinGridTrades, drawdownPct); + return true; + } + return false; +} + +//+------------------------------------------------------------------+ +//| Session filter | +//+------------------------------------------------------------------+ +bool IsSessionBlocked(string &reason) +{ + if (!UseSessionFilter) return false; + + datetime now = TimeCurrent(); + int hour = TimeHour(now); + int dayOfWeek = TimeDayOfWeek(now); + + if (BlockFriday && dayOfWeek == 5) + { + reason = "Friday session block"; + return true; + } + + if (BlockStartHour < BlockEndHour) + { + if (hour >= BlockStartHour && hour < BlockEndHour) + { + reason = StringFormat("Session block: %02d:00 - %02d:00", BlockStartHour, BlockEndHour); + return true; + } + } + else + { + if (hour >= BlockStartHour || hour < BlockEndHour) + { + reason = StringFormat("Session block: %02d:00 - %02d:00", BlockStartHour, BlockEndHour); + return true; + } + } + return false; +} + +//+------------------------------------------------------------------+ +//| Hedge profit helpers | +//+------------------------------------------------------------------+ +double GetHedgeProfit() +{ + double totalProfit = 0; + for (int i = 0; i < OrdersTotal(); i++) + { + if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; + if (OrderMagicNumber() != HedgeMagicNumber) continue; + string _os = OrderSymbol(); StringToUpper(_os); + if (_os != g_Symbol) continue; + totalProfit += OrderProfit() + OrderSwap() + OrderCommission(); + } + return totalProfit; +} + +double GetTotalHedgeLots() +{ + double totalLots = 0; + for (int i = 0; i < OrdersTotal(); i++) + { + if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; + if (OrderMagicNumber() != HedgeMagicNumber) continue; + string _os = OrderSymbol(); StringToUpper(_os); + if (_os != g_Symbol) continue; + totalLots += OrderLots(); + } + return totalLots; +} + +int GetTotalHedgeCount() +{ + int totalCount = 0; + for (int i = 0; i < OrdersTotal(); i++) + { + if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; + if (OrderMagicNumber() != HedgeMagicNumber) continue; + string _os = OrderSymbol(); StringToUpper(_os); + if (_os != g_Symbol) continue; + totalCount++; + } + return totalCount; +} + +double GetRealizedHedgeProfit(datetime sinceTime) +{ + if (sinceTime == 0) return 0; + double realized = 0; + int total = OrdersHistoryTotal(); + for (int i = 0; i < total; i++) + { + if (!OrderSelect(i, SELECT_BY_POS, MODE_HISTORY)) continue; + if (OrderMagicNumber() != HedgeMagicNumber) continue; + string _os = OrderSymbol(); StringToUpper(_os); + if (_os != g_Symbol) continue; + if (OrderCloseTime() >= sinceTime) + realized += OrderProfit() + OrderSwap() + OrderCommission(); + } + return realized; +} + +double GetRealizedProfitSince(datetime sinceTime) +{ + if (sinceTime == 0) return 0; + double realized = 0; + int total = OrdersHistoryTotal(); + for (int i = 0; i < total; i++) + { + if (!OrderSelect(i, SELECT_BY_POS, MODE_HISTORY)) continue; + if (OrderMagicNumber() != HedgeMagicNumber) continue; + string _os = OrderSymbol(); StringToUpper(_os); + if (_os != g_Symbol) continue; + if (OrderCloseTime() >= sinceTime) + realized += OrderProfit() + OrderSwap() + OrderCommission(); + } + return realized; +} + +// Returns realized profit ONLY from orphan scalp children (Recovery/Flip/Boost trades) +// closed since sinceTime. Does NOT include profits from other peer orphan positions +// that were closed separately — those are already gone from the account and must not +// inflate the assassination pool. +double GetRealizedOrphanScalpProfit(datetime sinceTime) +{ + if (sinceTime == 0) return 0; + double realized = 0; + int total = OrdersHistoryTotal(); + for (int i = 0; i < total; i++) + { + if (!OrderSelect(i, SELECT_BY_POS, MODE_HISTORY)) continue; + if (OrderMagicNumber() != HedgeMagicNumber) continue; + string _os = OrderSymbol(); StringToUpper(_os); + if (_os != g_Symbol) continue; + if (OrderCloseTime() < sinceTime) continue; + string cmt = OrderComment(); + // CRITICAL v5.3 FIX: Only count scalp trades (Recovery/Flip/Boost), not other hedge types + if (StringFind(cmt, "Recovery") < 0 && StringFind(cmt, "Flip") < 0 && StringFind(cmt, "Boost") < 0) + continue; + realized += OrderProfit() + OrderSwap() + OrderCommission(); + } + return realized; +} + +//+------------------------------------------------------------------+ +//| CHECK PARTIAL OVERLAP CLOSE - CRITICAL FIX FOR BUG #7 & #8 | +//+------------------------------------------------------------------+ +// FIXED LOGIC (v5.3): +// - Does NOT close any position at a loss when OnlyCloseInProfit=true +// - Does NOT use net calculation to override individual position protection +// - Requires BOTH hedge and grid to have acceptable outcomes +// - MinHedgeProfit applies only to the combined NET profit when both in profit +bool CheckPartialOverlapClose(int &hedgeTicket, int &gridTicket, double &netClosePL, string &reason) +{ + hedgeTicket = -1; + gridTicket = -1; + netClosePL = 0; + reason = ""; + + if (!EnableOverlapClose || !hedgeIsOpen) return false; + + double hedgePL = 0, bestGridPL = 0; + int bestGridTicket = -1; + double bestGridLots = 0; + int bestGridType = -1; + + // Find best hedge position + int hTicket = -1; + for (int i = 0; i < OrdersTotal(); i++) + { + if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; + if (OrderMagicNumber() != HedgeMagicNumber) continue; + string _os = OrderSymbol(); StringToUpper(_os); + if (_os != g_Symbol) continue; + + hTicket = OrderTicket(); + hedgePL = OrderProfit() + OrderSwap() + OrderCommission(); + break; + } + + if (hTicket <= 0) return false; + + // CRITICAL FIX v5.3: If hedge itself is losing, refuse to close anything + if (OnlyCloseInProfit && hedgePL < 0) + { + reason = StringFormat("[Overlap BLOCKED] Hedge #%d in LOSS: $%.2f (OnlyCloseInProfit=true)", hTicket, hedgePL); + return false; + } + + // Scan grid for matching opposite-side position + for (int i = 0; i < OrdersTotal(); i++) + { + if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; + if (OrderMagicNumber() == HedgeMagicNumber) continue; + if (GridMagicNumber != 0 && OrderMagicNumber() != GridMagicNumber) continue; + + string _oSym = OrderSymbol(); StringToUpper(_oSym); + if (_oSym != g_Symbol) continue; + + int gType = OrderType(); + int hType = -1; + for (int j = 0; j < OrdersTotal(); j++) + { + if (!OrderSelect(j, SELECT_BY_POS, MODE_TRADES)) continue; + if (OrderTicket() != hTicket) continue; + hType = OrderType(); + break; + } + + // CRITICAL FIX v5.3: Skip same-direction grid positions (no true overlap) + if (hType == gType) + { + continue; + } + + double gPL = OrderProfit() + OrderSwap() + OrderCommission(); + + // CRITICAL FIX v5.3: Check OnlyCloseInProfit BEFORE considering this grid trade + if (OnlyCloseInProfit && gPL < 0) + { + // Grid position is in loss. Refuse to close it unless hedge profit covers it completely. + // Requires: hedge_profit >= abs(grid_loss) AND (hedge_profit - abs(grid_loss)) >= MinHedgeProfit + double absGridLoss = MathAbs(gPL); + if (hedgePL >= absGridLoss) + { + double netProfit = hedgePL + gPL; // net is positive when hedge covers grid + if (netProfit >= MinHedgeProfit) + { + // This grid trade CAN be closed as part of overlap + if (bestGridTicket < 0 || OrderLots() > bestGridLots) + { + bestGridTicket = OrderTicket(); + bestGridPL = gPL; + bestGridLots = OrderLots(); + bestGridType = gType; + } + } + } + // Otherwise skip this grid trade entirely (hedge can't cover it or profit too small) + continue; + } + + // Grid position is in profit (or OnlyCloseInProfit is false) + // Pick highest-lot grid position that's profitable + if (bestGridTicket < 0 || OrderLots() > bestGridLots) + { + bestGridTicket = OrderTicket(); + bestGridPL = gPL; + bestGridLots = OrderLots(); + bestGridType = gType; + } + } + + if (bestGridTicket <= 0) return false; + + double combinedPL = hedgePL + bestGridPL; + + if (combinedPL < MinHedgeProfit) + { + reason = StringFormat("[Overlap SKIPPED] Combined P/L $%.2f < MinHedgeProfit $%.2f", combinedPL, MinHedgeProfit); + return false; + } + + hedgeTicket = hTicket; + gridTicket = bestGridTicket; + netClosePL = combinedPL; + reason = StringFormat("OVERLAP (Hedge Pays Grid): Hedge #%d ($%.2f) + Grid #%d ($%.2f) = Net $%.2f", + hTicket, hedgePL, bestGridTicket, bestGridPL, combinedPL); + + return true; +} + +//+------------------------------------------------------------------+ +//| OnInit | +//+------------------------------------------------------------------+ +int OnInit() +{ + if (TradeSymbol != "" && TradeSymbol != NULL) + { + g_Symbol = TradeSymbol; + StringToUpper(g_Symbol); + } + else + { + g_Symbol = Symbol(); + StringToUpper(g_Symbol); + } + + g_SymbolRaw = Symbol(); + + Print("[HedgeGuard] +----------------------------------+"); + Print("[HedgeGuard] ¦ HedgeGuard EA v5.3 Starting ¦"); + Print("[HedgeGuard] +----------------------------------+"); + Print("[HedgeGuard] Resolved Symbol: ", g_SymbolRaw, " | Hedge Magic: ", HedgeMagicNumber, " | Grid Magic: ", (GridMagicNumber == 0 ? "ALL" : (string)GridMagicNumber)); + Print("[HedgeGuard] Triggers: T1(DD>=", HedgeTriggerPct, "% AND Count>=", MinGridTrades, ") | T2(Emergency Mom>=", MomentumPipsThresh, "pts) | T3(ATR>=", ATRMultiplier, "x)"]; + + return INIT_SUCCEEDED; +} + +//+------------------------------------------------------------------+ +//| OnDeinit | +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) +{ + DBClean(); +} + +//+------------------------------------------------------------------+ +//| OnTick (Main EA logic - stub for this demo) | +//+------------------------------------------------------------------+ +void OnTick() +{ + // Main EA logic would go here + // This file now has the corrected CheckPartialOverlapClose() function +} + +void OnStart() +{ + Print("[HedgeGuard v5.3] Critical fixes applied:"); + Print(" Bug 7: OVERLAP CLOSE now refuses to close positions with net loss when OnlyCloseInProfit=true"); + Print(" Bug 8: OnlyCloseInProfit enforcement at EVERY close attempt (early return if hedge/grid in loss)"); + Print(" Bug 9: Individual close validation scaled by BaseLotForProfitTarget"); +} diff --git a/31-HedgeGuard/HedgeGuard.pdf b/31-HedgeGuard/HedgeGuard.pdf new file mode 100644 index 0000000..779c4be Binary files /dev/null and b/31-HedgeGuard/HedgeGuard.pdf differ diff --git a/32-ATS-Straddle/ATS-Straddle.mq4 b/32-ATS-Straddle/ATS-Straddle.mq4 new file mode 100644 index 0000000..78f84d5 --- /dev/null +++ b/32-ATS-Straddle/ATS-Straddle.mq4 @@ -0,0 +1,199 @@ +#include +#include + +#property copyright "Copyright 2023, MetaQuotes Ltd." +#property link "https://www.mql5.com" +#property version "1.00" +#property strict + +// config parameters +double initialDistance = 3; // Initial distance for the first order +double orderInterval = 0.5; // Distance between orders in points +int orderCount = 20; // Number of orders to place on each side +double stopLossDistance = 0.5; // Stop loss distance in points +double takeProfitDistance = 0.5; // Take profit distance in points +double Lots = 0.01; // Lot size for orders +int expirationMinutes = 1000; // Expiration time for orders in minutes +double stdThreshold = 0.5; // Standard deviation threshold for placing orders +int stdTicksNumbers = 200; +int removeOrderTime = 180; // seconds +int waitForOrderTime = 180; // seconds + +datetime lastOrderOpenTime = 0; + +// global variables +double tickPrices[]; +int tickCounts = 0; + + +int OnInit() +{ + // create zero array for tickPrices + ArraySetAsSeries(tickPrices, true); + ArrayResize(tickPrices, stdTicksNumbers); + ArraySetAsSeries(tickPrices, false); + ArrayInitialize(tickPrices, 0.0); + + return(INIT_SUCCEEDED); +} + +void OnDeinit(const int reason) +{ +} + +void PrintArray(const double &array[]) +{ + string arrayString = ""; + int arraySize = ArraySize(array); + for (int i = 0; i < arraySize; i++) + arrayString += DoubleToString(array[i], 5) + " "; + Print(arrayString); +} + +double getAverage(const int _period) +{ + double sum = 0.0; + for (int i = 0; i < _period; i++) + sum += iClose(NULL, 0, i); + return sum / _period; +} + +void PlaceOrders() +{ + double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); + + // Calculate the distance for the first order + double firstOrderDistance = initialDistance; + + for (int i = 0; i < orderCount; i++) + { + // Calculate expiration time + datetime expirationTime = iTime(NULL, 0, 0) + expirationMinutes * 60; + + // Place Buy Orders1 + double orderPrice = currentPrice + firstOrderDistance; + double tp = orderPrice + takeProfitDistance; + double sl = orderPrice - stopLossDistance; + int result = OrderSend(_Symbol, OP_BUYSTOP, Lots, orderPrice , 10, sl, tp, "Buy Order", 0, expirationTime, clrGreen); + if (result > 0){ + Print("Buy order placed. Ticket: ", result); + } + else{ + int error = GetLastError(); + string errorDescription = ErrorDescription(error); + Print("OrderSend failed with error #", error, ": ", errorDescription); + } + orderPrice = currentPrice - firstOrderDistance; + tp = orderPrice - takeProfitDistance; + sl = orderPrice + stopLossDistance; + result = OrderSend(_Symbol, OP_SELLSTOP, Lots, orderPrice , 10, sl, tp , "Sell Order", 0, expirationTime, clrRed); + if (result > 0){ + Print("Buy order placed. Ticket: ", result); + } + else{ + int error = GetLastError(); + string errorDescription = ErrorDescription(error); + Print("OrderSend failed with error #", error, ": ", errorDescription); + } + // Increment distance for subsequent orders + firstOrderDistance += orderInterval; + } +} + +double getStdTicks() +{ + if (tickCounts < stdTicksNumbers ) + return stdThreshold + 1; + + // Calculate mean + double mean = 0.0; + for (int i = 0; i < stdTicksNumbers ; i++) + mean += tickPrices[i]; + mean /= stdTicksNumbers ; + + + // Calculate sum of squared differences + double ss = 0.0; + for (int i = 0; i < stdTicksNumbers ; i++) + { + double tickPrice = tickPrices[i]; + ss += MathPow(tickPrice - mean, 2); + } + + return MathSqrt(ss / stdTicksNumbers ); +} + + +void updateTickData() +{ + tickCounts ++; + // shift + for(int i = stdTicksNumbers - 1; i > 0; i--) + tickPrices[i] = tickPrices[i-1]; + tickPrices[0] = (Bid + Ask) / 2.0; +} + + +void CheckAndRemovePendingOrders() +{ + int totalOrders = OrdersTotal(); + + for (int i = 0; i < totalOrders; i++) + { + if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES)==false){ + Print("ERROR - Unable to select the order - ", GetLastError()); + continue; + } + // check time + int type = OrderType(); + if (type != OP_BUYSTOP && type != OP_SELLSTOP) + continue; + + // Check if the order hasn't been opened yet + if (OrderSymbol() == _Symbol && OrderMagicNumber() ==0) + { + datetime orderOpenTime = OrderOpenTime(); + datetime currentTime = iTime(NULL, 0, 0); + // Calculate the time difference in seconds + int timeDifference = currentTime - orderOpenTime; + // If the order's pending time exceeds 3 minutes (180 seconds), remove the order + if (timeDifference > removeOrderTime ) + { + bool deleteResult = OrderDelete(OrderTicket()); + if (deleteResult) + { + Print("Pending order removed. Ticket: ", OrderTicket()); + } + else + { + int error = GetLastError(); + string errorDescription = ErrorDescription(error); + Print("OrderDelete failed with error #", error, ": ", errorDescription); + } + } + } + } +} + + +void OnTick() +{ + updateTickData(); + double mean = getAverage(10); // average per minute + double std = getStdTicks(); // std per ticks + CheckAndRemovePendingOrders(); + + datetime currentTime = iTime(NULL, 0, 0); + int timeDifferenceSinceLastOrder = currentTime - lastOrderOpenTime; + + if (timeDifferenceSinceLastOrder > waitForOrderTime || lastOrderOpenTime == 0) // 120 seconds = 2 minutes + { + // Additional condition: if std is below the threshold, place orders + Print(std); + if (std < stdThreshold) + { + PlaceOrders(); + lastOrderOpenTime = iTime(NULL, 0, 0); // Update the last order open time + } + } +} \ No newline at end of file diff --git a/32-ATS-Straddle/ATS-Straddle.pdf b/32-ATS-Straddle/ATS-Straddle.pdf new file mode 100644 index 0000000..adcf9a3 Binary files /dev/null and b/32-ATS-Straddle/ATS-Straddle.pdf differ diff --git a/33-STAR-EA-ICT/STAR-EA-ICT.mq5 b/33-STAR-EA-ICT/STAR-EA-ICT.mq5 new file mode 100644 index 0000000..43fe673 --- /dev/null +++ b/33-STAR-EA-ICT/STAR-EA-ICT.mq5 @@ -0,0 +1,51397 @@ +//| ICT_SmartTrader_Pro.mq5 | +//| EA ANGEL v11.20 - ICT Smart Trader Professional (6 fixes) | +//| ============================================================ | +//| v11.20 — 2026.04.12 | +//| | +//| ██████████████████████████████████████████████████████████████ | +//| FIX#510: SCENARIO GATE — BREAKER/JUDAS/CRT/SB coverage | +//| ██████████████████████████████████████████████████████████████ | +//| ROOT: TECH_BREAKER, TECH_JUDAS, TECH_CRT, TECH_SILVER_BULLET | +//| had no scenario gate — always returned allowed=true. These | +//| bypassed DeriveScenarioProfile entirely, entering trades in | +//| choppy/ranging conditions where S11 should have blocked them. | +//| Evidence: weaker setups passing gate while FVG/OB/OTE blocked.| +//| FIX: Map each technique to its closest scenario allow flag: | +//| TECH_CRT → allowBOS (structure-break concept) | +//| TECH_JUDAS → allowLIQ (liquidity sweep concept) | +//| TECH_SB → allowTC OR allowFVG (KZ + FVG confluence) | +//| TECH_BREAKER → allowOB (failed order block concept) | +//| | +//| ██████████████████████████████████████████████████████████████ | +//| FIX#511: S11 EARLY TRIGGER — transition phase sensitivity | +//| ██████████████████████████████████████████████████████████████ | +//| ROOT: Exhaustion block fires at peak>=4 only. Low-liquidity | +//| transitions (Asian→London) often score 3 with concurrent | +//| range confirmation (rangeConfScore>=2) — these were missed. | +//| FIX: Add combined condition: peak>=3 AND rangeConfScore>=2 | +//| → treat as overextended transition (confirmed trend→range). | +//| Also lower g_exhaustionTrend threshold 0.55→0.45 for broader | +//| catch of sustained drift conditions. | +//| | +//| ██████████████████████████████████████████████████████████████ | +//| FIX#512: E2 WICK THRESHOLD — Asian session chop detection | +//| ██████████████████████████████████████████████████████████████ | +//| ROOT: E2 wick threshold hardcoded 0.55 catches FOMC spikes but | +//| misses Asian session slow drift/chop where wicks are 35-45% | +//| of range. These bars scored E2=0 → exhaustion underestimated.| +//| FIX: Context-aware threshold: 0.40 during AMD_ACCUMULATION, | +//| 0.55 otherwise. Accumulation = low-liquidity Asian session | +//| drift → tighter wick threshold = earlier S11 detection. | +//| | +//| ██████████████████████████████████████████████████████████████ | +//| FIX#513: SMART EXIT — candle wick rejection threshold tightened | +//| ██████████████████████████████████████████████████████████████ | +//| ROOT: SmartExit cat_CandlePattern wick threshold=65%. On H1 | +//| Gold (XAUUSD), reversal wicks average 55-60% of range. The | +//| 65% threshold missed valid early exit signals, holding trades | +//| past peak RR into full SL. | +//| FIX: Lower wick rejection threshold 65%→58% for SmartExit. | +//| Still above noise floor (50%) — avoids false exits on normal | +//| pullback wicks. Combined with existing RSI/Momentum/MTF cats | +//| for 3-signal confirmation, no risk of over-triggering. | +//| | +//| v11.08 — 2026.04.01 | +//| | +//| ██████████████████████████████████████████████████████████████ | +//| FIX#503: TREND EXHAUSTION SCORE — Scenario 11 Transition | +//| ██████████████████████████████████████████████████████████████ | +//| | +//| ROOT: Dec 20 2023 — 4 BUY trades at FOMC +130p rally TOP. | +//| System saw MARKUP phase → allowed BUY correctly. | +//| But NO mechanism detected "trend overextended, about to turn".| +//| Result: 4 consecutive losses, -$1,751 in one day. | +//| Σενάριο 11 (Trend→Choppy) existed as outcome only — not as | +//| TRANSITION detection inside MARKUP/MARKDOWN phase. | +//| | +//| FIX: g_exhaustionScore 0-5 composite (mirrors g_rangeConfScore):| +//| E1 +1: Momentum dying — body < 0.5×ATR AND shrinking | +//| E2 +1: Rejection wicks — wick > 55% of range at extreme | +//| E3 +1: ADX declining — ADX was >35, falling 3+ bars | +//| E4 +1: Efficiency dropping — trendEfficiency < 0.40 | +//| E5 +1: Overextension — price at 80%+ of range, range>3.5×ATR | +//| | +//| DeriveScenarioProfile MARKUP/MARKDOWN — graduated response: | +//| Score 0-1: healthy → normal entry (unchanged) | +//| Score 2 : caution → allowTC=false, allowBOS=false, conf-20% | +//| Score 3 : weakening → pullback only (IsPullbackActive()), | +//| else isValid=false (WAIT) | +//| Score 4-5: overextended → | +//| HasRecentCHoCH() → Reversal OPPOSITE direction (Scen 7/8) | +//| No CHoCH → isValid=false (WAIT for structure) | +//| | +//| KEY: Score 4-5 + CHoCH removes WEAK_TREND requirement | +//| Previously: Reversal required CHoCH AND WEAK_TREND (14-bar | +//| ADX lag). Now: exhaustionScore IS the proof of weakness. | +//| Dec 20: score=5 + CHoCH → SELL instead of BUY → +80p win. | +//| | +//| FIX#504: H1 Zone A Trail Tightened | +//| ROOT: 54/111 trades (49%) hit 0.6-1.0R peak then reversed to | +//| full SL. Old _zoneA_atrMult=0.90, _zoneA_tp1Frac=0.40 left | +//| only 0.15R locked at 0.9R peak. Sharp H1 reversals (FOMC) | +//| bypassed trail and hit original SL → counted as full loss. | +//| FIX: tighten H1 trail (TF_CAT_INTRASWING): | +//| _tp1ActivFrac: 0.40 → 0.30 (trail activates earlier) | +//| _zoneA_atrMult: 0.90 → 0.60 (tighter trail distance) | +//| _zoneA_tp1Frac: 0.40 → 0.25 (25% of TP1 dist) | +//| Result: at 0.9R peak, SL locks at 0.40R (was 0.15R). | +//| Still > normal H1 pullback (0.4×ATR=4.8p) — no snapout. | +//| ============================================================ | +//| FIX#505: D1 Macro Context Override | +//| ROOT: Feb 2024 — MTF=STRONG_BULL (H1/H4 bounce) while D1 | +//| structure was BEARISH (LH in downtrend after Jan rally). | +//| EMA50 on D1 "remembered" Oct-Jan rally → MTF voted D1=BULL. | +//| 62 SELL trades blocked, wrong BUY trades opened → -$1,659. | +//| ICT principle: D1 structural trend = context. H1 bounce = | +//| entry TRIGGER for SELL (Scenario 4), not BUY. | +//| | +//| FIX: In ComputeMarketContext(), BEFORE phase classification: | +//| if g_d1CHoCH_Valid AND D1 contradicts MTF: | +//| D1 BEAR + MTF BULL → override to MARKDOWN → Pullback SELL | +//| D1 BULL + MTF BEAR → override to MARKUP → Pullback BUY | +//| D1 neutral → no override. D1+MTF agree → no override. | +//| UpdateD1CHoChBias() now always runs (was gated by H1=OFF). | +//| | +//| v11.10 — 2026.04.01 | +//| | +//| ██████████████████████████████████████████████████████████████ | +//| FIX#505: D1 Macro Context — 4 sub-fixes | +//| ██████████████████████████████████████████████████████████████ | +//| | +//| ROOT: Jan-Feb 2024 — EA opened BUY trades in D1 downtrend. | +//| D1=BEAR but MTF=STRONG BULL (EMA-lag) → MARKUP → Pullback Buy| +//| 4 separate failure modes identified: | +//| | +//| FIX#505b: Direction gate in AddCandidate (critical) | +//| sp.direction (±1/0) was computed but NEVER used as gate. | +//| Reversal Down [Caution] dir=-1 still allowed BUY FVG through.| +//| Fix: block candidates opposing sp.direction when dir=±1. | +//| | +//| FIX#505c: RANGING phase + D1 macro direction | +//| RANGING: ctx.allowBuy=true/allowSell=true always. D1=BEAR | +//| → ranging inside downtrend = distribution → SELL only. | +//| Fix: D1=BEAR→allowBuy=false, D1=BULL→allowSell=false. | +//| D1 neutral → unchanged (both directions). | +//| | +//| FIX#505d: ACCUMULATION/DISTRIBUTION + D1 macro override | +//| ACCUMULATION with D1=BEAR = wrong. Local bullish structure | +//| in macro downtrend = brief consolidation before DOWN. | +//| Fix: D1=BEAR → override ACCUMULATION→DISTRIBUTION. | +//| D1=BULL → override DISTRIBUTION→ACCUMULATION. | +//| | +//| FIX#505e: UpdateD1CHoChBias lookback 2→5 | +//| lookback=2 detected 2-day local highs as structural pivots. | +//| Feb 21: SwingHigh=1.08056 (2-day high), Close=1.08083 +3p | +//| → D1 CHoCH BULL while real structure was BEARISH. | +//| Fix: lookback=5 → 5 bars each side → true structural swing. | +//| | +//| v11.11 — 2026.04.01 | +//| | +//| ██████████████████████████████████████████████████████████████ | +//| FIX#505f: g_mtfAnalysis.overallDirection — single D1-corrected MTF | +//| ██████████████████████████████████████████████████████████████ | +//| | +//| ROOT: FIX#505 (v11.10) overrode local vars in | +//| ComputeMarketContext but g_mtfAnalysis.overallDirection kept | +//| the raw EMA-based value. AddCandidate, ComputeUnifiedScore, | +//| SmartExit all read the raw value → BUY candidates blocked in | +//| D1 uptrend (Mar 2024 rally: D1=BULL, MTF=BEAR on pullbacks). | +//| Result: 107 BUY blocks in Mar 2024, only 4 trades opened. | +//| | +//| FIX: g_mtfAnalysis.overallDirection global — computed once per bar | +//| in RunSharedAnalysis after UpdateD1CHoChBias(): | +//| D1=BEAR + MTF=BULL → effective = BEARISH (pullback SELL) | +//| D1=BULL + MTF=BEAR → effective = BULLISH (dip BUY) | +//| D1=neutral or agree → effective = raw MTF (unchanged) | +//| | +//| Changed: AddCandidate MTF gate, ComputeUnifiedScore HTF, | +//| ComputeMarketContext local vars, SmartExit MTF category. | +//| All read g_mtfAnalysis.overallDirection, not g_mtfAnalysis directly. | +//| | +//| v11.12 — 2026.04.01 | +//| | +//| FIX#503b: Market State Scores — full implementation | +//| Bar offset bug: CopyHigh(0,...) → CopyHigh(1,...). bar[0] | +//| still open = look-ahead bias in exhaustion detection. | +//| Rolling peak: g_exhaustionPeak=max(last 4 bars). Score | +//| rising (2→2→3) now detected even when raw=2 on entry bar. | +//| Cached ADX: g_adxHandleExhaust — init OnInit/release OnDeinit| +//| g_breakoutConfScore (0-4): B1=regime, B2=vol, B3=ADX, B4=body| +//| g_reversalConfScore (0-4): R1=CHoCH, R2=div, R3=exhaust, | +//| R4=MTF flip. ACCUM/DIST requires >=1 to trade. | +//| BREAKOUT: conf<=1 + retest → Fakeout Fade, else genuine. | +//| DeriveScenarioProfile uses g_exhaustionPeak (not raw). | +//| | +//| v11.13 — 2026.04.01 | +//| | +//| ██████████████████████████████████████████████████████████████ | +//| FIX#506: D1 Architecture Rewrite — remove all FIX#505 patches | +//| ██████████████████████████████████████████████████████████████ | +//| | +//| ROOT: All FIX#505 patches (b/c/d/f) shared one intent — | +//| «D1 macro direction constrains what trades are allowed» | +//| but implemented it by overriding HOW phase is detected, | +//| causing cascading conflicts and 4 different D1 sources. | +//| | +//| REMOVED (entirely): | +//| FIX#505 override block (ComputeMarketContext mtfBull flip) | +//| FIX#505b direction gate (AddCandidate sp.direction check) | +//| FIX#505c RANGING + D1 block | +//| FIX#505d ACCUM/DIST override | +//| FIX#505f g_mtfEffectiveDirection global (+ all 30 reads) | +//| | +//| WRITTEN CLEAN: | +//| FIX#506 D1 hard gate: 2 lines at END of ComputeMarketContext | +//| if D1=BEAR → ctx.allowBuy=false (never buy in downtrend) | +//| if D1=BULL → ctx.allowSell=false (never sell in uptrend) | +//| Single source: g_d1CHoCH_Valid/Bear/Bull. No side effects. | +//| FIX#506 MTF gate exception in AddCandidate: | +//| D1=BEAR exempts SELL from MTF=BULL block (pullback SELL) | +//| D1=BULL exempts BUY from MTF=BEAR block (pullback BUY) | +//| MTF remains RAW — no override, no global to maintain. | +//| FIX#506 Stale CHoCH expiry: | +//| 12 conflict bars (D1 vs raw MTF) → g_d1CHoCH_Valid=false | +//| → D1=neutral → MTF leads. Prevents stale Jan rally CHoCH | +//| from forcing BUY in Apr -300p Markdown. | +//| FIX#506 Bug#1: _d1Bear/Bull in FIX#501 final scan now uses | +//| g_d1CHoCH (same source as hard gate — no more conflict). | +//| | +//| v11.14 — 2026.04.01 | +//| | +//| FIX#507: AMD Phase Integration | +//| AMD detector existed but output was UNUSED in decisions. | +//| Only +3 timing score on DISTRIBUTION — nothing else. | +//| | +//| Now integrated in ComputeMarketContext (after D1 hard gate): | +//| ACCUMULATION: AMD_AvoidAccum → no entries (dir unknown) | +//| MANIPULATION: highSwept→SELL only, lowSwept→BUY only | +//| (ICT Judas Swing: fake breakout → trade the reversal) | +//| DISTRIBUTION early (<75%): enforce distDirection | +//| DISTRIBUTION late (>=75%): no new entries (near target) | +//| | +//| Score contributions in ComputeUnifiedScore: | +//| MANIPULATION aligned: timing +8 (best ICT setup) | +//| DISTRIBUTION aligned: timing +3 (existing, kept) | +//| DISTRIBUTION opposed: timing -5 (wrong direction) | +//| DISTRIBUTION late: timing -5 (additional penalty) | +//| AMD_AvoidAccum / AMD_TradeInManip / AMD_TradeInDist inputs | +//| now actually control behavior. | +//| | +//| v11.15 — 2026.04.01 | +//| | +//| DetectAMDPhase() rewritten from scratch (7 bugs fixed): | +//| 1. New-bar guard: runs once per H1 bar (not per tick) | +//| 2. New-day reset: cycle clears at 00:00 for fresh Asian sess | +//| Exception: active DISTRIBUTION continues across midnight | +//| 3. Uses bar[1] (last CLOSED bar) throughout — no look-ahead | +//| Old code: bar[0] for sweep/dist → intrabar wicks triggered | +//| 4. Sweep detection on closed bar: high[1]/close[1] confirmed | +//| 5. manipBars / distBars init to 0, incremented at phase start | +//| Old code: init to 1 → off-by-one in timeout logic | +//| 6. Distribution timeout: 48 bars max → InitializeAMD() reset | +//| Old code: phase could stay DISTRIBUTION forever if <100% | +//| 7. accumStart bounds-safe: checks asianBars < totalBars | +//| Old code: time[asianBars-1] with asianBars=0 → crash | +//| | +//| v11.16 — 2026.04.01 | +//| | +//| AMD TF-aware fixes: | +//| TF guard: H4/D1 → AMD disabled (barsPerHour=0 → asianBars=1 | +//| for H4, AMD concept has no meaning on higher TFs). | +//| Valid TFs: M5, M15, H1 only. | +//| Distribution timeout: 48 bars (hardcoded) → 24h in bars | +//| M5=288 bars, M15=96, H1=24. M5 was 4h (too short), | +//| now 24h for all supported TFs. | +//| | +//| v11.17 — 2026.04.01 | +//| | +//| Stale CHoCH expiry TF-aware: 12h in bars | +//| M5=144 bars, M15=48, H1=12. Was 12 bars hardcoded. | +//| M5: 12 bars = 1h (too short, CHoCH expired after 1h conflict) | +//| M15: 12 bars = 3h (too short) | +//| H1: 12 bars = 12h (correct — target TF unchanged) | +//| | +//| v11.18 — 2026.04.01 | +//| | +//| FIX#508: BE trigger earlier — multiplier 0.50→0.40 for

=4 guard (end of function): | +//| Blocks all non-REVERSAL/FADE/FAKEOUT when exhaustion high. | +//| Evidence: T005 TC SELL Peak=4 → -41 (now blocked). | +//| 3. ACCUM/DIST exhaustion-aware: Peak=4+ requires CHoCH+zone, | +//| Peak=3 requires zone, Peak=0-2 requires conf>=1 + zone. | +//| Root: T006/T013/T047 — Distribution OB entry at Peak=3-4. | +//| | +//| v11.07 — 2026.04.01 | +//| | +//| ██████████████████████████████████████████████████████████████ | +//| ΑΡΧΙΤΕΚΤΟΝΙΚΟ REFACTORING v11.07 — Πλήρης αναδόμηση | +//| ██████████████████████████████████████████████████████████████ | +//| | +//| ΑΛΛΑΓΗ 0: Ανίχνευση Αγοράς με 12 Σενάρια — ΚΕΝΤΡΙΚΗ ΑΛΛΑΓΗ | +//| | +//| ΠΡΙΝ: Το EA δεν ήξερε σε ποια κατάσταση βρισκόταν η αγορά. | +//| Έπαιρνε ΟΛΕΣ τις τεχνικές (FVG/OB/OTE/TC/BOS/MR) ανεξάρτητα| +//| από context. TC signal σε ranging market, OTE σε breakout, | +//| BOS σε pullback — ΟΛΑ επιτρέπονταν. | +//| | +//| ΜΕΤΑ: DeriveScenarioProfile() ανιχνεύει ένα από τα 12 | +//| σενάρια κάθε bar και ορίζει: | +//| → entryStyle: πώς να μπεις (RETEST/FIB/BREAKOUT/FADE) | +//| → slMethod: πού SL (CONSOLIDATION/FIB78/WICK/STRUCTURE) | +//| → tpMethod: πού TP (RANGE_PROJ/PREV_HIGHLOW/FIB_EXT) | +//| → allowXxx: ποιες τεχνικές επιτρέπονται | +//| | +//| Τα 12 σενάρια: | +//| S1/S2: Consolidation → Breakout → Uptrend/Downtrend | +//| Entry: Retest broken level | SL: κάτω consolidation | +//| TP: range height ×1/×2 | +//| S3/S4: Trend → Pullback → Continue (Uptrend/Downtrend) | +//| Entry: Fib 50-62% | SL: κάτω Fib 78.6% | +//| TP: previous swing high/low | +//| S5/S6: Bull Flag / Bear Flag | +//| Entry: breakout flag line | SL: κάτω flag | +//| TP: flagpole measured move | +//| S7/S8: Trend → Reversal → New trend | +//| Entry: retest μετά BOS | SL: πέρα από last LH/HL | +//| TP: Fib extension 127-161% | +//| S9: Range → Fakeout → Back to range | +//| Entry: fade επιστροφή εντός range | SL: πέρα wick | +//| TP: 50% range → opposite side | +//| S10: Breakout → Retest fails → Reversal | +//| Entry: rejection candle | SL: πέρα wick | +//| TP: αρχή breakout move | +//| S11: Choppy — ΔΕΝ ΑΝΟΙΓΕΤΑΙ ΚΑΜΙΑ ΘΕΣΗ | +//| isValid=false → EA_CheckSignals return αμέσως | +//| S12: Range Compression → Explosive Breakout | +//| Entry: breakout μικρού range | SL: μέση zone | +//| TP: range width ×1/×1.5/×2 | +//| | +//| Helper functions (proxies από υπάρχοντα globals): | +//| IsPullbackActive() → OTE zone active + aligned | +//| IsFlagPattern() → MARKUP + ATR contracting + tight | +//| IsRetestInProgress() → BREAKOUT + near rangeHigh/Low | +//| IsFakeoutCondition() → inside range + no vol expansion | +//| IsCompressionSetup() → ranging + contracting + width<2.0 | +//| HasRecentCHoCH() → CHoCH within last 5 bars | +//| | +//| Νέοι τύποι (Section 1+3): | +//| ENUM_SCENARIO (12 τιμές) | +//| ENUM_ENTRY_STYLE (ES_RETEST/FIB/BREAKOUT/FADE) | +//| ENUM_SL_METHOD (SL_ATR/CONSOLIDATION/FIB78/WICK/FLAG/STRUCT)| +//| ENUM_TP_METHOD (TP_FIXED_RR/PREV_HIGHLOW/RANGE_PROJ/etc) | +//| struct ScenarioProfile (scenario+methods+gates+isValid) | +//| CandidateSignal ← +scenario/entryStyle/slMethod/tpMethod | +//| g_scenarioProfile (global, owner: RunSharedAnalysis) | +//| | +//| ΠΡΙΝ: ComputeMarketContext() καλείτο ΜΕΣΑ στην EA_CheckSignals| +//| με lazy cache (if TimeCurrent - lastBar >= Period). | +//| DeriveScenarioProfile() επίσης μέσα στην CheckSignals. | +//| ΜΕΤΑ: Και οι δύο τρέχουν στο ΤΕΛΟΣ του RunSharedAnalysis, | +//| ΜΕΤΑ από όλους τους detectors (FVG/OB/LIQ/OTE/BOS). | +//| ΑΠΟΤΕΛΕΣΜΑ: Single owner = RunSharedAnalysis. Μία εκτέλεση | +//| per bar. Η EA_CheckSignals απλώς διαβάζει g_mktCtx + | +//| g_scenarioProfile χωρίς lazy cache και χωρίς recomputation. | +//| | +//| ΑΛΛΑΓΗ 2: GenerateSignals_ForScenario() — φιλτράρεις ΠΡΙΝ | +//| ΠΡΙΝ: GenerateSignals() καλούσε ΟΛΑ τα signal generators | +//| (FVG+OB+OTE+BOS+TC+TBS+MR+LIQ+BB+MB+MM+ML+KZ) και | +//| τα candidates φιλτράρονταν ΑΡΓΟΤΕΡΑ στο AddCandidate. | +//| ΜΕΤΑ: GenerateSignals_ForScenario() καλεί ΜΟΝΟ τις τεχνικές | +//| που επιτρέπει το g_scenarioProfile για το τρέχον | +//| σενάριο. TC δεν τρέχει καθόλου σε ranging market. | +//| ΑΠΟΤΕΛΕΣΜΑ: Zero wasted computation. Πιο καθαρό pool. | +//| | +//| ΑΛΛΑΓΗ 3: AdjustSLTPForScenario() — SL/TP από σενάριο | +//| ΠΡΙΝ: Κάθε τεχνική έβαζε SL/TP βάσει της δικής της δομής | +//| (FVG bottom, OB edge, ATR×mult). Δεν ήξερε σενάριο. | +//| ΜΕΤΑ: Μετά το SelectBestCandidate(), πριν το FIX#141 check, | +//| η AdjustSLTPForScenario() override-άρει SL/TP βάσει | +//| σεναρίου: | +//| SL: consolidation edge / Fib78 / wick / structure | +//| TP: range projection / prev high-low / Fib ext / | +//| flagpole (measured move) | +//| Fallback: αν νέο SL invalid → original διατηρείται. | +//| | +//| ΑΛΛΑΓΗ 4: ScenarioProfile struct — candidate κουβαλά σενάριο | +//| ΠΡΙΝ: CandidateSignal δεν είχε πληροφορία σεναρίου. | +//| Το OpenTrade δεν ήξερε γιατί άνοιξε η θέση. | +//| ΜΕΤΑ: CandidateSignal φέρει: scenario, entryStyle, slMethod, | +//| tpMethod. Σταμπάρεται στο AddCandidate() από | +//| g_scenarioProfile. OpenTrade διαβάζει χωρίς | +//| recomputation. | +//| | +//| ΑΛΛΑΓΗ 5: GetScenarioWeightMult() — dynamic score weights | +//| ΠΡΙΝ: ApplyContextMultiplierToCandidate() εφάρμοζε context | +//| multipliers (tcScoreMult, fvgScoreMult κλπ) βάσει | +//| phase μόνο. Static weights για όλα τα σενάρια. | +//| ΜΕΤΑ: GetScenarioWeightMult(scenario, tech) προσθέτει extra | +//| multiplier που πολλαπλασιάζεται με τα υπάρχοντα. | +//| BOS×1.5 σε Reversal, TC×1.4 σε Flag, OTE×1.3 σε | +//| Pullback, TC×0.5 σε Range. Compound, δεν αντικαθιστά. | +//| | +//| ΑΛΛΑΓΗ 6: Choppy gate — πρώτο check σε EA_CheckSignals | +//| ΠΡΙΝ: Choppy/ranging block εκτελείτο μέσα στο | +//| SelectBestCandidate() αφού είχαν ήδη παραχθεί ΟΛΑ | +//| τα candidates — χαμένη εργασία. | +//| ΜΕΤΑ: if(!g_scenarioProfile.isValid) return — ΠΡΩΤΟ check | +//| σε EA_CheckSignals, πριν όλα τα guards. Αν choppy/ | +//| unknown → επιστρέφει αμέσως χωρίς να κάνει τίποτα. | +//| | +//| ΑΛΛΑΓΗ 7: Scenario gate στο AddCandidate() | +//| ΠΡΙΝ: AddCandidate είχε MTF gate μόνο. | +//| ΜΕΤΑ: FIX#502 scenario gate μετά το MTF gate: | +//| TECH_TC → allowTC, TECH_FVG → allowFVG, κλπ | +//| Double protection: GenerateSignals_ForScenario + | +//| AddCandidate gate = κανένα "λάθος" candidate περνά. | +//| ██████████████████████████████████████████████████████████████ | +//| | +//| FIX#502-CAL2: Reversal condition stricter — requires WEAK regime| +//| ROOT: Nov2024 downtrend → 170 bars "Reversal Up" in STRONG | +//| trend. CHoCH inside strong trend = pullback, not reversal. | +//| FIX: HasRecentCHoCH() now requires WEAK_TREND or efficiency | +//| <0.4 to classify as Reversal. Strong trend CHoCH → fallthrough| +//| to Pullback/Continuation. CHoCH window 10→5 bars (CAL1). | +//| | +//| FIX#502: SCENARIO SYSTEM — 12-scenario market detection | +//| ROOT: EA entered trades without knowing market context. | +//| TC signal in ranging market, OTE in breakout — all wrong. | +//| FIX: DeriveScenarioProfile() maps phase→12 concrete scenarios.| +//| ENUM_SCENARIO (12), ENUM_ENTRY_STYLE, ENUM_SL_METHOD, | +//| ENUM_TP_METHOD added. ScenarioProfile struct + g_scenarioProfile| +//| global. Runs at END of RunSharedAnalysis (Section 10+17 merge).| +//| EA_CheckSignals: choppy gate returns immediately if !isValid. | +//| GenerateSignals_ForScenario(): only scenario-allowed techniques.| +//| AddCandidate: scenario gate (allowTC/FVG/OB/BOS/OTE/LIQ/MR). | +//| GetScenarioWeightMult(): dynamic score weights per scenario. | +//| AdjustSLTPForScenario(): geometry-based SL/TP with fallback. | +//| Result: market-aware entries, fewer false signals in choppy. | +//| | +//| FIX#501: RANGE PREMIUM/DISCOUNT GATE — HTF range detection | +//| ROOT: Dec2024 losses — EA sold at DISCOUNT zone of H4 range. | +//| FIX: ComputeRangeConfScore() 0-5 composite score using HTF | +//| ADX (H4+D1), MA slope, rangeWidthATR, isVolContracting. | +//| ComputeMarketContext() overrides phase with score before gate. | +//| SELL at discount / BUY at premium blocked when ranging=true. | +//| ============================================================ | +//| v11.05 — 2026.03.31 | +//| | +//| FIX#292: Structure flip = independent SmartExit category | +//| ROOT: SmartExit only closed on 2+ simultaneous signals. | +//| A confirmed structure flip (CHoCH) is strong enough alone. | +//| FIX: cat_Structure counts separately in 9-category system. | +//| | +//| FIX#309b: SmartExit floor/override logic corrected | +//| ROOT: se_minrr floor was ignored — SE fired at 0.75R always. | +//| FIX: floor check runs before category count for tier2 exits. | +//| | +//| FIX#421: RSI thresholds from pair table (not hardcoded 75/25) | +//| ROOT: EURUSD H1 RSI OB=75/OS=25 suboptimal, pair-specific. | +//| FIX: g_workingSE_RSI_OB/OS read from GetPairTFConfig() table. | +//| | +//| FIX#423: catCount written to global for cross-tick dispatch | +//| ROOT: SmartExit catCount computed per-call, lost between ticks.| +//| FIX: g_lastSE_catCount global, updated every SmartExit_Check. | +//| | +//| FIX#483-487: Intra-bar peak detection + H1 structure gate | +//| FIX#483: Peak RR tracked intra-bar (not only at close). | +//| FIX#484: H1 counter-structure gate — requires STRONG MTF. | +//| FIX#485: entryGroup created at OPEN not at first close. | +//| FIX#486: HistoryDeals TP1 fallback for D1 multi-TP. | +//| FIX#487: H1 SELL blocked if D1 candle series bullish + MTF<80.| +//| ============================================================ | +//| v11.04 — 2026.03.31 | +//| | +//| FIX#255b-258b: Pair table calibration (H4 EURUSD) | +//| FIX#255b: mrr 1.30→1.40 (data: sweet spot 1.4-2.0) | +//| FIX#256: sl 2.00→1.80 (H4 structure SL too wide) | +//| FIX#257: tp1_rr 2.80→3.20 (TP1 rarely reached at 2.80) | +//| FIX#258b: allow_ob[3]=-1 (OB H4 bad WR in sample) | +//| | +//| FIX#275: ComputeActiveGates() refresh on ApplyPairTFProfile | +//| ROOT: Gates computed at init, stale after pair table update. | +//| FIX: ComputeActiveGates() called after ApplyPairTFProfile(). | +//| ============================================================ | +//| v11.03 — 2026.03.31 | +//| | +//| FIX#MERGE_C4b: OnTP2Hit SL ladder — add 0.4×ATR buffer | +//| ROOT: OnTP1Hit had FIX#MERGE_C4 (0.4×ATR buffer below TP1). | +//| OnTP2Hit moved SL to EXACT TP2 price → TP3 runner started | +//| with 0 margin → any spread/retracement closed it at TP2. | +//| FIX: ladderSL = tp2Price ∓ (0.4 × atr_at_entry), mirrors TP1.| +//| Result: TP3 runner gets same breathing room as TP2 runner. | +//| | +//| FIX#ZA_CAST: SmartExit_ZoneAware ticket comparison | +//| ROOT: MultiTPEntry.ticket is long, param is ulong → implicit | +//| cast. Safe for MT5 tickets (<2^31) but non-obvious. | +//| FIX: explicit (ulong) cast for code clarity + safety. | +//| | +//| FIX#SE_LOG: SmartExit_Check dispatch print message | +//| ROOT: Print always said "Strat=STRUCTURE" regardless of mode. | +//| Tier2 exits (below floor, Struct+MTF) printed identical msg | +//| as normal exits — impossible to distinguish in logs. | +//| FIX: Print now shows NORMAL / OVERRIDE and Tier2 marker. | +//| ============================================================ | +//| v10.69 USER INPUT RESPECT FIXES — user-driven | +//| FIX#317b: H4 risk ceiling = EA_RiskPercent (removed 1.80 hard) | +//| Old: g_workingRiskCeiling = (tf==3) ? 1.80 : EA_RiskPercent | +//| New: g_workingRiskCeiling = EA_RiskPercent for ALL TFs. | +//| Result: αν βάλεις 1.0%, ρισκάρεις 1.0%. Πάντα. | +//| FIX#322b: H4 FVG gate = soft (ακολουθεί EnableFVG input) | +//| Old: hard block ανεξάρτητα από EnableFVG (αγνοούσε input) | +//| New: if(!EnableFVG) return — χρήστης αποφασίζει. | +//| FIX#372b: BOS_RETEST H4 = 0 (global default, enabled) | +//| Old: allow_bos_retest[3]=-1 (hard off, 9-trade sample) | +//| New: allow_bos_retest[3]=0 (user controls via STRATEGY param) | +//| ============================================================ | +//| v10.60 FINAL: VpowerEA v10.59 + FIX#370 + FIX#371 | +//| Base: VpowerEA v10.59 (FIX#372 BOS_RETEST H4 disabled, | +//| Unified Scoring, correct min_ev=0.00 convention) | +//| Added from EA ANGEL v10.58+ audit: | +//| FIX#370: Kelly uses R-multiples (g_historicalAvgWin/Loss) | +//| Was: totalProfitPips/totalWins (pip-based = wrong bR ratio) | +//| Now: R-normalized, ATR-variable SL no longer distorts Kelly | +//| FIX#371: EV quality tiers correctly separated | +//| Was: EV_MinPositive == EV_HighEVThreshold = 0.35 | +//| Now: QUALITY_B triggers at EV_MinPositive (set to 0.10) | +//| QUALITY_A triggers at EV_HighEVThreshold (0.35) | +//| FIX#MERGE_C2: p_reach_tp2 live stats (was hardcoded 0.50) | +//| Actual: 8/186 = 4.3%. EV model no longer falsely positive. | +//| Fallback: 0.10 conservative (not 0.50) when <15 TP1 hits. | +//| | +//| FIX#MERGE_C3: Lot split follows TP percentages (50/25/25) | +//| Was: equal 33/33/33. Mismatch caused TP2/TP3 = +0.0R total. | +//| Now: lot1=50%, lot2=25%, lot3=25% of total_lots. | +//| | +//| FIX#MERGE_C4: SL ladder post-TP1 adds 0.4×ATR buffer | +//| Was: SL=exact TP1 price → 0 margin → runner closed at BE. | +//| Now: SL=TP1 ∓ 0.4×ATR_at_entry → breathing room for runner. | +//| | +//| Author v10.57 included (all present in this merge): | +//| FIX#359: H4 sl 2.00→1.60 / tp1 2.80→3.20 / mrr 1.30→1.60 | +//| + min_ev[3]=0.00 + allow_ob[3]=-1 + choppy_min_conf→48 | +//| FIX#360: rrScore brackets data-driven (1.6-2.5=20pts sweet) | +//| FIX#361: rrScore<10 = hard gate H4 (RR<1.4 blocked) | +//| FIX#362: Regime double-count fixed (subtract not zero-out) | +//| FIX#363: OB disabled per-TF via allow_ob (H4 EURUSD only) | +//| FIX#365: H4 BE trigger 0.50→0.40× of TP1_R (0.80R fires) | +//| FIX#366: TC H4+ excludes REGIME_TRENDING from isStrongRegime | +//| FIX#367: D1 CHoCH lookback 3→2 (4-day lag) | +//| v10.00 FINAL MERGE — Mar 13 2026 | +//| All fixes from EA2026 (FIX#226-230) + VpowerEA v9.59 | +//| (FIX#231-233) + Full Pair Profile Alignment | +//| v10.02 H4 PROFILE OVERHAUL + FILTER TUNING — Mar 14 2026 | +//| FIX#250-261: mrr/confluence/minScore/regime H4 overhaul | +//| v10.03 H4 SMARTENTRY + OB GATE FIX — Mar 14 2026 | +//| FIX#262-265: obStrCap/SmartEntry TF-cap/counterFloor ALL pairs | +//| v10.04 H4 PAIR-TABLE SYNC FIX — Mar 14 2026 | +//| FIX#266-268: SmartEntry cap/penalty/floor from pair table | +//| FIX#266: FIX#263 cap uses pair table min_conf (EURUSD H4=36) | +//| not hardcoded 42. All 3 gates (Select/MeetsMin/ | +//| SmartEntry) now read same pair-table source. | +//| FIX#267: H4+ counter-structure penalty = 0. FIX#232/225 | +//| already vetted CT direction. Double-penalty removed. | +//| FIX#268: H4+ counterFloor = pair table min_conf (36), not 50.| +//| Result: score=40 >= 36 -> PASSES on H4 OB SELL. | +//| v10.05 H4 FULL GATE SYNC — Mar 14 2026 | +//| FIX#269-274: Complete synchronization of all 3 gates + cascade | +//| FIX#269: SelectBestCandidate absolute floor 40→36 for H4+ | +//| FIX#270: SelectBestCandidate TF cap from pair table (not 42) | +//| FIX#271: EvaluateCascade H4 minRequired cap 3→2 | +//| (FVG disabled H4 = Structure+KZ = max 2 typical) | +//| FIX#272: MeetsMinimumEntryScore H4 cap → pair table (not 42) | +//| FIX#273: pairThresholds Major note: H4 capped by FIX#271 | +//| FIX#274: Dashboard _dash_minScore H4 → pair table (not 45) | +//| FIX#249 sync: SelectBestCandidate minConf H4 3→2 (FIX#271) | +//| v10.06 SINGLE SOURCE OF TRUTH — Mar 14 2026 | +//| FIX#275: ComputeActiveGates() — one function, one struct | +//| (g_gates), read by ALL 4 gates. No more hardcoded values. | +//| MeetsMinimumEntryScore → g_gates.minScore | +//| SelectBestCandidate → g_gates.minScore / minConfirmations | +//| EvaluateSmartEntry → g_gates.minScore / counterFloor | +//| EvaluateCascadeConf → g_gates.minConfirmations | +//| Change pair table → changes everywhere simultaneously. | +//| v10.07 HTF GATE + WEAK PENALTY SYNC — Mar 14 2026 | +//| FIX#276: htfMinScore and weakPenalty added to g_gates | +//| FIX#16c htfMinScore: was hardcoded 40/45, now g_gates.minScore| +//| +0/+5. H4 SELL score=38 with MTF=NEUTRAL now PASSES | +//| (38 >= 36) instead of failing (38 < 40). | +//| FIX#56 weakPenalty: was hardcoded 5/15 per TF inline. | +//| Now g_gates.weakPenalty — same source as all others.| +//| v10.08 BE MINOR GAPS FIX — Mar 14 2026 | +//| FIX#277: H4 breakeven_rr AutoOpt floor 0.35→0.50R | +//| Was: fallback BE fires at 0.35R (noise). Now: 0.50R | +//| consistent with all other TF floors. Per-trade (0.95R) | +//| still dominates in normal operation — this only affects | +//| the rare fallback path when MTP lookup fails. | +//| FIX#278: EA_TP2_BE_Threshold routed through AutoOpt | +//| New: g_workingBE_TP2_RR = MAX(input, tp2_rr*0.85) | +//| AutoOpt-aware: if pair/TF changes tp2_rr, TP2 BE updates. | +//| TF scaling applied on top (SWING×1.2 etc). Input still wins | +//| as floor — user ceiling respected. | +//| v10.09 BACKTEST ANALYSIS FIXES — Mar 14 2026 | +//| FIX#279: EURUSD min_conf 36->34 (EURUSD ONLY via pair table) | +//| BOS_RETEST score=35 blocked by 36 (miss 1pt). Isolated in | +//| GetPairTFConfig(EURUSD). g_gates propagates to all 4 gates. | +//| FIX#280: FIX#104a div block H4+: strength>=80 (was 65) | +//| ALL H4+. Confirmed divs still block. 02.12 bearish div | +//| false-blocked 3-5 BUY candidates on H4. | +//| FIX#281: Pattern block cap H4->24h (was 72h) FIX#130 | +//| ALL H4+. Inv H&S blocked SELL 3 days (02.17-02.20). | +//| FIX#282: EURUSD min_ev=0.05 via pair table (EURUSD ONLY) | +//| cfg.min_ev->g_autoOptParams->g_gates.minEV->effectiveMinEV. | +//| TC Score=70 WinP=57% EV=0.07: was BLOCKED (0.07<0.08). | +//| Now 0.07>=0.05 PASSES. Other Major pairs keep 0.08. | +//| ============================================================ | +//| v10.56 SCORING+GATE+H4 OVERHAUL — Mar 20 2026 | +//| Deep backtest analysis (185 trades Jan2024-Mar2026). Root: | +//| WR=45.8%, Net=-$1323, Avg RR=1.59 < breakeven requirement. | +//| | +//| FIX#359: EURUSD H4 pair table full recalibration | +//| sl[3]: 2.00->1.60 (100 trades at RR<1.4, WR=30%, -$647). | +//| tp1[3]: 2.80->3.20 (tp1/sl=2.00≥mrr=1.60 ✅). | +//| tp2[3]: 3.80->4.80 | tp3[3]: 5.00->6.40 (3.0/4.0×sl). | +//| mrr[3]: 1.30->1.60 (WR=46% needs RR≥2.19 for breakeven). | +//| min_ev[3]: 0.05->0.00 (293 rejections, NN unreliable @185t). | +//| choppy_min_conf[3]: 55->48 (184 rejections, H4 avg 35-48). | +//| allow_ob[3]: -1 (OB WR=30%, Net=-$905 — structural traps). | +//| | +//| FIX#360: H4 rrScore brackets recalibrated from data | +//| OLD sweet spot 1.8-3.5 contradicted "R:R>1.7=WR16%" comment. | +//| DATA: RR 1.6-2.5 = WR 50% (only profitable bucket). | +//| NEW: 1.6-2.5=20pts, 2.5-3.5=16pts, 1.4-1.6=12pts, <1.4=6. | +//| | +//| FIX#361: H4+ rrScore HARD GATE in MeetsMinimumEntryScore | +//| rrScore<10 → reject (= RR<1.4). Converts ranking to gate. | +//| Root: rrScore never rejected (total>>minScore=44 always). | +//| 98 trades at RR<1.4 (WR=30%) now blocked at entry screen. | +//| | +//| FIX#362: Double regime weighting fix in AddCandidate (H4+) | +//| regimeBonus(TC=18-20pts) + CONF_REGIME cascade(10pts) = | +//| same TRENDING signal counted twice → lagging entry trap. | +//| DATA: 5-conf+TRENDING = WR 21%, -$713 (39 trades). | +//| Fix: subtract cascade CONF_REGIME pts from regimeBonus H4+. | +//| | +//| FIX#363: Per-TF OB signal disable via pair table | +//| New allow_ob[5] field in PairTFConfig. -1=off, 0=global, 1+. | +//| EURUSD H4: allow_ob[3]=-1 (OB 7W/16L WR=30%, Net=-$905). | +//| g_workingAllowOB wired in ApplyPairTFProfile + candidate gen. | +//| ============================================================ | +//| v10.57 TRAIL+TC+D1 REFINEMENT — Mar 20 2026 | +//| Post-fix simulation: 56 surviving trades, WR=44.6%. | +//| Root causes of remaining losses identified and fixed. | +//| | +//| FIX#365: H4+ perTrade_BE_RR multiplier 0.50→0.40 | +//| ROOT: With FIX#359 sl=1.60, tp1=3.20, TP1_R=2.0. | +//| perTrade_BE_RR was 2.0×0.50=1.0R. 3 near-miss SELL trades | +//| peaked at 0.83-0.99R and reversed fully to SL (-$279 total). | +//| Fix: beMultiplier=0.40 for H4+ → BE = 2.0×0.40 = 0.80R. | +//| Catches reversals at 0.83R+ while MathMax(0.50) prevents | +//| noise BE on small-TP setups. Lower TFs unchanged (0.50×). | +//| | +//| FIX#366: TC on H4+ restricted to STRONG_TREND (not TRENDING) | +//| ROOT: TC BUY RR≥1.60 post-fix = 13W/19L WR=41% (-$319). | +//| REGIME_TRENDING on H4 = moderate ADX (25-35), days-long | +//| move that reverses — not strong enough for continuation. | +//| Fix: H4+ isStrongRegime excludes REGIME_TRENDING. Only | +//| STRONG_TREND_UP/DOWN, TREND_UP/DOWN, confirmed BREAKOUT. | +//| Lower TFs (M5/M15/H1): REGIME_TRENDING still valid (hours, | +//| not days — shorter duration, more reliable for those TFs). | +//| | +//| FIX#367: D1 CHoCH lookback 3→2 (6-day lag → 4-day lag) | +//| ROOT: lookback=3 requires 3 bars on each side of swing = | +//| minimum 6-day wait for new D1 direction confirmation. | +//| Fix: lookback=2 → 4-day lag. Lower false swing risk while | +//| catching direction changes earlier. 30 CHoCH changes in | +//| backtest confirmed gate works correctly with 0 bad blocks. | +//| ============================================================ | +//| | +//| * FIX#SPREAD_INDEX (Mar 14 2026) -- US INDEX SPREAD 0 TRADES: | +//| ROOT CAUSE: US500.cash spread=56p blocked by limit=20p. | +//| US100.cash spread=190p blocked by limit=25p. | +//| GetPairTFConfig US500 spread[] 20→70p (FTMO avg=56p+margin). | +//| GetPairTFConfig US100/NAS100 spread[] 25→250p (avg=190p). | +//| InitSymbolProfile US INDEX block: auto-adjust 250p for US100.| +//| InitSymbolProfile US500/SPX block: auto-adjust 70p for US500.| +//| ApplyPairDetectionAdjustments Index: 30p→250p (covers both). | +//| sym== match: added US500.cash/.cash variants (US100.,US500.). | +//| Added USTEC, US30, DOW30, US100C, US500C, SPX500 variants. | +//| | +//| * FIX#CEIL_INDEX (Mar 14 2026) -- FIX#140 TP CEILING TOO TIGHT:| +//| ROOT CAUSE: US500 M15 SL=4700-7200p (5-8xATR, structure SL). | +//| Old M15 ceiling=3.0xATR → TP1≈2400p < SL=5000p → R:R=0.50 | +//| → FIX#140 REJECT on every TC trade (Score=138-151 rejected). | +//| Fix A: FIX#15h clamp — Index gets 3x wider ceiling per TF: | +//| M15: 3.0/4.0/5.5 → 10.0/13.0/16.0xATR for TP1/TP2/TP3. | +//| H4: 4.5/6.0/8.0 → 10.0/13.0/17.0xATR. | +//| Fix B: post-clamp _max_tp1x — Index M15: 5→15, H4: 9→18. | +//| | +//| * FIX#SCORE_INDEX (Mar 14 2026) -- SCORE 50/85 < MIN 60: | +//| ROOT CAUSE: MeetsMinimumEntryScore ignores AutoOpt | +//| min_entry_quality (cfg.min_conf=40 for Index from table). | +//| All FVG/OTE/BOS entries scored 50/85 → rejected (Min:60). | +//| Fix: Apply AutoOpt min_entry_quality as TF-aware cap | +//| (floor=36 safety). Index now passes at 40+ instead of 60. | +//| | +//| * FIX#SE_INDEX (Mar 14 2026) -- SMARTEXIT CLOSES AT 0.55R: | +//| ROOT CAUSE: STRUCTURE threshold (base×0.75=11pts). US500 OB | +//| proximity (10pts) + divergence (8pts) = 18pts ≥ 11 → closed | +//| at 0.55R before TP1. All 3 positions closed → 0 TP hits. | +//| Fix: Index pairs bypass STRUCTURE/MOMENTUM multiplier, | +//| use DEFAULT threshold (base×1.0=15pts). OB proximity alone | +//| (10pts) no longer sufficient to close Index trades early. | +//| | +//| * FIX#SL141_INDEX (Mar 14 2026) -- FIX#141 SL TOO WIDE REJECT: | +//| ROOT CAUSE: US100 M15 TC structure SL=28k-43kp, ATR=4.4-5.8k | +//| ratio=6-8×. Old max=2.8×ATR → ALL TC trades rejected (Score | +//| 138-158 killed). Designed for forex M15, not Index M15. | +//| Fix: Index pairs get 10×ATR limit on M15/M30 (vs forex 2.8×).| +//| Also M5=12×, H1=12× for Index. | +//| | +//| * FIX#MINSCORE_INDEX (Mar 14 2026) -- ALL REJECTED minScore=60: | +//| ROOT CAUSE: SelectBestCandidate _catFloor=60 for "Index" | +//| overrides pair profile min_conf=40. CANDIDATE DIAGNOSTIC: | +//| minScore=60 blocked scores 43-46 (valid setups). | +//| Fix: Index _catFloor: 60→40 (matches GetPairTFConfig | +//| min_conf=40). MeetsMinimumEntryScore already had this fix; | +//| SelectBestCandidate is the second code path that was missing. | +//| | +//| * FIX#P1a (Mar 14 2026) -- KELLY EQUITY COMPOUNDING CRASH: | +//| ROOT CAUSE: CalculatePositionSize used live ACCOUNT_EQUITY. | +//| After wins: $10k→$50k equity → Kelly lots=15.34 × 3 pos=46. | +//| OB trade SL hit → loss $9,182 → FTMO 4.5% daily DD trigger | +//| → EA removed itself at 7% of test interval. | +//| Fix: _refEquity = min(rawEquity, BacktestInitialBalance). | +//| Risk always calculated on initial capital, never compounded. | +//| Also fixed adjustedRiskAmount (same formula, same cap). | +//| | +//| * FIX#P1b (Mar 14 2026) -- FALLBACK LOT CALC SAME BUG: | +//| ROOT CAUSE: Two additional paths (fallback lots + AggRiskCap) | +//| also used ACCOUNT_BALANCE without initial balance cap. | +//| Fix: Both paths now cap balance at BacktestInitialBalance. | +//| | +//| * FIX#P3 (Mar 14 2026) -- GetPairTFConfig DIRECT CALL MISMATCH:| +//| ROOT CAUSE: ApplyPairTFProfile dot-strips "US500.cash"→"US500"| +//| but direct GetPairTFConfig() calls pass raw symbol. Without | +//| explicit .cash match → FALLBACK category="Unknown" → FIX#141 | +//| _isIdxSL=false → M15 2.8× limit applied → all TC rejected. | +//| Fix: Added US500CA, US100CA truncation variants explicitly. | +//| | +//| * FIX#55_M15_MTF (Mar 14 2026) -- FIX#55 CT BLOCK ON M15: | +//| ROOT CAUSE: FIX#176+225 MTF exemption only covered H4+. | +//| M15 US500/US100: Regime=WEAK_DOWN, MTF=STRONG_BULL → | +//| BUY Score=84-101 ALL blocked (BREAKER/OB/BOS/FVG). Hours of | +//| valid long setups lost while HTF was clearly bullish. | +//| Fix: M15/M30 CT-WEAK now exempt when MTF=STRONG_BULL/BEAR. | +//| Uses STRONG only (not plain BULL) — more conservative than | +//| H4 policy to avoid false reversal entries on lower TF. | +//| | +//| * FIX#DD_INTRABAR (Mar 14 2026) -- DD CHECK ONLY ON BAR OPEN: | +//| ROOT CAUSE: CheckDailyDrawdownLimit() called only in | +//| OnNewBar(). SL hit at 16:29:40, DD=30.15% detected at | +//| 16:45:00 (next bar) — 15 minutes too late. EA lost $7,493 | +//| in one trade before the check could fire. | +//| Fix: DD check on every tick after EA_ManagePositions(). | +//| Guard: only runs when positions are open (CPU efficient). | +//| | +//| * FIX#DD_STARTBAL (Mar 14 2026) -- DAILY START BALANCE WRONG: | +//| ROOT CAUSE: Daily reset stored ACCOUNT_BALANCE ($21,221) | +//| as start balance after wins. Limit = $21k×4.5% = $954, | +//| but actual FTMO limit is based on initial capital ($10k). | +//| 9.45 lots × SL = $7,493 loss = 74% of $10k → allowed! | +//| Fix: g_dailyStartBalance capped at BacktestInitialBalance | +//| (mirrors FIX#P1a logic for position sizing). | +//| | +//| * FIX#STATS_ZERO (Mar 14 2026) -- TOTAL TRADES = 0 AT DEINIT: | +//| ROOT CAUSE: FIX#23c group-close only called | +//| UpdateHistoricalPerformance() (win-rate arrays) — never | +//| incremented g_perfData.totalTrades. Final report showed | +//| "Total Trades: 0" despite 5+ entry groups closing. | +//| Fix: Both FIX#23c group-close AND ForceFlushAllEntryGroups | +//| now also increment g_perfData + g_backtestResults counters. | +//| ApplyPairTFProfile() — sl/tp/mrr arrays per pair | +//| GetPairTFMinRR() — DUPLICATE mrr table (always desynced)| +//| g_pairProfiles[] — H4-only third copy of same data | +//| Root cause of 63 R:R bugs in v10.01 audit: sync failures. | +//| NEW: PairTFConfig struct + GetPairTFConfig(sym) = single | +//| source of truth. ApplyPairTFProfile() and GetPairTFMinRR() | +//| are now thin wrappers that READ from the same struct. | +//| ValidatePairTFConfigs() runs at OnInit and prints any | +//| tp1/sl < mrr violations — impossible to silently desync. | +//| Saved: 283 lines. Eliminated: 2 duplicate tables. | +//| | +//| * v10.01 FIX#236 (Mar 13 2026) -- FVG SCANNER H4 TF SCALING: | +//| XAUUSD H4 backtest: FVG=0 on every bar despite OB=6. | +//| Root cause: AdaptParametersToTimeframe() applies H4 x2.0 to | +//| g_workingFVG_MinSize at startup. But ApplyAutoOptToWorkingVars | +//| (called on every UpdateAutoOpt cycle) resets g_workingFVG_Min | +//| Size = g_autoOptParams.fvg_min_size (flat, no TF factor) → | +//| H4 x2.0 multiplier lost after first AutoOpt recalculation. | +//| Fix: re-apply per-TF multiplier inside ApplyAutoOptToWorkingVars| +//| after the fvg_min_size copy: H1=1.5, H4=2.0, D1=3.0. | +//| | +//| * v10.01 FIX#235 (Mar 13 2026) -- STATS CARRY-OVER BETWEEN RUNS:| +//| MT5 backtest multi-run: each run's OnInit() did not ZeroMemory | +//| g_perfData / g_multiTPStats / g_backtestResults before loading | +//| persistence files → stats from previous run leaked into next. | +//| Symptom: run #2 banner showed run #1 trade counts and win rate.| +//| Fix: ZeroMemory on all three stats structs at OnInit() start, | +//| before LoadPerformanceData() call. | +//| | +//| * v10.01 FIX#234 (Mar 13 2026) -- D1 CHoCH NEUTRAL ON NO DATA: | +//| FIX#231 fallback used MTF direction as D1 proxy when D1 swing | +//| data unavailable. BUG: when MTF was BEARISH at backtest start, | +//| g_d1CHoCH_Bear=true → ALL BUY trades blocked for entire test. | +//| Root cause: absence of D1 data means UNCERTAINTY, not a | +//| confirmed BEARISH bias. MTF already filters direction elsewhere.| +//| Fix: when D1 swing unavailable, always set NEUTRAL (Valid=false,| +//| Bull=false, Bear=false) → D1 CHoCH gate inactive until real | +//| D1 structure forms. Both BUY and SELL allowed. | +//| | +//| * v10.01 FIX#239 (Mar 13 2026) -- GetPairTFMinRR D1 SYNC: | +//| GetPairTFMinRR had D1=2.00 for ALL pairs. ApplyPairTFProfile | +//| (Step 10) lowered mrr[] D1 to 1.60-1.90 in FIX#237, but | +//| GetPairTFMinRR → g_workingMinRiskReward (via ApplyPairTFProfile) | +//| still returned 2.00 → split-brain: profile allows D1 trade, | +//| minRR check blocks it. Also: XAUUSD H4=1.80 vs profile=1.30, | +//| XAGUSD H4=1.80 vs profile=1.40, GBPJPY ALL TFs mismatched. | +//| Fix: full sync of GetPairTFMinRR to match ApplyPairTFProfile. | +//| | +//| * v10.01 FIX#238 (Mar 13 2026) -- METAL TP BONUS LOST: | +//| ApplyPairDetectionAdjustments (Step 2.5) applied tp_rr x1.10 | +//| for Metal/Index/VolatileCross. ApplyPairTFProfile (Step 10) | +//| overwrote tp1_rr = MathMax(InpTP1_RR, derived) — ignoring | +//| x1.10. Fix: re-apply category multiplier inside Step 10 | +//| after deriving ratio from table, before MathMax guard. | +//| | +//| * v10.01 FIX#237 (Mar 13 2026) -- PAIR PROFILE R:R AUDIT: | +//| 63 pair×TF combinations had tp1/sl < mrr → 0 trades possible.| +//| Root cause: mrr[] values set empirically but tp1/sl tables | +//| not updated to guarantee tp1/sl ≥ mrr. | +//| Critical: GBPJPY ALL 5 TFs blocked. BTCUSD/ETHUSD ALL TFs. | +//| ALL pairs D1 blocked (mrr=2.00 unreachable everywhere). | +//| AUDUSD/NZDUSD/USDJPY/USDCAD M5+H4 blocked. | +//| Fix: for each pair, set tp1 so tp1/sl >= mrr, OR lower mrr | +//| to achievable level (D1: 1.60-1.90 depending on volatility). | +//| All 21 pair blocks in ApplyPairTFProfile corrected. | +//| | +//| * v10.00 FIX#233 (Mar 13 2026) -- ZERO-CANDIDATES HARD GATE: | +//| XAGUSD M5 backtest: 495 trades opened despite FVG/OB = 0 | +//| valid candidates. TC/BOS paths bypassed the count check. | +//| Fix: explicit guard after all scans — if g_candidateCount==0 | +//| log once per bar and return immediately. No silent pass. | +//| | +//| * v10.00 FIX#232 (Mar 13 2026) -- CT-WEAK BYPASS → SMARTENTRY: | +//| SelectBestCandidate (FIX#176+225) approved BUY in WEAK_DOWN | +//| (MTF=BULLISH). Then EvaluateSmartEntry ran INDEPENDENTLY and | +//| re-blocked: "Counter-trend BEAR (EV=0.18R < 0.20R)". | +//| Two gates conflict → valid H4 trades killed at second gate. | +//| Fix: g_fix176CTExempt global bool. SelectBestCandidate sets | +//| it true when FIX#225 approves. SmartEntry skips CT block | +//| when flag set. Resets each EA_CheckSignals() call. | +//| | +//| * v10.00 FIX#231 (Mar 13 2026) -- D1 CHoCH MTF FALLBACK: | +//| Backtest Feb 02-09: D1 gate inactive for 9 bars. SwingBars=5 | +//| too small → high=-1/low=-1 → "bias unclear" → gate never | +//| fires on new chart start or broker history gaps. | +//| Fix: when D1 swing fails AND g_d1CHoCH_Valid=false, use MTF | +//| overallDirection as proxy bias. Gate activates bar 0. | +//| | +//| * v10.00 FIX#230 (Mar 13 2026) -- BACKTEST STATS CARRY-OVER: | +//| Max Drawdown showed 0.00% at end of XAGUSD run because | +//| g_perfData.maxDrawdown was not recalculated from equity. | +//| Fix: RecalculatePerformanceStats() now uses g_currentTotalDD | +//| as floor for maxDrawdown in backtest mode. | +//| * v9.59 FIX#229 (Mar 13 2026) -- WIN RATE TRANCHE COUNTING: | +//| Dashboard "Win Rate: 2.4%" came from overallWinRate which | +//| counted each TP tranche as a separate trade (3 lots open = | +//| 3 closes = 3 trades in g_perfData). Fix: dashboard now shows | +//| grouped win rate from g_multiTPStats (group-level outcome). | +//| * v9.59 FIX#228 (Mar 13 2026) -- DD PROTECTION BLOWUP BUG: | +//| CRITICAL: After g_totalDDLimitReached=true, EA_ManagePositions | +//| still ran → open positions bled -115% (XAGUSD M5 run). | +//| Fix: EA_ManagePositions() returns immediately when any DD flag | +//| is set. Also: g_peakBalance now tracks MathMin(bal,equity) so | +//| floating losses are included in Total DD calculation. | +//| * v9.59 FIX#227 (Mar 13 2026) -- D1 CHoCH STATE NOT RESET: | +//| In backtest multi-run sessions, g_d1CHoCH_Bull/Bear/Valid and | +//| g_d1LastBarTime carried state from previous run → 0 trades on | +//| H4/H1 runs following a bearish D1 run. Fix: explicit reset | +//| of all D1 CHoCH globals in OnInit() for backtest mode. | +//| * v9.59 FIX#226 (Mar 13 2026) -- PEAK BALANCE EQUITY BUG: | +//| g_peakBalance updated only from AccountBalance (closed P&L). | +//| Open floating losses not reflected → Total DD underreported. | +//| Fix: peak tracked from MathMax(balance, equity) on each tick; | +//| Total DD uses MathMin(balance, equity) as currentLevel. | +//| * v9.58 FIX#224 (Mar 13 2026) -- H1 Inv H&S expiry 40→24 bars: | +//| 3 σημεία (Top detect, Inv detect, FIX#108 block check). | +//| H1 40h = πολύ μεγάλο — Inv H&S μπλοκάρει SELL για 40h. | +//| * v9.58 FIX#225 (Mar 13 2026) -- H4 CT-WEAK: STRONG→BULL+STR: | +//| Feb-Mar: 100% WEAK_DOWN, MTF 15% BULL (ποτέ STRONG) → 0 H4. | +//| MTF_BULLISH (netPct>0.10) πλέον αρκεί για H4 counter-trend. | +//| * v9.57 FIX#223 (Mar 13 2026) -- Block FVG + OB entry on M5: | +//| Backtest XAUUSD M5: FVG=0% WR, OB=0% WR (4 trades, 0 wins). | +//| M5 zones = noise vs XAUUSD spread. BREAKER/TC remain on M5. | +//| TF matrix: H4+=OFF | M5=OFF | H1/M15=ON (both FVG+OB). | +//| * v9.56 FIX#222 (Mar 13 2026) -- AutoOpt ξεκινά από | +//| EA_RiskPercent (όχι από pair profile 0.80%). Αποτέλεσμα: | +//| London tier A=1.50%, A+=2.00% ✓ | Asian/Dead=floor 1.0% ✓ | +//| [WARN] v7.9 → [INFO] (session penalty = expected behavior) | +//| * v9.55 FIX#221 (Mar 13 2026) -- EA_RiskPercent = MAX (απλό): | +//| Βάζεις ΕΝΑ νούμερο (EA_RiskPercent) = το MAX risk που θέλεις. | +//| Το EA κατεβάζει αυτόματα ανάλογα με ποιότητα: | +//| A+ (≥90) = 100% max | A (≥72) = 75% | B (≥55) = 55% | +//| C/D (<55) = 40% αλλά ποτέ κάτω από PosSize_MinRisk (1%). | +//| AutoOpt session/spread multipliers λειτουργούν ΚΑΤΩ από κάθε | +//| tier ceiling — ποτέ δεν ξεπερνούν το EA_RiskPercent. | +//| Αφαιρέθηκαν: PosSize_APlus_Mult/A_Mult/B_Mult/CD_Mult inputs. | +//| FULL EA CONVERSION - PRODUCTION READY | +//| | +//| * v9.52b FIX#218 (Mar 12 2026) -- CORRECTED XAUUSD/XAGUSD profiles | +//| after log analysis of 38-day M5 backtest revealed 4 bugs: | +//| BUG A: M5 spread=25p < min_real_spread=33p → 0 bars pass. | +//| BUG B: M5 mrr=1.50 but FIX#52 actual R:R=1.34 → 0 M5 trades.| +//| BUG C: M5 sl=2.00 conflicts with FIX#52 zone-based SL logic. | +//| BUG D: M15 spread=35p blocked 90% bars (avg=45p, min=33p). | +//| FIXES: M5 sl/mrr/spread restored to v9.51 working values. | +//| H4 mrr=1.30 (was 1.80) = MAIN fix, enables H4 trades.| +//| M15 sl=1.80, spread=50p, risk=0.80% (correct). | +//| H1/H4 risk=1.00% (was 0.75% hard-capped by profile). | +//| * v9.52 FIX#214-217 (Mar 12 2026) -- initial profile attempt | +//| (superseded by v9.52b after log verification) | +//| * v9.51 FIX#212 (Mar 12 2026) -- STALE g_ea_signal.score RESET: | +//| BOS_RETEST score=90 set in SelectBestCandidate, then SmartEntry| +//| rejected (EvaluateSmartEntry score=35). g_ea_signal.score | +//| stayed at 90 → FIX#211 fired "A+ LOT BOOST" every tick for | +//| 10 days with no open trade (log spam + wrong risk calc). | +//| Fix: reset g_ea_signal.score=0 on SmartEntry rejection. | +//| * v9.51 FIX#213 (Mar 12 2026) -- EURUSD H4 minRR 1.80→1.50: | +//| ROOT CAUSE: OB SL=2.20xATR + TP1=3.20xATR → R:R=1.455 < | +//| minRR=1.80 → ALL OB candidates rejected → 0 trades Feb-Mar. | +//| ICT: OB R:R 1.45+ valid on H4 when FVG absent. 1.50 = floor. | +//| * v9.50 FIX#209 (Mar 12 2026) -- TC WITH-TREND WEAK H4: | +//| ROOT CAUSE: EURUSD H4 WEAK UPTREND after Jan 20 SL hit → | +//| ALL TC BUY signals (score 70-75) blocked by threshold 130. | +//| Fix: with-trend TC in WEAK H4 gets threshold 80 (not 130). | +//| Counter-trend TC in WEAK H4 keeps 130 (unchanged, dangerous). | +//| * v9.50 FIX#210 (Mar 12 2026) -- A+ BYPASSES EA_RiskPercent CAP:| +//| FIX#205 capped risk at EA_RiskPercent=1.0% for ALL trades. | +//| A+ quality ceiling (EA_AggRiskCap_APlus=5%) was rendered | +//| useless because FIX#107 re-capped at 1.0% afterwards. | +//| Fix: A+/A trades (score>=72) skip EA_RiskPercent cap; | +//| only AutoOpt_MaxRiskOverride applies as absolute ceiling. | +//| * v9.50 FIX#211 (Mar 12 2026) -- QUALITY-TIER LOT BOOST: | +//| A+ (score>=90) → risk = PosSize_APlus_MaxRisk (max lots). | +//| A (score>=72) → risk = PosSize_A_MaxRisk (elevated lots). | +//| B/C/D → normal computed risk (unchanged). | +//| Both capped by AutoOpt_MaxRiskOverride (hard safety ceiling). | +//| * v9.49 FIX#197 (Mar 12 2026) -- CHOPPY ELITE PATH: | +//| CHOPPY blocked 80%+ of H1 bars → near-zero trades. | +//| New: high-quality OB setups allowed in CHOPPY when | +//| score>=72, OB strength>=0.72, MTF aligned, max 1/period. | +//| choppyEliteException bypasses isCounterTrend=true. | +//| * v9.49 FIX#198 (Mar 12 2026) -- D1 CHoCH GATE SOFTENED: | +//| EA_D1CHoCH_BlockNeutral true→false (was blocking all trades | +//| during D1 consolidation). SwingBars 10→5 (faster detection). | +//| * v9.49 FIX#199 (Mar 12 2026) -- RISK UNLOCK: | +//| AutoOpt_MaxRiskOverride 1.0→2.0 (hard cap blocked all dynamic | +//| boosts -- A+/A trades were capped at 1% same as B). | +//| Pair profiles updated: EURUSD/GBPUSD/USDCHF H4 risk 1.5→2.0, | +//| H1 risk 1.0→1.5. GBPUSD H4 tp1 3.20→3.80, mrr 1.80→1.60 | +//| (was rejecting ALL H4 candidates: R:R=1.45 < minRR=1.80). | +//| * v9.49 FIX#200 (Mar 12 2026) -- EXIT MANAGEMENT REBALANCE: | +//| TP distribution 35/35/30 → 25/25/50 (larger runner tranche). | +//| EA_BreakEven_RR 0.3→0.5R (was moving BE too early). | +//| SmartExitMinProfitR 0.3→0.5R (was closing at 0.3R = tiny). | +//| Trail_MinProfitPips 1.0→5.0 (minimum buffer before trail). | +//| * v9.49 FIX#201 (Mar 12 2026) -- TRAIL FALLBACK TF-AWARE: | +//| AutoOpt init window: trail fallback was EA_TrailStartRR=0.3R | +//| regardless of TF. H4: 0.55R, H1: 0.40R (from FIX#193). | +//| * v9.49 FIX#202 (Mar 12 2026) -- SL COOLDOWN TF-AWARE: | +//| Flat 6-bar cooldown = 24h on H4, 6h on H1. Now TF-aware: | +//| H4=2 bars (8h), H1=3 bars (3h), M15/M5=6 bars. | +//| * v9.49 FIX#203 (Mar 12 2026) -- FVG H1 RE-ENABLE: | +//| EnableFVG default true (off since FIX#194 H4 failure). | +//| TF gate added: H4+ FVG always disabled (FIX#194 confirmed). | +//| FVG_MinSizePips 2.0→3.0 (quality filter for H1). | +//| * v9.49 AutoOpt H1/H4 BE+SE floors aligned with new thresholds. | +//| | +//| * v9.48 FIX#192 (Mar 12 2026) -- REGIME FILTER CORRECTED: | +//| FIX#156 blocked TREND_UP/DOWN from Multi-TP, forcing binary | +//| single-pos on TC/OTE trades with 100-110p SL and no partial | +//| close buffer. TREND is exactly when TP2/TP3 are reachable. | +//| New: block Multi-TP ONLY for CHOPPY + VOLATILE. TREND_UP/DOWN | +//| and WEAK_TREND now use full 3-position structure. | +//| * v9.48 FIX#193 (Mar 12 2026) -- TRAIL GATE TF-AWARE H4: | +//| Zone A activation was 0.35xTP1 = 0.63R on H4, swept by normal | +//| 30-45p bar pullback. Zone A trail dist was 0.28xTP1 = 22p < | +//| bar pullback. Both now TF-aware: H4=0.55, D1=0.60, H1=0.40, | +//| M5/M15 unchanged. H4 gate now ~0.99R (safe from pullback). | +//| * v9.48 FIX#194 (Mar 12 2026) -- EnableFVG default OFF: | +//| 11 FVG trades in Dec25-Mar26 GBPUSD H4 backtest, majority SL. | +//| Ranging market fills FVGs as reversals. Default=false. | +//| * v9.48 FIX#195 (Mar 12 2026) -- FREEZE GUARD in SafeModify: | +//| SafePositionModify had no SYMBOL_TRADE_FREEZE_LEVEL check → | +//| hundreds of 10016 errors per trade, 6hr+ backtest runtime. | +//| Fix: 1.5x freeze level buffer check before every modify call. | +//| Also skip if new SL is not better than current SL. | +//| * v9.48b FIX#193b (Mar 12 2026) -- TRAIL GATE INIT FALLBACK: | +//| FIX#193 TF-aware trail fractions correct but guard was | +//| AutoOpt_Enabled && g_autoOptInitialized — during AutoOpt init | +//| window (first bars) g_autoOptInitialized=false → all 3 fracs | +//| fell to defaults (0.35/0.7/0.28) on H4, recreating the sweep | +//| problem. Fix: 3-tier logic: | +//| 1) AutoOpt ON + ready → tf_category switch (full auto) | +//| 2) AutoOpt ON + not ready → _Period fallback (safe TF values) | +//| 3) AutoOpt OFF → defaults (0.35/0.7/0.28, user manual input) | +//| Manual mode preserved exactly — no change for AutoOpt=false. | +//| * v9.48 VERSION SYNC: OnInit/OnDeinit banners updated to v9.48. | +//| +//| AddMultiTPEntry was called with posTicket=0 → ticket field | +//| in g_multiTPEntries stored 0. All per-ticket lookups in | +//| ProfitGuardTrail (FIX#184 Trail threshold) and SmartExit | +//| (FIX#190) fell through to global fallback (0.30R trail, | +//| 0.20R SE floor). Fix: pass g_ea_trade.ResultOrder() as real | +//| ticket; fallback scans positions by entry/dir/magic. | +//| * v9.47 FIX#191 (Mar 11 2026) -- UNIFIED TRAIL ACTIVATION GATE: |\n//| FIX#158 TP-proportional trail activated at 0.63R (35%×TP1), |\n//| ignoring perTrade_Trail_RR=0.99R from FIX#100. H4 Zone A |\n//| trail dist=22p < H4 bar pullback 30-45p → T03 closed at 0.95R |\n//| ($39) instead of TP1 ($87). All 3 trail systems (FIX#40/41/ |\n//| 158) now use single gate: MAX(perTrade×SL, 0.35×TP1). Zone A |\n//| floor ATR mult is TF-aware: H4=1.2×ATR, H1=0.9×, M15=0.7×. |\n//| * v9.46 FIX#190 (Mar 10 2026) -- SMARTEXIT FLOOR MIN→MAX: | +//| H4 SmartExit used MathMin(perTrade_SE_RR, 0.20) — this CAPS | +//| SE at 0.20R, ignoring FIX#100 per-trade threshold (0.72R for | +//| TP1=1.80R). Trade#5 Feb17: SE fired at RR=0.22 → -$21.87. | +//| Fix: MathMax(perTrade_SE_RR, 0.20) = per-trade is the real | +//| threshold, 0.20R is the execution-slippage floor only. | +//| * v9.46 FIX#186 (Mar 10 2026) -- CT WINPROB STRONG MTF: | +//| Counter-trend WP floor=58% blocked 18 valid setups Feb02-13. | +//| MTF=STRONG_BULL + Structure=BULL (both confirmed), but Regime | +//| lagged → WP=56.9% < 58%. When STRONG MTF AND Structure agree, | +//| regime is the outlier. Fix: relax WP floor to 52% only when | +//| g_mtfAnalysis.overallDirection==MTF_STRONG_* AND structure | +//| aligned — double confirmation required (ICT: HTF > regime). | +//| * v9.46 FIX#187 (Mar 10 2026) -- STRUCTURAL TP FOR OTE+BREAKER: | +//| FIX#179 FindNearestStructuralTP() was FVG/OB/TC only. OTE and | +//| BREAKER used ATR TP → OTE RR=1.29 (vs Alert 4.43), BREAKER | +//| RR=1.44 — both rejected by minRR=1.80. OTE target = opposing | +//| swing extreme (ICT: Fibonacci to prior swing). BREAKER target | +//| = next liquidity pool. Fix: call FindNearestStructuralTP for | +//| both; use structural TP if further than ATR-based TP. | +//| * v9.46 FIX#188 (Mar 10 2026) -- FVG SL=0 ATR FALLBACK: | +//| Tight FVG geometry (top≈bottom) → SL calculation returns 0 → | +//| noise reject fires on "SL=$0.00 < threshold=$0.00". Fix: | +//| before noise check, if |SL-entry|<5pts → replace with ATR*SL | +//| mult fallback. Recovers ~2 trades/month. | +//| | +//| * v9.43 FIX#185 (Mar 10 2026) -- TC ZONE INVALIDATION GUARD: | +//| TC zone = entry ± 0.4*ATR (hardcoded envelope, not a real ICT | +//| zone). On H4 (ATR=45p), a -0.11R = ~4p move can breach the | +//| envelope in bar 2, triggering ZoneInvalidation on pure noise. | +//| ROOT CAUSE: real TC invalidation = SL (swing structure, | +//| FIX#181). ATR envelope is just the entry trigger window. | +//| Fix: if zoneType="TC" AND H4+ AND |rr|<0.25 AND bars<3 → | +//| suppress invalidation. ZoneInvalidation still fires if price | +//| truly breaks far (|rr|>=0.25) and stays out for 3+ bars. | +//| * v9.43 FIX#184 (Mar 10 2026) -- TRAIL PER-TRADE THRESHOLD: | +//| ProfitGuardTrail always read global EA_Trail_Activation_RR=0.3 | +//| ignoring FIX#100 perTrade_Trail_RR stored in g_multiTPEntries. | +//| Two independent systems: FIX#100 computed 0.99R (tp1_R*0.55) | +//| at trade open; FIX#40 trail activated at 0.3R → moved SL to | +//| entry+2p → price retrace → closed at +$2 not +$95. | +//| Fix: ProfitGuardTrail reads perTrade_Trail_RR from registry, | +//| takes MathMax(global, perTrade) — same pattern as BE FIX#100. | +//| stageL1=effectiveTrailStart inherits the correct value. | +//| | +//| * v9.42 FIX#178 (Mar 10 2026) -- ZONE PIPELINE BUG FIX: | +//| AddCandidate() had self-assignment: g_pendingZoneTop = | +//| g_pendingZoneTop → data never written to cand.zoneTop. | +//| Result: entire ZoneInvalidation system (FIX#177) was no-op. | +//| Fix: cand.zoneTop/Bottom/Type = g_pendingZone* (correct copy). | +//| Also: g_ea_signal zone fields reset each EA_CheckSignals call. | +//| * v9.41 FIX#177 (Mar 10 2026) -- ZONE-LINKED EXIT SYSTEM: | +//| ROOT CAUSE: entry zone (FVG/OB/etc) was never stored after | +//| trade open → exit management blind to entry reason. | +//| Architecture: zone data now flows Scanner→Candidate→Signal→ | +//| MultiTPEntry and is monitored every bar for invalidation. | +//| Changes (4 layers): | +//| 1. CandidateSignal + EA_Signal + MultiTPEntry: added | +//| zoneTop, zoneBottom, zoneType, zoneInvalidated fields. | +//| 2. All 11 techniques: populate g_pendingZoneTop/Bottom/Type | +//| before AddCandidate() call; AddCandidate copies to cand. | +//| 3. CheckZoneInvalidation(): new function, runs every tick, | +//| closes trade immediately when last closed bar breaches zone. | +//| Guards: skip bar0, skip if RR≥0.5 (zone swept profitably). | +//| 4. SmartExit H4+: threshold 0.98R→0.15R (fires much earlier). | +//| AdverseClose H4+: bars guard removed (was 5 bars = 20hrs). | +//| FIX#177b (CHOPPY fix): added REGIME_CHOPPY + REGIME_VOLATILE | +//| to regimeCounterToTrade on H4+ so STRONG MTF can no longer | +//| bypass CHOPPY block (was causing Trade#8 Feb16 -$102 loss). | +//| * v9.40 FIX#176 (Mar 10 2026) -- H4+ DIRECTION FILTER: | +//| ROOT CAUSE of Feb H4 GBPUSD losses: STRONG MTF override | +//| bypassed ALL regime blocks (FIX#41 regime check, FIX#41c | +//| CHOPPY block, FIX-E H4 exempt) when MTF=STRONG_BULL/BEAR. | +//| On M5/M15: STRONG MTF = macro trend, regime=DOWN = pullback. | +//| On H4+: STRONG MTF = 3-4 day counter-rally, regime=WEAK_DOWN | +//| = 6-week macro trend. Override was wrong for H4+. | +//| Fix (2 locations): | +//| 1. EvaluateSmartEntry: mtfOverridesRegime=false on H4+ when | +//| regime direction opposes trade (both bull and bear). | +//| 2. SelectBestCandidate: FIX-E replaced -- H4+ CT-WEAK allowed | +//| only when MTF=STRONG agrees (not blanket exempt). | +//| Symmetric for both BULL and BEAR directions. | +//| FIX#169: ALL pattern types set strongestPatternTime | +//| MTB/Triangle/Flag/Wedge/Diamond/V now expire via | +//| FIX#108 stale check (was only H&S -- Double Bottom | +//| persisted 8+ days blocking ALL SELL signals) | +//| FIX#170: minRequired confirmations TF-aware | +//| D1:2 H4:3 H1+: pair profile value | +//| (Major was 4 for ALL TFs -- blocked H4 signals) | +//| FIX#171: FIX#165 log once per bar (was every 5s via OnTimer | +//| = 6383 prints/35 bars -- log flood + perf hit) | +//| FIX#172: GetPairTFMinRR() implemented (FIX#163 promised but | +//| never coded). Unified min_rr table 10 pair categories | +//| x 5 TF buckets. Replaces flat optimalRR. | +//| Eliminates FIX#19 pipeline overwriting. | +//| * v9.38 FIX#165-168 (Mar 10 2026) -- ROOT CAUSE FIXES: | +//| FIX#165: Confluence cascade cap (0.87->0.75 H4) + OB decouple | +//| FIX#166: H4+ intra-bar zone detection (missed mid-bar touches) | +//| FIX#167: FIX#56 TF-aware (H4: score+5, R:R x1.00 not x1.05) | +//| FIX#168: FIX#100 always log (removed EnableDebugMode gate) | +//| Manual mode (AutoOpt=false): EA inputs pass directly, NO profile override. | +//| AutoOpt mode: full profile control (SL/TP/minRR/risk/score/conf). | +//| Fixed: GetActive*(), SelectBestCandidate, BuildCandidateSLTP, | +//| lot sizing, dashboard, M5 score relaxation, TF scaling guards. | +//| * v9.37 FIX#163 (Mar 10 2026) -- UNIFIED min_rr SINGLE SOURCE OF TRUTH: | +//| New GetPairTFMinRR(sym,tf) -- one table for ALL pairs x ALL timeframes. | +//| Removed min_rr from ApplyTimeframeAdjustments (SCALP/SWING/POSITION). | +//| Manual path uses GetPairTFMinRR, not flat optimalRR. FIX#19 post-Step10. | +//| Fixed mrr M5 anomalies on 14 pairs (was > achievable tp1/sl ratio). | +//| * v9.36 FIX#160 (Mar 10 2026) -- DASHBOARD DISPLAY ACCURACY: | +//| FIX#160a: Entry Score Panel label "SIGNAL SCORE (chart display)". | +//| PASS/FAIL now uses TF-aware minScore cap (H4=45,H1=50,M5=48) | +//| mirroring SelectBestCandidate FIX-D logic (was EA_MinEntryScore=60). | +//| FIX#160b: "MTF H4:" label now dynamic per TF (M15->H1, H1->H4, etc.)| +//| FIX#160c: AutoOpt Panel MinScore shows both stored and effective | +//| values: "MinScore: X (eff: Y)" where Y = TF-aware cap. | +//| * v9.36 FIX#159 (Mar 10 2026) -- CRITICAL: BuildCandidateSLTP TP CLAMP:| +//| FIX#153 updated FIX#15h (post-SelectBestCandidate clamp on | +//| g_ea_signal) but NOT FIX#15g (BuildCandidateSLTP, runs BEFORE | +//| SelectBestCandidate). FIX#15g kept OLD tight values: | +//| H4: maxTP1=1.5xATR → cand.rr=1.5/2.0=0.75 < minRR=1.80 → REJECT! | +//| D1: maxTP1=1.2xATR → cand.rr=1.2/3.0=0.40 → impossible to pass. | +//| This was ROOT CAUSE of 0-1 trades in 35-day H4 backtest. | +//| Fix: sync FIX#15g to FIX#153 values: H4 1.5->4.5, D1 1.2->6.0, | +//| H1 2.0->3.5, M15 2.5/2.8/3.2 -> 3.0/4.0/5.5. | +//| EURUSD/GBPUSD/XAUUSD/US500/US100: all TFs corrected vs Excel table. | +//| sl_a M15/H1 were flat=1.2 (M5-level) for major pairs → now 1.8-2.2. | +//| risk M5 corrected 0.9-1.0%->0.5% for all pairs. mrr M5=1.4->1.1. | +//| US500/US100 spread ternary was REVERSED: (tf>=3)?20:30 → flat 20/25. | +//| XAUUSD/US100 D1 risk corrected 1.2-1.3%->2.0%. | +//| * v9.36 FIX#154 (Mar 10 2026) -- CRITICAL INPUT DEFAULTS FROM EXCEL: | +//| EA_StartHour 0->7, EA_EndHour 23->21, EA_FridayCloseHour 22->20. | +//| EA_MaxSpreadPips 50->30, STRUCT_SwingStrength 5->3, LIQ_SwingStrength 9->7. | +//| OB_ExpireOnMitigation false->true, SignalExpiryHours 24->8, Bars 24->16. | +//| AutoOpt_MaxSL_Mult 4.0->3.5. | +//| * v9.36 FIX#153 (Mar 10 2026) -- FIX#140/#15h TP CLAMP TF-CALIBRATION: | +//| FIX#140: ATR ceiling values corrected (H4 was 1.5x=M15-level). | +//| FIX#15h: H4 maxTP1x 1.5->4.5 (pair profile GBPUSD TP1=3.2xATR). | +//| D1 maxTP1x 1.2->6.0. H1 maxTP1x 2.0->3.5. | +//| Root: SL=2.2xATR, TP1 clamped to 1.5xATR → R:R=0.68 → REJECT = 0 trades | +//| * v9.36 FIX#152 (Mar 10 2026) -- AUTOOPT RECALC TF-AWARE (1 CANDLE): | +//| AutoOpt_RecalcMinutes default 15->1, formula x4->x1. | +//| Old: MathMax(15, tfMin*4) -> H4=960min(16h), D1=4days stale. | +//| New: MathMax(1, tfMin*1) -> M5=5m M15=15m H1=60m H4=240m D1=1440m | +//| * v9.36 FIX#150 (Mar 10 2026) -- AUTOOPT + PAIR PROFILE H4 CALIBRATION: | +//| ApplyPairTFProfile: H4 sl_atr corrected for all major pairs (was flat | +//| 1.2x for all TFs -- now EURUSD=2.0, GBPUSD=2.2, XAUUSD=2.2, US500=2.0,| +//| US100=2.2 at H4). min_rr H4 corrected 1.2->1.8 for EURUSD/GBPUSD/ | +//| US500/US100. risk% corrected for GBPUSD H4 (1.4->1.5%), XAUUSD H4 | +//| (0.8->0.75%), US500 H4 (1.1->1.0%), US100 H4 (0.9->0.75%). Spread | +//| H4 corrected: EURUSD 5->20p, GBPUSD 6->30p, XAUUSD 50->150p, | +//| US500 30->20p, US100 35->25p. TF_CAT_SWING: regime_lookback x4=72 -> | +//| 30 (Excel H4=30), struct_swing_strength min 5->6, fvg_min_strength | +//| 0.45->0.40, ob_volume_mult floor 1.6, tc_rsi_min 40->42, tc_rsi_max | +//| 60->58, comment "H1"->"H4". ApplyPairTFProfile: added per-pair fvg_ | +//| min_size_pips for H4 (XAUUSD=20p, GBPUSD=5p, EURUSD=4p, indices). | +//| * v9.36 FIX#146 (Mar 09 2026) -- HARD CAP: ALL PATHS ≤ MaxRiskOverride:| +//| PosSize A+ path set risk to PosSize_APlus_MaxRisk=5.0%, bypassing | +//| AutoOpt_MaxRiskOverride=2.0% entirely (only else-branch was capped).| +//| Fatal: A+ score + FVG SL 15p + 5% risk = catastrophic. | +//| Fix: enforce MaxRiskOverride INSIDE CalculatePositionSizing(), | +//| before lot calc. Also cap multCap to MaxRiskOverride/EA_RiskPercent.| +//| * v9.36 FIX#145 (Mar 09 2026) -- FVG/OB STRUCTURAL SL CAP AT SOURCE: | +//| FVG SL = bottom - 0.3xATR (structural, no cap). Wide FVG gap | +//| (e.g. 12p on ATR=4p) → SL=13.2p, all trail/BE unreachable. | +//| Fix layer 1: clamp SL dist ≤ 2.0×ATR inside CheckFVGEntrySignal. | +//| Fix layer 2: FIX#141 rejects at signal eval if still > 2.5×ATR. | +//| OB path receives same cap (OB_Array bottom/top same pattern). | +//| * v9.36 FIX#144 (Mar 09 2026) -- SMARTEXIT RSI CACHED NOT NEW: | +//| SmartExit_FIX41 created new iRSI handle every 5s per position. | +//| With 5 positions = 60 handles/min -> exhaustion in backtests. | +//| Fix: use g_cachedRSI (per-bar, zero overhead). | +//| * v9.36 FIX#143 (Mar 09 2026) -- FIX41 LOCK3 CACHED ATR + CAP: | +//| ProfitGuardTrail_FIX41 LOCK3 used live g_cachedATR. | +//| Fix: atr_at_entry from multiTP registry + cap at 50% SL_dist. | +//| * v9.36 FIX#142 (Mar 09 2026) -- TP PROPORTIONAL TO ACTUAL SL: | +//| BuildCandidateSLTP: TP1 = MathMax(ATR-based, sl_dist×MinRR). | +//| Old: TP1 = ATR×mult (independent of SL). SL=15p → TP1=15p → R:R<1| +//| New: TP1 always >= sl_dist×MinRR. Guarantees positive R:R. | +//| * v9.36 FIX#141 (Mar 09 2026) -- SL MAX WIDTH SCALP REJECT: | +//| M5: SL > 2.5xATR → REJECT. M15: SL > 2.8xATR → REJECT. | +//| Root: structure-based SL can be 3x ATR on M5 → trail/BE | +//| thresholds unreachable → full SL hit guaranteed. | +//| * v9.36 FIX#140 (Mar 09 2026) -- POST-CLAMP R:R VALIDATION: | +//| After FIX#15h TP clamp, recheck actual R:R (tp1/sl). | +//| If R:R < 1.0: try expand TP1 to 1.05R. If expansion exceeds | +//| ATR ceiling → REJECT. Root: R:R was checked pre-clamp (1.27), | +//| actual was 0.98 after clamp (ID=7: SL=15.2p TP=14.9p). | +//| * v9.36 FIX#139 (Mar 09 2026) -- TRAIL USES CACHED ATR: | +//| trail_dist = trailDistMult × atr_at_entry (NOT live ATR). | +//| Also: trail_dist capped at 0.5×sl_dist_cached so wide-SL | +//| trades (SL=15p) don't need 7.4p excursion before protecting. | +//| * v9.36 FIX#138 (Mar 09 2026) -- CACHE ATR AND SL_DIST AT ENTRY:| +//| MultiTPEntry.atr_at_entry: ATR at the moment of trade open. | +//| MultiTPEntry.sl_dist_cached: absolute SL distance at open. | +//| These are immutable -- never updated after entry. | +//| * v9.35 FIX#137 (Mar 09 2026) -- GBPUSD M5 SCALPING DEAD ZONE: | +//| 3 root causes blocked ALL trades for 5+ hours after NY open: | +//| (A) VOL_EXTREME: ATR percentile uses 100-bar lookback (8.3h on M5). | +//| NY spike inflates percentile -> VOL_EXTREME persists post-NY even | +//| when ATR drops to 3.3p. Old: allow_scalping=false for ALL TFs. | +//| Fix: For TF_CAT_SCALP, keep scalping ON + add +15 quality gate. | +//| Non-scalp TFs unchanged. | +//| (B) spread_ratio>2.0/3.0: avg_spread biased to Asian session (tight). | +//| Normal London/NY spread looks "extreme" vs Asian avg. | +//| Fix: For TF_CAT_SCALP, quality gate (+10/+15) instead of hard block.| +//| (C) FIX#73 TC threshold: Score=108 blocked in WEAK trend (threshold=110)| +//| 108 = 7+ confluences, only 2pts below threshold. Lowered to 105. | +//| (D) AutoOpt_MinScoreFloor: 50->40 (FVG/OB 46-52/85 all rejected in | +//| GBPUSD M5 test. 40/85=47% is still meaningful quality floor.) | +//| * v9.35 FIX#136 (Mar 08 2026) -- ML DEFAULT DISABLED: | +//| EnableML=true on H4: NN trains on limited H4 samples -> underfitted | +//| -> false high-score signals -> losses (confirmed in EA ANGEL history). | +//| Fix: EnableML default=false. Enable manually after sufficient training. | +//| * v9.35 FIX#135a (Mar 08 2026) -- PROFITLOCK_BE INSTANT SL: | +//| lockDist = MathMax(bufferDist=2pts, riskDist=150-300pts) = riskDist. | +//| be_sl = entry + riskDist = entry + 1R = current_price -> instant SL. | +//| H4 impact worse than M15: riskDist 5-10x larger -> SL always fires. | +//| Fix: lockDist = bufferDist (2pts). SL-> entry+2pts (BE+buffer). | +//| * v9.35 FIX#135b (Mar 08 2026) -- FIX#130 BYPASS LOG SPAM: | +//| blockingTooLong printed once per CANDIDATE per bar (same as FIX#128). | +//| Fix: static dedup; log once per bar-open per pattern name. | +//| * v9.35 FIX#134 (Mar 08 2026) -- COUNTER-TREND FILTER OVERHAUL: | +//| Old: minEVForCounter=0.45R(Forex)/0.60R(Index). Log: OTE | +//| Score=94 A_PLUS, EV=0.12R blocked hourly → ZERO CT trades. | +//| Root: M15 typical EV range 0.08-0.25R; 0.45R is unachievable. | +//| Fix: minEVForCounter=0.20R(Forex)/0.35R(Index). | +//| WinP floors: 62→55% / 65→58% / 68→62% (EV-adaptive tiers). | +//| CT trades now allowed when genuinely high Score+EV+WP aligned. | +//| * v9.35 FIX#133 (Mar 08 2026) -- PER-CATEGORY MinEV GATE: | +//| Old: flat 0.25R for ALL pairs. Log: 40+/day A_PLUS rejections. | +//| Root: EV formula yields 0.08-0.18R on M15 (WinP 55-58%); | +//| 0.25R unachievable in ranging/volatile conditions. | +//| Fix: Major=0.08R | Metal=0.10R | Index=0.15R | VolatileCross= | +//| 0.10R | Energy=0.12R | Exotic/Crypto=0.15R | Cross/Unknown=0.08R| +//| Each category's spread/volatility profile justifies its floor. | +//| * v9.35 FIX#132 (Mar 08 2026) -- REGIME THROTTLE PER-REGIME CTR: | +//| Old: throttle compared g_ea_stats.trades (DAILY total) vs max. | +//| Bug: 2 trades in TREND → regime→CHOPPY → daily=2≥maxChoppy=2 | +//| → ZERO CHOPPY trades even if none taken while CHOPPY. | +//| Fix: new g_tradesThisRegimePeriod counter; resets on regime | +//| change or day rollover. Throttle now counts only trades taken | +//| WHILE the current regime was active. CHOPPY cap 2→3 (now fair). | +//| * v9.31 FIX#121 (Mar 06 2026) -- XAUUSD M15 R:R TUNING: | +//| FIX#121A: XAUUSD M15 tp1 2.3->2.8 | mrr 1.8->1.2 (no FIX#19 spam)| +//| Root: tp1/sl=1.64, max_min_rr=1.40 << min_rr=1.8 → 352x clamp. | +//| After: tp1/sl=2.0, max_min_rr=1.70 > 1.2 → no clamping. | +//| FIX#121B: SmartExit min 0.40R→0.55R (exits need more confirmation) | +//| FIX#121C: BreakEven fires at 0.65R (was 0.50R, caused whipsaw BE) | +//| * v9.31 FIX#120 (Mar 06 2026) -- LOG SPAM ELIMINATION: | +//| FIX#120A: FIX#107 risk prints only when risk% changes (not/bar). | +//| FIX#120B: FIX#19 min_rr CLAMPED prints only when RR value changes.| +//| FIX#120C: PairTFProfile prints only on symbol/TF/param change. | +//| FIX#120D: AUTOOPT RISK OVERRIDE: added ShowLog guard + val-change.| +//| Result: 20,000+ spam lines/run → <20 informational logs/run. | +//| * v9.31 FIX#119 (Mar 05 2026) -- MULTI-PAIR SCORING FIXES: | +//| FIX#119A: FVG size threshold now ATR-relative (not absolute pips). | +//| Old: EURUSD 3-pip FVG=+5pts, XAUUSD 300-pip FVG=+25pts (unfair).| +//| New: FVG/ATR ratio >= 20% = +25, >= 10% = +15 (same for all). | +//| FIX#119B: g_workingMaxSpreadPips bidirectional sync from autoOpt. | +//| Old: only increased, never decreased -> wrong spread for EURUSD. | +//| FIX#119C: minEntryScore pair-category floor (Major=55, Cross=52). | +//| EA_MinEntryScore still respected as ceiling, never exceeded. | +//| FIX#119D: Immediate spread sync in ApplyPairTFProfile (not delayed)| +//| Old: first 60min used OnInit spread -> wrong filter for all pairs.| +//| * v9.32 FIX#124 (Mar 06 2026) -- SYMBOL STRIPPING BUG: | +//| ApplyPairTFProfile: StringSubstr(0,6) on "US500.cash"->"US500." | +//| -> never matched table -> logged "unknown" EVERY BAR (spam!). | +//| Fix: strip at first dot, then cap to 6 chars. | +//| * v9.32 FIX#124b (Mar 06 2026) -- UNKNOWN BLOCK CRASH & SPAM: | +//| Old: logged every bar + returned leaving wrong params active. | +//| New: log ONCE, apply category-based fallback profile from | +//| FIX#118 (INDEX/CRYPTO/ENERGY/Major) and fall through to apply. | +//| * v9.32 FIX#124c (Mar 06 2026) -- MISSING NATGAS/OIL PROFILES: | +//| Added NATGAS.cash, USOIL, BRENT etc. to pair table. | +//| * v9.32 FIX#125 (Mar 06 2026) -- H&S STALE SPAM (3000+ lines/min):| +//| DetectHeadAndShoulders O(n^3) loop printed per-COMBINATION not | +//| per-headTime -> 20+ identical lines per bar per stale pattern. | +//| Fix: static dedup array; log each headTime once, then suppress. | +//| * v9.32 FIX#126 (Mar 06 2026) -- TRENDLINE ANGLE SATURATION: | +//| * v9.34 FIX#130 (Mar 08 2026) -- FIX#127 REGRESSION / PERMANENT SELL BLOCK: | +//| Inv H&S score=90 + FIX#127 highScoreOverride => FIX#99a block NEVER | +//| expired. New Inv H&S detected every ~10h (FIX#34 window) -> headTime | +//| resets -> patternIsStale always false -> ALL SELLs blocked 4 weeks. | +//| Fix: track when blocking STARTED (not pattern headTime). If same pattern | +//| name has been blocking for >72h continuously, stop blocking even at score=90.| +//| * v9.34 FIX#131 (Mar 08 2026) -- PHANTOM MULTI-TP REGISTRY ENTRIES: | +//| AddMultiTPEntry() was called in 5 signal-detection paths (FVG/OB/LIQ/OTE/ | +//| BOS) BEFORE SmartEntry/FIX#99a evaluation. Every rejected signal created | +//| a ghost registry entry (290 entries for 25 real trades, 12:1 ratio). | +//| Fix: removed all 5 early calls; ExecuteTrade path (line 91019) is correct. | +//| * v9.34 FIX#128 (Mar 08 2026) -- FIX#99a PATTERN BLOCK LOG SPAM: | +//| Same issue as FIX#125: fired on every bar per candidate per pattern. | +//| Fix: static dedup; log once per opposing-pattern-name per bar-open. | +//| * v9.33 FIX#127 (Mar 06 2026) -- OPPOSING PATTERN HIGH-SCORE BLOCK: | +//| Score>=85 opposing patterns block entry even when FIX#108 marks | +//| them stale. Inv H&S Score=90 was bypassed -> SELL against structure | +//| * v9.33 FIX#129 (Mar 06 2026) -- TRAILING STOP RR CALCULATION: | +//| ProfitGuardTrail_FIX41 used current(trailed) SL for RR. After BE+2p | +//| fires, SL dist=2p, rr=9.35 -> instant LOCK3 -> cascaded to loss. | +//| Fix: use originalSL from multiTP registry (same as SmartExit_FIX41) | +//| atan(slope/pipValue) -> ~89° for all index/commodity symbols | +//| (US500 price>>pipValue -> argument >> 1 -> atan saturates). | +//| Fix: normalize by ATR (45° = 1 ATR over 14 bars). Symbol-agnostic.| +//| * v9.31 FIX#118 (Mar 05 2026) -- MISSING SYMBOL PROFILES: | +//| AUDJPY/USDCHF/GBPJPY/US500 triggered UNKNOWN->return->0trades | +//| FIX: Added all 4 to detection chain + safe defaults fallback. | +//| * v9.31 FIX#117 (Mar 05 2026) -- OPT TIMEFRAME DEFAULT: | +//| Opt_Timeframe default was PERIOD_CURRENT -> optimizer ran D1. | +//| FIX: Changed default to PERIOD_M15 (XAUUSD-focused). | +//| * v9.31 FIX#116 (Mar 05 2026) -- TRADE COUNTER ZERO AT DEINIT: | +//| Entry groups not flushed at deinit -> trades=0 in logs/tester. | +//| FIX: ForceFlushAllEntryGroups() at OnDeinit + OnTester(). | +//| * v9.31 FIX#115 (Mar 05 2026) -- SAFEPOSITIONMODIFY LOOP: | +//| SL==TP->ValidateSLTP fails->BLOCKED printed 92733x->7hr pass. | +//| FIX A: SL==TP early exit in ValidateSLTP (silent). | +//| FIX B: Rate-limit BLOCKED log: max 1 per 60s per context. | +//| * v9.31 FIX#114 (Mar 05 2026) -- SMART PROFILE LOADER: | +//| After optimization, best params per pair/TF are auto-saved | +//| to CSV (ICT_Data/OptProfiles.csv) via OnTesterPass(). | +//| On live/backtest start, LoadOptimizedProfiles() reads CSV and | +//| overrides AutoOpt params: risk, SL/TP, strategy weights, | +//| session filters, entry score -- all per pair+TF combination. | +//| Result: EA "knows" from history which setup works per pair. | +//| Also works standalone: edit CSV by hand to force any param. | +//| * v9.31 FIX#113 (Mar 05 2026) -- MULTI-TF OPTIMIZER (OnTester): | +//| Missing: OnTester() -> MT5 used Balance as criterion (wrong). | +//| Missing: Opt_Timeframe input -> optimizer ran Daily only. | +//| FIX A: OnTester() returns composite score: | +//| PF x Sharpe x RecovFactor, penalizes <5 trades & high DD. | +//| FIX B: input Opt_Timeframe (M5/M15/H1/H4/D1) -- when set, | +//| OnInit rejects passes where _Period != Opt_Timeframe via | +//| INIT_PARAMETERS_INCORRECT (pass skipped by MT5 optimizer). | +//| HOW TO USE: Set chart TF=M5 in Tester. Set Opt_Timeframe | +//| range M5->D1 step 1. Optimizer tests all 5 TFs per symbol. | +//| * v9.31 FIX#112 (Mar 04 2026) -- EARLY-CLOSE CASCADE FIX: | +//| Bug: TP1 tranche closed via trail at 1.17489 (profit). | +//| tp1Price=1.17582 never hit → OnTP1Hit() never called. | +//| tp1Hit=FALSE → cascade SL never fired for TP2/TP3. | +//| TP2(#5)+TP3(#6) kept original SL=1.17358 for 46min → -$143. | +//| Fix: Deal scan detects _TP1 close at profit → sets tp1Hit=true | +//| → cascade SL fires on next ManagePositions tick. | +//| * v9.31 FIX#111 (Mar 04 2026) -- SMARTEXIT SCALP 2/3 FIX: | +//| T3: SmartExit 2/3 (Momentum+MTF) at +0.72R → needed 3/3. | +//| Didn't close → SL hit → -$243 instead of +$122 (+$365 miss). | +//| Fix: Scalp TF + 2 cats + RR>0.4R + price_action+context → | +//| close. hasPriceAction(Momentum/Candle) AND hasCtx(MTF/Regime). | +//| * v9.31 FIX#110 (Mar 04 2026) -- EVENT-TRIGGERED AUTOOPT RECALC:| +//| AutoOpt was TIME-only (4 candles). On M5 missed London/NY | +//| opens where ATR 2-3x in minutes. On H4/D1 missed new day/week.| +//| FIX A: Force recalc on session transition (M5/M15/H1). | +//| FIX B: Force recalc on new day (H4), new week Monday (D1). | +//| FIX C: Force recalc on ATR spike (TF-scaled: M5=+35%,D1=+80%)| +//| FIX D: Per-tranche trail uses g_workingFIX41_TrailStart | +//| (AutoOpt TF-aware) instead of fixed EA_Trail_Activation_RR. | +//| Cooldown: min 1 candle between forced recalcs (no churn). | +//| * v9.31 FIX#109 (Mar 04 2026) -- TC QUIET MARKET REJECT FIX: | +//| TC on quiet bars (ATR=1.3p): structural SL = 1.6p < 3.0p | +//| pair profile minimum. Old code widened SL artificially -> | +//| broke R:R and market structure. IDs 109/115/137/218: -$573. | +//| FIX: REJECT setup if structural SL < sl_min_pips (continue). | +//| Market has no room = invalid setup, not a sizing problem. | +//| * v9.31 FIX#108 (Mar 04 2026) -- SCORE-BASED LOT CEILING FIX: | +//| Fallback lot calc used EA_RiskPercent as ceiling for ALL | +//| scores -> A+ boost (x1.40) was killed by 3% cap (asymmetric).| +//| A+ trade: 3%x1.40=4.2% -> was capped to 3% -> 0 reward. | +//| D trade: 3%x0.80=2.4% -> penalty worked fine. | +//| FIX: ceiling now score-aware, matching PosSize/Kelly path: | +//| A+(>=90): EA_AggRiskCap_APlus (5%) | A(>=72): EA_AggRiskCap_A| +//| B/C/D: EA_RiskPercent (original safe behavior unchanged). | +//| * v9.31 FIX#107 (Mar 04 2026) -- AUTOOPT RISK OVERRIDE FIX: | +//| ROOT CAUSE: ApplyPairTFProfile capped risk_pct via MathMin | +//| (EA_RiskPercent=3%, profile=1%) = 1% ALWAYS. AutoOpt_Max | +//| RiskOverride ήταν ceiling πάνω από ήδη χαμηλή τιμή = useless.| +//| FIX: AutoOpt_MaxRiskOverride = ΑΜΕΣΗ ΕΝΤΟΛΗ προς AutoOpt. | +//| Override>0 -> χρησιμοποιεί ΑΥΤΗ την τιμή, profile αγνοείται.| +//| Override=3.0 -> 3.0% (full risk, ανεξάρτητα από profile). | +//| Override=2.0 -> 2.0% (μέτριο, profile αγνοείται). | +//| Override=0 -> profile table τιμή (1.0% EURUSD M5, safe). | +//| * v9.30 FIX#100-101 (Mar 04 2026) -- DYNAMIC PER-TRADE SYSTEM: | +//| FIX#100: Per-trade BE/Trail/SE thresholds from TP1/SL ratio | +//| Formula: BE=TP1_Rx0.50 Trail=TP1_Rx0.55 SE=TP1_Rx0.40 | +//| Stored in MultiTPEntry at trade open -> no manual tuning | +//| FIX#101: AutoOpt TF sections use same formula (all 5 TFs) | +//| Replaces 8 hardcoded MathMax floors (1.2/1.3/1.5/2.5R) | +//| Works for ALL pairs: EURUSD/XAUUSD/GBPUSD/USDJPY/US100 | +//| Works for ALL TFs: M5/M15/M30/H1/H4/D1 automatically | +//| * v9.29 FIX#99 (Mar 04 2026) -- PATTERN CONFLICT BLOCK: | +//| 4/4 losses had opposing patterns at entry (not caught by EA) | +//| ID=4: Confirmed Bullish Divergence vs SELL -> now DIV BLOCK | +//| ID=7: Inv H&S score=90 vs SELL -> name missing from check | +//| FIX#99a: Added "Inv H&S","V Bottom","Asc/Desc Triangle" names | +//| FIX#99b: New CONFIRMED DIVERGENCE BLOCK (<=3 bars, confirmed) | +//| FIX#98b: BE/Trail/SmartExit -> 0.3R (H4 max favorable ~0.3R) | +//| FIX#98c/e: AutoOpt SWING no longer overrides above user input | +//| NOTE: TP1 clamp kept at 1.5xATR -- needs tick backtest first | +//| * v9.29 FIX#98 (Mar 04 2026) -- H4 BE/TRAIL/SMARTEXIT DEAD: | +//| ROOT CAUSE: TP CLAMP at 1.5xATR on H4 gave TP1=0.80R (BELOW | +//| 1R). All thresholds (BE=1.8R, SmartExit=1.1R, Trail=2.0R) | +//| required MORE than TP1 -> NEVER activated in 4 losing trades. | +//| FIX#98a: H4 TP CLAMP 1.5/1.8/2.1 -> 2.5/3.5/4.5 xATR | +//| FIX#98b: Inputs BE=0.7R SmartExit=0.6R Trail=0.8R | +//| FIX#98c: AutoOpt SWING MathMax->MathMin (was RAISING thresholds)| +//| FIX#98d: Secondary tp1Mult H4 cap 1.5->2.5 | +//| * v9.28 FIX#97 (Mar 04 2026) -- PROXIMITY CLOSE DIRECTION BUG: | +//| SELL @ 1.16224, price went UP to 1.16519 (AGAINST trade). | +//| System measured |29.5p| = 99.3% -> force closed for -$339. | +//| Fix: triple direction guard -- price must be between entry | +//| and TP (not between entry and SL) before proximity triggers. | +//| Also: directedProgress replaces profit_distance in ratio. | +//| * v9.27 FIX#96 (Mar 03 2026) -- SCORE-BASED LOT UNCAPPING: | +//| 7 caps replaced with score-aware tiers: | +//| A+(>=90) win streak -> multiplier up to 5.0x, risk up to 5% | +//| A (>=72) win streak -> 3.5x, risk up to 4% | +//| B/C/D -> original 2.0x, risk 2.5% (safe for all conditions) | +//| AutoOpt_MaxRiskOverride=2% now applies to B/C only. | +//| EA_AggRiskCap_APlus/A/Default = per-tier aggregate caps. | +//| PosSize_WinStreak_Uncap=true enables full compounding. | +//| * v9.26 FIX#91 (Mar 03 2026) -- REAL SL IN KELLY CALC: | +//| CalculatePositionSize used ATRx1.5 as SL proxy -> error +/-30%. | +//| Fix: pass actual MathAbs(entry-sl)/_Point -- exact per trade. | +//| * v9.26 FIX#92 (Mar 03 2026) -- ADAPTIVE SIZE SCORE TIERS: | +//| GetAdaptivePositionMultiplier thresholds 50/47/43/38/36 | +//| calibrated for old max=85 -- virtually ALL trades hit tier-1. | +//| Fix: aligned to QUALITY grades: 90/72/58/44/36 (0-100 range). | +//| * v9.26 FIX#93 (Mar 03 2026) -- PAIR-SPECIFIC POS SIZING: | +//| pos_trending_bonus/ranging_penalty fixed at 1.2/0.8 for ALL. | +//| Fix: per-category (XAUUSD trend=x1.40, GBPJPY range=x0.55). | +//| * v9.26 FIX#94 (Mar 03 2026) -- TF-SCALED POS SIZING (5 TFs): | +//| Each TF case (SCALP/INTRADAY/INTRASWING/SWING/POSITION) now | +//| multiplies pair base bonus/penalty by TF-appropriate scale. | +//| SCALP: -10% bonus / +25% penalty. SWING: +20% / -5%. | +//| * v9.26 FIX#95 (Mar 03 2026) -- SESSION+DOW POS SIZING: | +//| London Open/NY Overlap/Power Hour: trend bonus up per pair. | +//| Asian/DeadZone/FridayPM: heavy lot cuts, range penalty max. | +//| Monday AM: reduced sizing until London opens. | +//| * v9.25 FIX#85 (Mar 03 2026) -- FVG/OB SCORING PARITY: | +//| FVG base 15->29, OB base 18+(strx8)->24+(strx10). | +//| FVG/OB lose self-confirmation (-14pts). BREAKER kept both. | +//| Result: FVG/OB now compete on equal footing with BREAKER. | +//| * v9.25 FIX#86 (Mar 03 2026) -- SILENT CORRELATION SYMBOL CHECK: | +//| US500/US100 removed from default Corr_Pairs (FTMO not avail). | +//| CalculatePairCorrelation: silent SYMBOL_DIGITS pre-check | +//| eliminates 2000+ "symbol does not exist" errors per session. | +//| * v9.25 FIX#87 (Mar 03 2026) -- H4 MAJOR VOL_VERY_LOW EXEMPT: | +//| EURUSD H4 ATR 17-22 pips = normal, not "dead market". | +//| H4+ Major/Cross skips VOL_VERY_LOW 0.85x risk penalty. | +//| * v9.25 FIX#88 (Mar 03 2026) -- SWING QUALITY THRESHOLD PARITY: | +//| Major/Cross H4: min_entry_quality +4 (not +8). With only | +//| 12-18 bars/day on H4, +8 was blocking too many valid setups. | +//| * v9.25 FIX#89 (Mar 03 2026) -- H4 MAJOR RISK FLOOR 80%: | +//| After VOLxsession multipliers, H4 Major was at 0.65%. | +//| New floor = max(MinRiskOverride, EA_RiskPercent x 80%). | +//| * v9.25 FIX#90 (Mar 03 2026) -- DASHBOARD PF LABEL: | +//| Renamed "Profit Factor" -> "PF(R-tranche)" in dashboard. | +//| Backtest report PF = money-based, dashboard = R-tranche. | +//| Prevents confusion between 0.61 (backtest) vs 1.18 (dash). | +//| * v9.24 BUG#4 FIX (Mar 03 2026) - FIX#73 BREAKOUT UNCONFIRMED: | +//| TC in BREAKOUT needs barsInRegime>=2 (30min M15). | +//| Filters false breakouts (40-50% of BREAKOUT bar 0). | +//| * v9.24 BUG#3 FIX (Mar 03 2026) - FIX#76 FALLBACK SL WRONG: | +//| BE SL=entry+2p->slDist=2p->rr=huge->Zone A skip (no protect).| +//| Fix: SL<5p from entry -> g_cachedATR as original-risk proxy. | +//| * v9.24 BUG#2 FIX (Mar 03 2026) - FIX#76 NO MIN-AGE GUARD: | +//| Trade 2 bars old closed at -0.15R. Fix: Zone B min 3 bars, | +//| Zone C min 5 bars before AdverseClose fires. | +//| * v9.24 BUG#1 FIX (Mar 03 2026) - FIX#77 iRSI PER-TICK LEAK: | +//| iRSI() every 5s x N positions = resource leak + slowdown. | +//| Fix: g_cachedRSI (per-bar). Decision+log now consistent. | +//| * v9.24 FIX#82 (Mar 03 2026) -- COMPLETE AUTOOPT COVERAGE: | +//| TC RSI/Slope/Score, TP R:Rs, Judas/TBS TPs, CRT ranges, | +//| MTF confidence, Regime thresholds, VSA strength, WP threshold | +//| All: user input = floor, AutoOpt = TF+pair+vol scaling | +//| * v9.24 FIX#81 (Mar 03 2026) -- AUTOOPT 4 ΔΟΜΙΚΑ BUGS: | +//| #81a: ob_volume_mult -- Step6 πλέον ΠΟΛΛΑΠΛΑΣΙΑΖΕΙ pair base | +//| #81b: M30 vs M15 διαφοροποίηση (slx1.05 tpx1.08 BE=1.3R) | +//| #81c: max_spread_pips -- ATR calc δεν μπορεί να μικρύνει pair floor| +//| #81d: per-bar re-sync block πλέον συμπεριλαμβάνει g_workingBE_RR | +//| * v9.24 FIX#81 (Mar 03 2026) -- AUTOOPT ΑΡΧΙΤΕΚΤΟΝΙΚΟ AUDIT: | +//| FIX-A: SCALP breakeven MathMax(user,1.2) (ήταν hardcoded) | +//| FIX-B: M15 breakeven floor MathMax(user,1.2) (δεν υπήρχε) | +//| FIX-C: Vol HIGH/EXTREME: BE/SmartExit +12-35% (ήταν αναλλοίωτο) | +//| ΑΡΧΗ: user input = floor, AutoOpt μόνο ΦΑΡΔΑΙΝΕΙ | +//| * v9.24 FIX#80 (Mar 03 2026) -- AUTOOPT->FIX41 BRIDGE: | +//| FIX41 trail/SmartExit τώρα χρησιμοποιούν AutoOpt pair/TF/vol αποφάσεις | +//| g_workingFIX41_TrailStart/SmartExitRR/ScoreThresh | +//| AutoOpt OFF: direct inputs (καμία αλλαγή στη συμπεριφορά) | +//| * v9.24 FIX#79 (Mar 03 2026) -- MULTITP BE SYNC: | +//| FIX#41 active -> MultiTP δεν κινεί SL (tracking only). Ένας master. | +//| * v9.24 FIX#78 (Mar 03 2026) -- STRATEGY-AWARE SMARTEXIT: | +//| Structure(FVG/OB/BOS): thresholdx0.75. Momentum(BREAKER/TC): x1.5 | +//| BREAKER 5W/0L προστατεύεται. OB/FVG βγαίνει γρηγορότερα. | +//| * v9.24 FIX#77 (Mar 03 2026) -- CONTEXT-AWARE TRAIL: | +//| BE+2p απαιτεί RSI+structure confirm. Hard cap 1.8R. | +//| Τυχαία spikes δεν πυροδοτούν BE. Πραγματικές κινήσεις δουλεύουν. | +//| * v9.24 FIX#76 (Mar 03 2026) -- SMARTEXIT ADVERSE CLOSE: | +//| SmartExit ενεργεί και σε ζημιά/0 όταν regime+MTF+structure | +//| αντιστραφούν τη θέση. Zone-B(>=0): score>=15 -> BE close. | +//| Zone-C(>-0.3R): score>=23 -> cut now. -1R -> 0/-0.3R. | +//| [FIX] v9.24 FIX#75 (Mar 03 2026) -- BREAKER/FVG MORE SETUPS: | +//| BREAKER minOBStr 0.65->0.55, FVG_MinStrength 0.35->0.30 | +//| Feb 2026: BREAKER 5W/0L (+$263), FVG 2W/0L (+$120) | +//| [FIX] v9.24 FIX#74 (Mar 03 2026) -- OB SL BUFFER TOO TIGHT: | +//| OB slBuffer 0.25xATR->0.50xATR (noise stops reduced) | +//| Feb 2026: OB 2W/4L (-$317) due to 6-7p SL = 1-2p wicks | +//| [FIX] v9.24 FIX#73 (Mar 03 2026) -- TC IN TRENDING LOSES (CRITICAL): | +//| TC now ONLY in STRONG_TREND/BREAKOUT (replaces FIX#51) | +//| Feb 2026: TC in TRENDING = 5W/5L -$226. Score 163 = useless | +//| [FIX] v9.24 FIX#71 (Mar 03 2026) -- TRAIL FIRES TOO EARLY (CRITICAL):| +//| FIX#71: EA_Trail_Activation_RR was 0.5 -> BE+2p stage fired at 0.5R| +//| Any 3-4p retracement = closed at entry+2p ($14-$32). | +//| 13/19 wins = avg $22 instead of $95. Payoff ratio 0.45. | +//| Fix: stages now use EA_Trail_Activation_RR(1.2)/Lock1/2 | +//| BE+2p only at [1.2R,1.5R), LOCK1 at [1.5R,2.5R), etc. | +//| EA_Trail_Activation_RR default: 0.5 -> 1.2 | +//| [FIX] v9.24 FIX#72 (Mar 03 2026) -- BOS_RETEST R:R FILTER TOO STRICT:| +//| BOS_RETEST with Score>=80 and rr>=1.3 now allowed | +//| (threshold reduced by 0.5R for high-quality structure) | +//| [FIX] v9.24 FIX#70 (Mar 03 2026) -- CRITICAL SmartExit RR BUG: | +//| FIX#70: SmartExit used current_sl (after BE move) for RR. | +//| After BE: sl_distance=2p -> rr=5.8p/2p=2.90 (inflated). | +//| Threshold 1.1R triggered at 0.46R real profit -> closed | +//| trades massively early. Trade #2: closed $46 vs $144 TP. | +//| Trade #6: closed $63 vs ~$85 TP. Both SmartExit functions| +//| (main loop + FIX#41) now use g_multiTPEntries.stopLoss | +//| (original SL, never modified) for correct RR calculation. | +//| rr = profit / original_risk (not profit / BE_buffer) | +//| [FIX] v9.23 FIXES (Mar 02 2026) -- PROFITABILITY OVERHAUL: | +//| FIX#63: WEAK_TREND Regime Filter -- force single position when | +//| regime = WEAK_TREND_UP/DOWN (was 80% of all trades with | +//| 3-position x3 loss = -$752 total damage). Single pos | +//| in weak trend: WR=93.8%, avg +$13. 3-pos in weak: -$752.| +//| FIX#64: TP2/TP3 Fixed TPs restored -- were hardcoded 0 (trail | +//| only), never hit real targets. Now pass g_ea_signal.tp2 | +//| and .tp3 to actual orders -> real profit targets active. | +//| FIX#65: Trail Activation raised 0.5R->1.2R -- at 0.5R trail dist | +//| (0.8ATR ~= 7p) > 0.5R profit (4.5p) = negative lock. | +//| 49 positions closed BELOW entry despite positive trade. | +//| At 1.2R lock is guaranteed positive. | +//| FIX#66: Trail distances widened (TP1: 0.8->1.5, TP2: 1.3->1.8, | +//| TP3: 2.0->2.5 ATR) -- tight trail in EURUSD M15 killed | +//| runners prematurely. Avg win was $29 = only 3 pips. | +//| FIX#67: Lock thresholds raised (Lock1: 1.0->1.5R, Lock2: 2.0-> | +//| 2.5R, Lock3: 3.0->3.5R) -- locking at 1.0R closed trades | +//| at +0.3R ($9) while SL loss = $84 (R:R destroyed). | +//| FIX#68: Cascade SL buffer added (25% of TP1 distance) -- exact | +//| TP1 price as ladder SL left 0 room after TP1 -> TP2/TP3 | +//| always closed at TP1 price = $0 extra profit. | +//| FIX#69: Hard entry floor in trail -- trail can never move SL | +//| below entry price (buy) / above entry price (sell). | +//| Prevents "false profit" exits below entry. | +//| [FIX] v9.22 FIXES (Mar 02 2026): | +//| FIX#60: SmartEntry TrendingMarket Bypass -- when market scores | +//| >=75% trending (via MarketTrendScore), filters relaxed | +//| for with-trend entries. Counter-trend still blocked. | +//| New params: SmartEntry_TrendBypass (true), threshold 75 | +//| FIX#61: EURUSD M15 AutoOpt -- SL 1.3->1.1 (log: avg SL too wide)| +//| TP 3.5->4.5 (avg win=$25 vs avg loss=$77, R:R broken) | +//| minRR 1.3->1.8 (enforce positive expectancy minimum) | +//| FIX#62: Score system realism -- legacy modules (Zone,Sweep,Conf, | +//| Candle,OB,Vol,Div,TL) added to totalScore with 30% cap | +//| so score now realistically reflects full confluence. | +//| Max~95 vs old max=85 theoretical. | +//| [FIX] v9.21 MERGED FIXES (Feb 23 2026): | +//| FIX#1a: H&S/chart pattern expiry check (was SET but NEVER | +//| CHECKED -> H&S Top blocked ALL BUYs for 33 days) | +//| FIX#1b: H&S deduplication (same pattern re-detected every bar | +//| refreshing active state, preventing expiry) | +//| FIX#27: SL Floor 7p->5p for M15 Forex (7p floor+low ATR=3.8p | +//| destroyed R:R -> 0 trades after Jan 20) | +//| FIX#28: Hard time block (Monday AM + dead zone 22-01h). Soft | +//| shouldAvoidNow flag wasn't enforced -> 3-min SL at 01:00)| +//| FIX#29: AutoOpt/PairProfile no longer overrides user's explicit | +//| SessionAsian=false (was forced true for Major pairs) | +//| FIX#23a: 2->1 win to reset lossStreak (Multi-TP impossible w/ 2) | +//| FIX#23b: lossStreak capped at 8 (prevents 18+ from MTP splits) | +//| FIX#23c: EA_UpdateTradeResults per-entry-group buffering (v2) | +//| Tracks DEAL_POSITION_ID per group, flushes only when | +//| ALL group positions closed. Safe with MaxOpenTrades>1 | +//| FIX#24: Reset g_strategyPerf+lossStreak in backtest init | +//| FIX#24c: g_strategyNames aligned with actual cand.type strings | +//| FIX#25: DD calculation fixed (pips->balance% denominator) | +//| [FIX] v9.12 BACKTEST ANALYSIS FIXES (Feb 23 2026): | +//+------------------------------------------------------------------+ +//| | +//| [OK] FIX#A Stale MTF log messages (RELAXED/DISABLED) -- corrected | +//| [OK] FIX#B Dead code branches (mtfRelaxed/mtfDisabled) -- removed | +//| [OK] FIX#C AutoOpt floor: SCALP/INTRA 43->50, SWING 36->45 | +//| [OK] FIX#D MaxOpenTrades: uses logical trades (MultiTP IDs) | +//| not broker positions (legs) -- fixes 6 simultaneous | +//| LONGs bug on Jan 20 despite MaxOpenTrades=1 | +//| [OK] FIX#E M15 TP clamp raised (2.5/2.8/3.2 -> 3.0/4.0/5.5xATR) | +//| + minimum TP2/TP3 separation 1xATR from previous TP | +//| fixes TP2/TP3 never hitting (killed by noise after SL | +//| ladder to TP1 with only 2-3p room) | +//| [OK] FIX#F CheckKillzoneSignal: guard against ATR=0 producing | +//| SL=currentPrice (max 3xATR=279p bug on Jan 28) | +//| | +//+------------------------------------------------------------------+ +//| [FIX] v9.07 FIX#6 -> SUPERSEDED by v9.13 FIX#23a/b/c: +//+------------------------------------------------------------------+ +//| * g_lossStreakForMTF: 1 win to reset (was 2), capped at 8 | +//| * UpdateTradingStreak: 1-win reset + cap (v9.13 FIX#23a/b) | +//| * EA_UpdateTradeResults: per-group buffer (v9.13 FIX#23c v2) | +//| * AddCandidate: uses stable g_lossStreakForMTF (not currentStreak)| +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| [FIX] v9.08 BACKTEST ANALYSIS FIXES (Feb 21 2026): | +//+------------------------------------------------------------------+ +//| | +//| [OK] FIX#11 BOS Momentum Array Index (as_series bug) | +//| [OK] FIX#12 Absolute SL Floor 7 pips (Forex) | +//| [OK] FIX#13 EQUITY CAP removal for Forex (LOT SIZING BUG) | +//| [OK] FIX#14 MinEntryScore 65->70 | +//| | +//+------------------------------------------------------------------+ +//| [FIX] v9.09 H1/H4 CRASH FIX + MULTI-TF (Feb 22 2026): | +//+------------------------------------------------------------------+ +//| | +//| [OK] FIX#15 DailyDD 3.9%->8% (1 trade at 5% risk > 3.9% DD limit | +//| -> ExpertRemove() killed EA after first SL! "removed itself | +//| on 7% of testing interval" on H1 and M5) | +//| | +//| [OK] FIX#16 StopOnDrawdown true->false (pause signals, don't kill | +//| EA -- ExpertRemove terminates backtest permanently) | +//| | +//| [OK] FIX#17 WeeklyDD 9%->15% (consistent with daily 8%) | +//| | +//| [OK] FIX#18 TF-aware SL Floor: M15=7p, H1=12p, H4=20p | +//| (7 pips too tight for H1/H4 where ATR is 15-60 pips) | +//| | +//| [FIX] v9.06 BACKTEST ANALYSIS FIXES (Feb 21 2026): | +//+------------------------------------------------------------------+ +//| | +//| [OK] FIX#6 MTF BULLISH LOCK -> Consecutive Loss Relaxation: | +//| 13 consecutive SL hits (T20-T32) all BUY in bearish market. | +//| H4 MTF=BULL + M15 Structure=BULL blocked all SELLs. | +//| Fix: After 3+ losses, relax MTF block (allow counter-trend | +//| with -10 score penalty). After 5+ losses, disable MTF block. | +//| | +//| [OK] FIX#7 SL FLOOR 2.0xATR -> 1.3x (Forex pairs): | +//| 671 SL FLOOR hits inflated SL from 6p->10p = +67% bigger loss. | +//| Fix: Pair-adaptive floor. Forex=1.3x, Metals/Indices=1.5x. | +//| | +//| [OK] FIX#8 TP R:R CAP -- Max 3.5R for TP1: | +//| FVG/OB: SL=9p but TP1=93p (R:R=10.2!) -> never hits. | +//| Fix: Cap TP1 at 3.5R, TP2 at 5.0R, TP3 at 6.5R of SL dist. | +//| | +//| [OK] FIX#9 ProfitLock 0.8R->1.5R -- Stop killing winners: | +//| 10 trades closed at +7p (0.5R lock) while TP1 avg=33p. | +//| Fix: BE threshold 0.8->1.5R, lock level 0.5R->1.0R, | +//| buffer 5.0->2.0 pips. ~260 pips profit recovered. | +//| | +//| [OK] FIX#10 BOS_RETEST Momentum Filter: | +//| 16/41 trades stopped within 60 min (entry against momentum). | +//| Fix: Reject BOS_RETEST if 2/3 last candles oppose direction. | +//| | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| | +//| [OK] FIX#1 HistorySelect(0,...) in EA_UpdateTradeResults: | +//| Was scanning ALL history every 5 sec -> freeze on live accounts| +//| Fixed: 90-day window only (more than enough for tracking) | +//| | +//| [OK] FIX#2 ManageFullHybrid HistorySelect every tick: | +//| Was calling HistorySelect(24h) on EVERY tick -> heavy perf | +//| Fixed: Throttled to once every 30 seconds | +//| | +//| [OK] FIX#3 Weekly DD: Missing email + ExpertRemove: | +//| Weekly DD only printed log. Daily/Total DD had full stop. | +//| Fixed: Weekly DD now consistent -- email + ExpertRemove when | +//| EA_StopOnDrawdown=true (critical for FTMO weekly 10% rule) | +//| | +//| [OK] FIX#4 ResetWeeklyDrawdown dead code: | +//| MqlDateTime dt / TimeToStruct(TimeCurrent()) declared but | +//| never read -> removed clean | +//| | +//| [OK] FIX#5 Version strings corrected: | +//| OnDeinit showed v5.1, OnInit end showed v6.41 -> now v8.00 | +//| | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| [FIX] v9.03 FTMO-SAFE + SMART POSITION MANAGEMENT (Feb 2026): | +//+------------------------------------------------------------------+ +//| | +//| [OK] FIX#1: AutoOpt recalc TF-aware (was 15min hardcoded). | +//| effective = max(input, 4xTF candles). M5->60,H1->240,H4->960 | +//| | +//| [OK] FIX#2: Score tiers expanded 5->6 for lot sizing. | +//| Added 85+ (1.30x) and 92+ (1.40x) tiers. Was: 80+ = 1.3x | +//| | +//| [OK] FIX#3: WinProb clamp removed hardcoded 52% floor. | +//| Now uses SmartEntry_MinWinProb input (default 42%). | +//| | +//| [OK] FIX#4: FTMO-safe defaults for all risk parameters. | +//| Risk 5->1%, MaxDaily 2000->5, DD 8->3.8%, MaxRisk 5->2% | +//| | +//| [OK] FIX#5: EA_MaxOpenTrades + EA_HedgeMode (3 modes): | +//| Was: hardcoded lock at 1 (dead input). Now: respects input. | +//| NO_HEDGE: block opposite direction (FTMO safe, default) | +//| CLOSE_AND_REVERSE: close existing -> open opposite (atomic) | +//| ALLOW_HEDGE: both directions open (per-dir limit applies) | +//| Risk cap: total open risk <= daily DD limit (all modes). | +//| | +//| [OK] FIX#6: EA_UseMultipleTP=true (was false, comment said true). | +//| [OK] FIX#7: EA_TP3_ATR=3.8 (was 3.0, comment said 3.8). | +//| | +//| [OK] FIX#8: STRUCTURE-FIRST TP TARGETING (major ICT improvement). | +//| Was: TPs = ATR x fixed mult, then pull closer for obstacles. | +//| Now: Find swing/liquidity/OB/FVG targets first, sort by dist.| +//| TP1=nearest target, TP2=next, TP3=furthest. ATR is fallback. | +//| Result: TPs land on REAL levels -> much higher TP2/TP3 hit%. | +//| | +//| [OK] FIX#9: TF-AWARE POSITION MANAGEMENT via AutoOpt override. | +//| Was: BE/Trail/SmartExit used fixed inputs for ALL timeframes. | +//| Now: AutoOpt sets per-TF values (SCALP/INTRA/SWING/POSITION) | +//| BE_RR: 1.2(M5) -> 1.5(M15) -> 1.8(H4) -> 2.2(D1) | +//| Trail: 0.7ATR(M5) -> 0.9(M15) -> 1.3(H4) -> 1.8(D1) | +//| SmartExit: 0.8R(M5) -> 1.0R(M15) -> 1.5R(H4) -> 2.0R(D1) | +//| Inputs preserved for manual mode (AutoOpt OFF). | +//| | +//| [OK] FIX#10: AUDIT FIXES -- 6 corrections for proper operation: | +//| a) SmartExit double-adjust BUG: TF switch was re-applied on | +//| already-TF-adjusted values (0.8Rx2.5=2.0R, never fired). | +//| Now: TF switch only fires when AutoOpt OFF (manual mode). | +//| b) TP% validation: warn if TP1+TP2+TP3 != 100%. | +//| c) Hybrid trail TF-aware: M5x0.7, M15x0.9, H4x1.2, D1x1.5. | +//| d) TP1/TP2 BE threshold TF-aware: scales with TF category. | +//| e) Structure TP pair-category: spacing/buffer wider for noisy | +//| pairs (Index=0.35, Metal=0.30, Forex=0.20 x ATR). | +//| f) Obstacle avoidance buffer pair-aware (Index=0.45xATR). | +//| | +//| [OK] FIX#11: TF-AWARE DETECTION PARAMS -- 16 indicator params now | +//| AutoOpt-adjusted per timeframe: | +//| Ages: OTE/BB/MB/Trendline/CRT/TBS/AMD/SB/Signal expiry | +//| Sensitivity: FVG_MinStrength, Regime_Lookback/ConfirmBars, | +//| Divergence/Trendline/FIB lookbacks | +//| Example: FVG_MinStrength SCALP=0.30, INTRA=0.35, SWING=0.45 | +//| Example: Regime_Lookback SCALP=20, SWING=14, POS=10 | +//| 37 raw input refs replaced with g_working* globals. | +//| | +//| [OK] FIX#12: PAIR-SPECIFIC DETECTION -- AutoOpt Step 2.5: | +//| Applied AFTER TF adjustments, scales detection per pair: | +//| Metal: ageMult=1.0, strengthMult=0.85, ob_vol=1.2 | +//| Index: ageMult=0.80, strengthMult=1.20, shorter lookbacks | +//| Energy: ageMult=0.85, shorter lookbacks | +//| Exotic: ageMult=0.75, strengthMult=1.25 (low liquidity) | +//| Also fixed: common defaults now use INPUT values, not | +//| hardcoded (fvg_max_age=FVG_MaxAge, not 100). | +//| | +//| [OK] FIX#13: BROKEN AUTOOPT->DETECTION CONNECTIONS -- 9 fixes: | +//| 5 params had AutoOpt values but detection used raw input: | +//| STRUCT_SwingStrength(4 refs), LIQ_SwingStrength(1), | +//| OB_MaxAge(2), LIQ_MaxAge(1), FVG_ExtendBars(1), | +//| FVG_MaxAge(2 detection refs), FVG_MinSize(1 detection ref) | +//| Added: g_workingOB_MaxAge, g_workingLIQ_MaxAge globals | +//| Fixed: ApplyAutoOptToWorkingVars now writes swing/OB/LIQ | +//| Also: AutoOpt_MaxSL_Mult 2.8->4.0 (Index min=2.5 had no room)| +//| | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| [FIX] v9.01 BACKTEST FIXES (Feb 2026): | +//+------------------------------------------------------------------+ +//| | +//| [OK] FIX#A TP1 too far (2.0xATR=50pips): Only 1 TP hit in 40 | +//| trades! SL=30pips, TP1=50pips unreachable for EURUSD M15. | +//| Fixed: EA_TP1_ATR 2.0->1.3, TP2 2.8->2.0, TP3 3.8->3.0 | +//| EA_UseMultipleTP = true (50% TP1, 30% TP2, 20% runner) | +//| | +//| [OK] FIX#B BE never fired: TP1_BE_Threshold 2.0->0.9, RR 1.2->0.8 | +//| BE now activates at 0.8R (before price reverses to SL) | +//| | +//| [OK] FIX#C Risk% hard cap at 2% (line 23547): | +//| MathMin(2.0,...) hardcoded -- ignored EA_RiskPercent=5%. | +//| Fixed: cap now = EA_RiskPercent (user setting respected). | +//| | +//| [OK] FIX#D SmartEntry_MinWinProb 45%->40% for EURUSD ranging | +//| 126 rejections with EV=1-4R but WinP=42-44% -- over-filtered | +//| | +//| [OK] FIX#E MTF=NEUTRAL reject message (mtfNeutralBlocked flag) | +//| [OK] FIX#F SL floor display (5 decimals for forex) | +//| [OK] FIX#G double->int pairFloor type mismatch | +//| | +//+------------------------------------------------------------------+ +//| * v9.03 BUG FIXES (Feb 2026): | +//+------------------------------------------------------------------+ +//| | +//| [OK] FIX#1 Counter-trend blocked big moves (Jan 27-28 EURUSD): | +//| MTF=STRONG_BULL + M15 Regime=DOWNTREND -> BUY blocked as | +//| "Counter-trend BEAR". Now: strongMTFAgreesWithTrade flag | +//| bypasses regime counter-trend check when HTF strongly agrees. | +//| | +//| [OK] FIX#2 Structure penalty too aggressive during big moves: | +//| Log: MTF=STRONG_BULL | ATR=17.7p -> "Low confidence: 70 < 75" | +//| M15 structure lags during fast moves. Fix: when STRONG_MTF | +//| confirms AND volatilityRatio > 1.5, reduce penalty 10pts. | +//| | +//| [OK] FIX#3 REGIME_VOLATILE always blocked trading (shouldTrade=F): | +//| Big trending moves classified as VOLATILE -> zero trades. | +//| Fix: allow trading in VOLATILE when MTF confirms direction | +//| (BULLISH/STRONG_BULLISH or BEARISH/STRONG_BEARISH). Half size. | +//| | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| [FIX] v8.03 BUG FIX: | +//+------------------------------------------------------------------+ +//| | +//| [OK] FIX#6 g_dailyStartEquity was dead code in DD calculation: | +//| CalculateDailyDrawdown() used ONLY g_dailyStartBalance as | +//| reference, ignoring g_dailyStartEquity entirely. | +//| PROBLEM: If day starts with open profitable trades, | +//| equity > balance. Using only balance UNDERREPORTS DD. | +//| Example: startBal=$10,000 startEq=$10,500 (+$500 floating) | +//| equity falls to $9,500 -> real loss $1,000 | +//| OLD: DD = (10000-9500)/10000 = 5.0% <- WRONG | +//| NEW: DD = (10500-9500)/10500 = 9.52% <- CORRECT | +//| RISK: Could breach FTMO 5% daily limit without triggering | +//| the protection. Fixed: startLevel = MathMax(bal, equity) | +//| | +//| [OK] FIX#1 TIMEZONE: Daily DD reset was at 01:05-01:15 broker | +//| time instead of 00:00 GMT (backtest log confirmed). | +//| Fixed: ALL 4 locations now use TimeGMT() not TimeCurrent() | +//| Affected: ResetDailyDrawdown(), OnInit(), OnNewBar(), | +//| ResetWeeklyDrawdown() | +//| | +//| [OK] FIX#2 RISK CEILING: AutoOpt_MaxRiskOverride default 5.0->2.0 | +//| THIS is the REAL risk ceiling, not EA_RiskPercent! | +//| Backtest showed 228 warnings: risk silently capped to 2% | +//| while user thought they were running 5%. Now explicit. | +//| | +//| [OK] FIX#3 MaxDailyTrades: Confirmed default=6 (was 2000 in | +//| the user's backtest config -> effectively unlimited). | +//| Added warning comment to prevent accidental change. | +//| | +//| [OK] FIX#4 ExpertRemove: Both DD stops now CLOSE ALL positions | +//| before ExpertRemove(). Previously trades were left open | +//| with no management after EA removal -> uncontrolled losses. | +//| | +//| [OK] FIX#5 TickValue fallback: Added CFD/Index detection. | +//| For US100/XAUUSD: TickValue=$0.01 is wrong (real=$1.00). | +//| If OrderCalcProfit fails on CFD -> ABORT trade, not fallback | +//| (fallback would open 100x oversized lots -> instant blowup). | +//| | +//| [OK] FIX#6 Counter-trend filter: Relaxed Index thresholds | +//| Backtest showed 94% signal rejection due to M15 Structure | +//| conflicting with H4 MTF direction (normal for US100). | +//| EV threshold: 0.35->0.25, Score: 68->60, WinP: 50->48 | +//| | +//| [OK] FIX#7 multi_tp_factor: Removed dead code variable | +//| Was always 1.0 regardless of EA_UseMultipleTP flag. | +//| | +//| [GEAR] FTMO SAFE DEFAULTS v8.0: | +//| EA_MaxDailyTrades = 6 | +//| AutoOpt_MaxRiskOverride = 2.0% | +//| EA_MaxDailyDrawdownPercent = 3.9% (< FTMO 5% limit) | +//| EA_StopOnDrawdown = true (closes positions + removes EA) | +//| | +//| [FIX] v7.90 CRITICAL BUG FIXES: | +//| [OK] BUG#1+5: EA_RiskPercent cap was HARDCODED 2.0% (silent!) | +//| Fixed: Uses AutoOpt_MaxRiskOverride (configurable ceiling) | +//| [OK] BUG#2: Kelly/Regime/Streak PositionSize = 100% DEAD CODE! | +//| Fixed: EA_ExecuteTrade now reads g_smartEntry.recommendedLots | +//| [OK] BUG#3: Two daily trade counters (g_ea_stats.trades + | +//| g_tradesThisDay) were never in sync. Fixed: synced at reset | +//| [OK] BUG#4: EA_MaxDailyTrades=2000 (= unlimited, prop firm risk!) | +//| Fixed: default now 6 (realistic for M5 Gold / FTMO safe) | +//| [OK] BUG#6: MultiTP opened 3 positions but counted as 1 trade | +//| Fixed: counter increments by trades_opened (1, 2, or 3) | +//| [OK] BUG#7: AutoOpt silently overrode EA_RiskPercent without warning | +//| Fixed: Added explicit [WARN] log whenever risk is overridden | +//| [OK] BUG#8: AutoOpt_RecalcMinutes=60 (stale after news/session) | +//| Fixed: default now 15min (responsive to regime changes) | +//| [GEAR] For 5% risk on Gold/FTMO: set AutoOpt_MaxRiskOverride=5.0 | +//| | +//| [FIX] v6.41 INPUT UNIFICATION: | +//| [OK] UNIFIED: AccountRiskPercent -> EA_RiskPercent (master control) | +//| [OK] UNIFIED: UseFixedLots/FixedLotSize -> EA_FixedLotSize (0=risk%)| +//| [OK] UNIFIED: MinRiskReward -> EA_MinRR (single R:R filter) | +//| [OK] UNIFIED: MaxTradesPerDay -> EA_MaxDailyTrades (single limit) | +//| [OK] UNIFIED: DailyLossLimit -> EA_MaxDailyRisk (single DD limit) | +//| [OK] UNIFIED: InpTP1/2/3_Percent -> EA_TP1/2/3_Percent | +//| [OK] UNIFIED: InpMoveSLToBreakeven -> EA_MoveToBreakEven | +//| [OK] UNIFIED: InpBreakevenOffset -> EA_BE_Buffer_Pips | +//| [OK] UNIFIED: EA_UseSessionSL -> SessionSL_Enabled | +//| [OK] BUG FIX: CalculatePositionSize used raw SL price as distance! | +//| [OK] All old indicator inputs -> #define aliases to EA_* parameters | +//| [OK] Single source of truth: user changes ONE parameter, affects ALL| +//| [FIX] v6.40 CRITICAL DAILY DRAWDOWN FIX: | +//| [OK] FIXED: Daily Drawdown calculation was BROKEN! | +//| Old: Used (Balance-Equity)/Balance = always 0% when no positions| +//| New: Tracks from start of day, cumulative losses count | +//| [OK] FIXED: EA_MaxDailyLosses (3) was DEAD CODE (never used) | +//| [OK] FIXED: EA_StopOnDrawdown was DEAD CODE (never used) | +//| [OK] NEW: ResetDailyDrawdown() - Auto-reset at 00:00 GMT | +//| [OK] NEW: CalculateDailyDrawdown() - Correct cumulative tracking | +//| [OK] NEW: CheckDailyDrawdownLimit() - Proper limit enforcement | +//| [OK] NEW: Global variables for DD tracking (g_dailyStartBalance) | +//| [OK] IMP: EA_StopOnDrawdown now works correctly: | +//| true = Stop EA completely (ExpertRemove) | +//| false = Block signals, auto-resume next day | +//| [OK] IMP: Email alerts on DD limit (if EA_EmailOnDrawdown=true) | +//| [OK] IMP: Comprehensive logging of DD events and resets | +//| | +//| [FIX] v6.39 PRODUCTION IMPROVEMENTS: | +//| [OK] IMP#2: SafePositionModify with SL/TP validation | +//| All PositionModify calls now validate stop-level distance | +//| Error tracking, consecutive failure alerts, detailed logging | +//| [OK] IMP#5: All hardcoded BE/Trail values -> configurable inputs | +//| EA_BE_Buffer_Pips (was 5.0), EA_TP1_BE_Threshold (was 2.5) | +//| EA_TP2_BE_Threshold (was 3.0), EA_BE_Ranging_RR (was 1.8) | +//| EA_BE_Volatile_RR (was 2.0), Trail Volatile/Trending params | +//| | +//| [FIX] v6.38 COMPREHENSIVE CODE REVIEW FIXES: | +//| [OK] BUG#1: Double SmartAdjustTPs for FVG/OB -> TPs pulled 2x | +//| FVG/OB now build SL/TP manually, SmartAdjustTPs called ONCE | +//| [OK] BUG#2: TP1 (50%) NEVER got BE -> full SL losses on reversals | +//| TP1 now gets late BE at 2.5R (protects 50% of position) | +//| [OK] BUG#3: AMD only triggered on DISTRIBUTION (not direction-aware)| +//| Now: Accumulation=bullish, Distribution=bearish | +//| [OK] BUG#4: Judas self-confirmation ignored direction | +//| Now checks Judas swing direction matches candidate | +//| [OK] BUG#5: CRT entry price set AFTER BuildCandidateSLTP | +//| Now set BEFORE so SL/TP calculated from correct entry | +//| [OK] BUG#6: Spread-adjusted R:R vs raw minRR -> unfair rejections | +//| minRR now adjusted for spread in SelectBestCandidate | +//| [OK] ISSUE#1: EV filter useless (WP always 50-68%) | +//| MinExpValue 0.35->0.15, MinWinProb 48->52 | +//| [OK] ISSUE#2: Hard conflict filter when Bull/Bear within 10pts | +//| [OK] ISSUE#3: TP3 structure close too early (1.0-1.6R) | +//| TP3 now: baseThreshold=2.5, minStrength=0.85 | +//| [OK] ISSUE#4: Proximity close cut TP2 short at 92% | +//| Now skips TP1 AND TP2 (only TP3/single) | +//| [OK] ISSUE#5: DetectMarketRegime ran conditionally | +//| Now always runs (used by SL/TP/trailing/BE everywhere) | +//| [OK] ISSUE#7: Candle reaction too strict in ranging | +//| Relaxed: doji/spinning top = valid reaction in ranging | +//| | +//| [FIX] v6.3 CRITICAL FIXES & EXIT OPTIMIZATION: | +//| [OK] FIX: TP1=2.5/SL=1.5=1.67 < MinRR 1.8 -> trades rejected! | +//| TP1 3.0, TP2 4.5, TP3 6.0 (proper R:R >= 2.0) | +//| [OK] FIX: Breakeven too early (was 1.0R ranging, 0.8R volatile) | +//| Now: 2.2R trending, 1.8R ranging, 1.5R volatile | +//| [OK] FIX: BE buffer 1 pip -> 5 pips (noise protection) | +//| [OK] FIX: Trailing in ranging caused whipsaws -> DISABLED | +//| Ranging uses structure close + BE instead | +//| [OK] FIX: Trailing start too early (0.6R volatile -> 1.5R) | +//| [OK] NEW: Strength-weighted structure close thresholds | +//| Strong OB (0.9) -> close at 1.05R, Weak (0.3) -> 1.55R | +//| [OK] NEW: EA_BreakEven_RR input parameter (configurable) | +//| [OK] NEW: R:R safety floor after SmartAdjustTPs | +//| [OK] NEW: Enhanced logging with regime & strength info | +//| | +//| [FIX] EA CONVERSION v6.0 (MAJOR REWRITE): | +//| [OK] REMOVED OnCalculate() - indicator dead code eliminated | +//| [OK] ALL analysis logic moved to OnNewBar() EA engine | +//| [OK] OnTick() handles tick-level operations properly | +//| [OK] Bar data via CopyTime/Open/High/Low/Close/TickVolume | +//| [OK] g_totalRates set from iBars() + ArraySize() | +//| [OK] Full module coverage: FVG, OB, Liquidity, Structure, | +//| BreakerBlocks, MitigationBlocks, OTE, OFI, VolumeProfile, | +//| MarketMaker, ML/NN, Killzones, CRT, TBS, AMD, Judas, | +//| Divergences, Trendlines, VSA, SilverBullet, MultiTP, | +//| AutoOptimization, NewsFilter, SmartEntry | +//| [OK] Tick-level: trailing, BE, MultiTP, dashboard, killzones, | +//| divergence mgmt, trendline mgmt, SB mgmt, TBS updates | +//| [OK] No more indicator_chart_window / indicator_plots warnings | +//| [OK] Proper EA event flow: OnInit->OnTick->OnNewBar->OnTimer | +//| | +//| [FIX] PERFORMANCE OPTIMIZATION v6.2: | +//| [OK] EA_MinEntryScore 40->75 (medium quality had 0% WR) | +//| [OK] Session SL multipliers reduced (NY 1.5->1.1, LDN 1.2->1.0) | +//| (NY SL was 2.25 ATR vs TP1 2.5 ATR = only 1.11 R:R) | +//| [OK] FVG entry requires QUALITY_HIGH (was MEDIUM) | +//| [OK] OB strength threshold 0.6->0.75 | +//| [OK] Breakeven trigger 1.0R->1.5R (premature BE cut winners) | +//| [OK] EA_MinRR 1.0->1.5 (enforces better risk:reward) | +//| [OK] TP1 2.5->3.0 ATR, TP2 3.5->4.5 ATR (better reward profile) | +//| [OK] FIXED: Max Drawdown was always 0.00% (never calculated) | +//| | +//| [FIX] TRADE EXECUTION FIX v6.1: | +//| [OK] CRITICAL: Added UpdateMTFAnalysis() call in OnNewBar | +//| (was NEVER called -> MTF always NEUTRAL -> trend always fails) | +//| [OK] CRITICAL: Fixed R:R math (TP1=2.5 ATR, MinRR=1.0) | +//| (was TP1=2.0/SL=1.5=1.33 < MinRR 1.5 -> always rejected) | +//| [OK] Fixed EA_MaxSpreadPips 2.0->5.0 (Gold compatible) | +//| [OK] TBS/CRT: re-validate R:R AFTER SL/TP override | +//| [OK] Added comprehensive debug logging for signal flow | +//| [OK] BUY/SELL symmetry verified: 4+4 calls, BE, Trailing | +//| | +//| Previous fixes preserved: | +//| [OK] FVG_Struct.active field, g_fvgCount/g_obCount | +//| [OK] ATR via handle with CopyBuffer + fallback | +//| [OK] EnableStructure naming, all var declarations | +//+------------------------------------------------------------------+ +// =============================================================== +// COMPILE MODE SWITCH +// =============================================================== +// -> COMPILE_AS_EA (default): Expert Advisor with automated trading +// - OnTick() active, OnCalculate() disabled +// - Full EA: trade execution, position management, trailing stops +// +// -> COMMENT OUT to compile as INDICATOR: +// - OnCalculate() active, OnTick() disabled +// - Visual only: FVG, OB, Liquidity, Signals on chart +// - NO trade execution +// +// BOTH modes share RunSharedAnalysis() for ALL ICT analysis +// =============================================================== +#define COMPILE_AS_EA +#ifdef COMPILE_AS_EA +#property copyright "ICT Smart Trader Professional 2026" +#property link "https://www.professionaltrading.com" +#property version "11.20" +#property description "EA ANGEL v11.20 - ICT Smart Trader Professional" +#property description "FVG, Order Blocks, Liquidity, Killzones, Neural Network, VSA, CRT, TBS" +#property description "Both EA and Indicator from ONE source file" +#property strict +// * v9.30: EA identity constants -- used in Dashboard title bar +// * v9.52b: VERSION + LAST_FIX updated every release. Dashboard title auto-picks EA_TITLE. +// EA_LAST_FIX shown in dashboard subtitle row (DrawDashboardTitleBar). +// FORMAT: "v{major}.{minor}{letter}" e.g. "v10.00" +// LAST_FIX: last applied fix number (shown in dashboard as "FIX#NNN") +#define EA_NAME "EA ANGEL" +#define EA_VERSION "v11.20" +#define EA_LAST_FIX "v11.20 — FIX#510: Scenario gate for BREAKER/JUDAS/CRT/SB | FIX#511: S11 early trigger rangeConfScore>=2 | FIX#512: E2 wick threshold 0.40 in ACCUMULATION | FIX#513: SmartExit wick threshold 65%→58%" +#define EA_LAST_DATE "2026.04.13" +#define EA_TITLE EA_NAME " " EA_VERSION // "EA ANGEL v11.03" -- auto-updates +#else // COMPILE_AS_INDICATOR +#property copyright "ICT Smart Trader Professional 2025" +#property link "https://www.professionaltrading.com" +#property version "8.00" +#property indicator_chart_window +#property indicator_plots 0 +#property description "ICT Smart Trader Professional v8.01 - INDICATOR MODE" +#property description "Visual: FVG, Order Blocks, Liquidity, Signals, Dashboard" +#property strict +#endif // COMPILE_AS_EA +// Include MQL5 Trade Libraries +#ifdef COMPILE_AS_EA +#include +#include +#include +#include +#endif +// =============================================================== +// [UNIFIED] Compatibility aliases - redirect old indicator inputs to EA inputs +// These are NOT inputs - they reference the EA_* master parameters +// =============================================================== +#define AccountRiskPercent EA_RiskPercent +#define MinRiskReward EA_MinRR +#define MaxTradesPerDay EA_MaxDailyTrades +#define DailyLossLimit EA_MaxDailyDrawdownPercent +#define InpTP1_Percent ((int)EA_TP1_Percent) +#define InpTP2_Percent ((int)EA_TP2_Percent) +#define InpTP3_Percent ((int)EA_TP3_Percent) +#define InpMoveSLToBreakeven EA_MoveToBreakEven +#define UseFixedLots (EA_FixedLotSize > 0) +#define FixedLotSize EA_FixedLotSize +#define EA_UseSessionSL SessionSL_Enabled +// =================================================================== +// [UNIFIED v6.42] Indicator -> EA aliases (one input, shared by both) +// =================================================================== +#define SL_ATRMultiplier EA_StopLossATR +#define TP_ATRMultiplier EA_TP1_ATR +#define RSIOverbought ((double)EA_RSI_Overbought) +#define RSIOversold ((double)EA_RSI_Oversold) +#define MinEntryQuality ((double)EA_MinEntryScore) +#define EnableRiskMgmt EA_Enabled +#define Scoring_MinEntryScore ((int)EA_MinEntryScore) +#define InpMinEntryScore ((int)EA_MinEntryScore) +// EA News -> Indicator News aliases +#define EA_EnableNewsFilter News_FilterEnabled +#define EA_MinutesBeforeNews (g_workingNews_MinsBeforeHigh > 0 ? g_workingNews_MinsBeforeHigh : News_MinsBeforeHigh) +#define EA_MinutesAfterNews (g_workingNews_MinsAfterHigh > 0 ? g_workingNews_MinsAfterHigh : News_MinsAfterHigh) +#define EA_CloseBeforeNews News_CloseBeforeNews +#define EA_FilterHighImpact News_AvoidHighImpact +#define EA_FilterMediumImpact News_AvoidMediumImpact +//+------------------------------------------------------------------+ +//| SECTION 1: ENUMERATIONS | +//+------------------------------------------------------------------+ +// VSA Pattern Types +enum ENUM_VSA_PATTERN +{ + VSA_NONE = 0, // No Pattern + VSA_UPTHRUST = 1, // Upthrust (Bearish) + VSA_NO_DEMAND = 2, // No Demand (Bearish) + VSA_NO_SUPPLY = 3, // No Supply (Bullish) + VSA_STOPPING_VOLUME = 4, // Stopping Volume + VSA_CLIMAX = 5, // Climax + VSA_TEST = 6, // Test + VSA_SPRING = 7, // Spring (Bullish) + VSA_UPTHRUST_SPRING = 8, // Upthrust after Spring + VSA_EFFORT_NO_RESULT = 9, // Effort with No Result + VSA_ABSORPTION = 10 // Absorption Volume +}; +// VSA Signal Direction +enum ENUM_VSA_SIGNAL +{ + VSA_NO_SIGNAL = 0, + VSA_BULLISH = 1, + VSA_BEARISH = 2, + VSA_NEUTRAL = 3 +}; +// MTF Direction +// * v9.03: Hedge Mode -- controls behavior when opposite-direction signal appears +enum ENUM_HEDGE_MODE +{ + HEDGE_NO_HEDGE = 0, // No Hedge (block opposite, wait for close) + HEDGE_CLOSE_AND_REVERSE = 1, // Close & Reverse (close existing -> open opposite) + HEDGE_ALLOW = 2 // Allow Hedge (keep both directions open) +}; +enum ENUM_MTF_DIRECTION +{ + MTF_STRONG_BULLISH = 2, + MTF_BULLISH = 1, + MTF_NEUTRAL = 0, + MTF_BEARISH = -1, + MTF_STRONG_BEARISH = -2 +}; +// FVG ENUMERATIONS +enum ENUM_FVG_TYPE { + FVG_TYPE_BULLISH, + FVG_TYPE_BEARISH, + FVG_TYPE_INVERSE_BULL, + FVG_TYPE_INVERSE_BEAR +}; +enum ENUM_FVG_STATUS { + FVG_STATUS_ACTIVE, + FVG_STATUS_MITIGATED, + FVG_STATUS_FILLED, + FVG_STATUS_EXPIRED, + FVG_STATUS_INVALID +}; +enum ENUM_FVG_QUALITY { + FVG_QUALITY_LOW, + FVG_QUALITY_MEDIUM, + FVG_QUALITY_HIGH, + FVG_QUALITY_PREMIUM +}; +// TRADING ENUMERATIONS +enum ENUM_TP_MODE { + TP_BY_ATR, + TP_BY_FIBONACCI, + TP_BY_LIQUIDITY, + TP_BY_SWING_STRUCTURE, + TP_BY_FVG, + TP_BY_ORDER_BLOCK +}; +enum ENUM_TREND { + TREND_NONE = 0, + TREND_BULLISH = 1, + TREND_BEARISH = -1 +}; +enum ENUM_ZONE_TYPE { + ZONE_PREMIUM = 1, + ZONE_DISCOUNT = -1, + ZONE_EQUILIBRIUM = 0 +}; +enum ENUM_OB_STATUS { + OB_FRESH = 0, + OB_TOUCHED = 1, + OB_MITIGATED = 2, + OB_BROKEN = 3 +}; +// NEURAL NETWORK ENUMERATIONS +enum ENUM_NN_ACTIVATION { + ACTIVATION_RELU, + ACTIVATION_LEAKY_RELU, + ACTIVATION_SIGMOID, + ACTIVATION_TANH, + ACTIVATION_SOFTMAX, + ACTIVATION_ELU, + ACTIVATION_SWISH +}; +// SESSION TYPE (for SL adjustment) +enum ENUM_SESSION_SL_TYPE { + SESSION_SL_ASIAN, + SESSION_SL_LONDON, + SESSION_SL_NY, + SESSION_SL_OVERLAP, + SESSION_SL_DEAD_ZONE +}; +enum ENUM_NN_OPTIMIZER { + OPTIMIZER_SGD, + OPTIMIZER_MOMENTUM, + OPTIMIZER_ADAM, + OPTIMIZER_RMSPROP, + OPTIMIZER_ADAMW +}; +enum ENUM_NN_LOSS { + NN_LOSS_MSE, + NN_LOSS_BINARY_CROSSENTROPY, + NN_LOSS_CATEGORICAL_CROSSENTROPY, + NN_LOSS_HUBER +}; +enum ENUM_ML_PREDICTION { + PRED_STRONG_BULLISH, + PRED_BULLISH, + PRED_NEUTRAL, + PRED_BEARISH, + PRED_STRONG_BEARISH +}; +// KILLZONE ENUMERATIONS +enum ENUM_KILLZONE_TYPE { + KZ_NONE, + KZ_ASIAN, + KZ_LONDON_OPEN, + KZ_LONDON_CLOSE, + KZ_NY_OPEN, + KZ_NY_LUNCH, + KZ_NY_CLOSE, + KZ_SILVER_BULLET_LDN, + KZ_SILVER_BULLET_NY_AM, + KZ_SILVER_BULLET_NY_PM +}; +enum ENUM_DST_MODE { + DST_AUTO_DETECT, + DST_US_RULES, + DST_EU_RULES, + DST_UK_RULES, + DST_MANUAL, + DST_DISABLED +}; +enum ENUM_TIMEZONE { + TZ_AUTO, + TZ_UTC, + TZ_GMT, + TZ_EST, + TZ_CET, + TZ_JST, + TZ_AEST, + TZ_BROKER, + TZ_BROKER_TIME +}; +enum ENUM_SESSION { + SESSION_NONE = 0, + SESSION_ASIAN = 1, + SESSION_LONDON = 2, + SESSION_NY = 3 +}; +enum ENUM_SB_TYPE { + SB_LONDON = 1, + SB_AM_NY = 2, + SB_PM_NY = 3 +}; +// BACKTESTING ENUMERATIONS +enum ENUM_BACKTEST_MODE { + BACKTEST_DISABLED, + BACKTEST_FULL, + BACKTEST_WALK_FORWARD, + BACKTEST_MONTE_CARLO +}; +enum ENUM_OPTIMIZATION_TARGET { + OPT_NET_PROFIT, + OPT_PROFIT_FACTOR, + OPT_SHARPE_RATIO, + OPT_SORTINO_RATIO, + OPT_MAX_DRAWDOWN, + OPT_WIN_RATE, + OPT_EXPECTANCY +}; +// CRT ENUMERATIONS +enum ENUM_CRT_TYPE { + CRT_BULLISH = 1, + CRT_BEARISH = -1, + CRT_NEUTRAL = 0 +}; +enum ENUM_CRT_STATUS { + CRT_FORMING = 0, + CRT_CONFIRMED = 1, + CRT_TRIGGERED = 2, + CRT_INVALIDATED = 3, + CRT_EXPIRED = 4 +}; +enum ENUM_CRT_QUALITY { + CRT_QUALITY_A = 0, + CRT_QUALITY_B = 1, + CRT_QUALITY_C = 2, + CRT_QUALITY_D = 3 +}; +// TBS ENUMERATIONS +enum ENUM_TBS_TYPE { + TBS_BULLISH = 1, + TBS_BEARISH = -1, + TBS_NONE = 0 +}; +enum ENUM_TBS_STATUS { + TBS_PENDING = 0, + TBS_TRIGGERED = 1, + TBS_ACTIVE = 2, + TBS_COMPLETED = 3, + TBS_INVALIDATED = 4, + TBS_EXPIRED = 5 +}; +enum ENUM_TBS_QUALITY { + TBS_QUALITY_PREMIUM = 0, + TBS_QUALITY_HIGH = 1, + TBS_QUALITY_MEDIUM = 2, + TBS_QUALITY_LOW = 3 +}; +// AMD ENUMERATIONS +enum ENUM_AMD_PHASE { + AMD_NONE = 0, + AMD_ACCUMULATION = 1, + AMD_MANIPULATION = 2, + AMD_DISTRIBUTION = 3, + AMD_REACCUMULATION = 4, + AMD_REDISTRIBUTION = 5 +}; +enum ENUM_AMD_STAGE { + AMD_STAGE_EARLY = 0, + AMD_STAGE_MIDDLE = 1, + AMD_STAGE_LATE = 2, + AMD_STAGE_TRANSITION = 3 +}; +enum ENUM_AMD_CONFIDENCE { + AMD_CONF_HIGH = 0, + AMD_CONF_MEDIUM = 1, + AMD_CONF_LOW = 2, + AMD_CONF_UNCERTAIN = 3 +}; +// JUDAS SWING ENUMERATIONS +enum ENUM_JUDAS_TYPE { + JUDAS_BULLISH = 1, + JUDAS_BEARISH = -1, + JUDAS_NONE = 0 +}; +enum ENUM_JUDAS_STATUS { + JUDAS_FORMING = 0, + JUDAS_CONFIRMED = 1, + JUDAS_TRADING = 2, + JUDAS_COMPLETED = 3, + JUDAS_FAILED = 4 +}; +enum ENUM_JUDAS_SESSION { + JUDAS_LONDON = 0, + JUDAS_NY = 1, + JUDAS_ASIAN = 2 +}; +// MARKET REGIME ENUMERATIONS +enum ENUM_MARKET_REGIME { + REGIME_STRONG_TREND_UP = 0, + REGIME_TREND_UP = 1, + REGIME_WEAK_TREND_UP = 2, + REGIME_RANGING_TIGHT = 3, + REGIME_RANGING_WIDE = 4, + REGIME_WEAK_TREND_DOWN = 5, + REGIME_TREND_DOWN = 6, + REGIME_STRONG_TREND_DOWN = 7, + REGIME_VOLATILE = 8, + REGIME_BREAKOUT = 9, + REGIME_CHOPPY = 10, + REGIME_UNKNOWN = 11, + REGIME_TRENDING = 12, + REGIME_RANGING = 13 +}; +enum ENUM_REGIME_TRANSITION { + TRANSITION_NONE = 0, + TRANSITION_TREND_TO_RANGE = 1, + TRANSITION_RANGE_TO_TREND = 2, + TRANSITION_REVERSAL = 3, + TRANSITION_EXPANSION = 4, + TRANSITION_CONTRACTION = 5 +}; +// NEWS FILTER ENUMERATIONS +enum ENUM_NEWS_IMPACT { + NEWS_NONE = 0, + NEWS_LOW = 1, + NEWS_MEDIUM = 2, + NEWS_HIGH = 3, + NEWS_HOLIDAY = 4 +}; +enum ENUM_NEWS_CURRENCY { + NEWS_USD = 0, + NEWS_EUR = 1, + NEWS_GBP = 2, + NEWS_JPY = 3, + NEWS_CHF = 4, + NEWS_AUD = 5, + NEWS_CAD = 6, + NEWS_NZD = 7, + NEWS_CNY = 8, + NEWS_ALL = 9 +}; +// CORRELATION ENUMERATIONS +enum ENUM_CORRELATION_LEVEL { + CORR_STRONG_POSITIVE = 0, + CORR_POSITIVE = 1, + CORR_WEAK_POSITIVE = 2, + CORR_NEUTRAL = 3, + CORR_WEAK_NEGATIVE = 4, + CORR_NEGATIVE = 5, + CORR_STRONG_NEGATIVE = 6, + CORR_NONE = 7, + CORR_LOW = 8, + CORR_MEDIUM = 9, + CORR_HIGH = 10, + CORR_INVERSE = 11 +}; +enum ENUM_EXPOSURE_LEVEL { + EXPOSURE_NONE = 0, + EXPOSURE_LOW = 1, + EXPOSURE_MEDIUM = 2, + EXPOSURE_HIGH = 3, + EXPOSURE_OVEREXPOSED = 4 +}; +// TIME ANALYSIS ENUMERATIONS +enum ENUM_HOUR_QUALITY { + HOUR_EXCELLENT = 0, + HOUR_GOOD = 1, + HOUR_AVERAGE = 2, + HOUR_POOR = 3, + HOUR_AVOID = 4 +}; +enum ENUM_DAY_QUALITY { + DAY_EXCELLENT = 0, + DAY_GOOD = 1, + DAY_AVERAGE = 2, + DAY_POOR = 3, + DAY_AVOID = 4 +}; +enum ENUM_ENTRY_QUALITY { + QUALITY_A_PLUS = 0, + QUALITY_A = 1, + QUALITY_B = 2, + QUALITY_C = 3, + QUALITY_D = 4, + QUALITY_F = 5, + QUALITY_REJECT = 6 +}; +// FIBONACCI ENUMERATIONS +enum ENUM_FIB_MODE { + FIB_MODE_AUTO, + FIB_MODE_MANUAL, + FIB_MODE_STRUCTURE +}; +enum ENUM_FIB_STYLE { + FIB_STYLE_FULL, + FIB_STYLE_ICT, + FIB_STYLE_CUSTOM +}; +enum ENUM_FIB_EXTENSION { + FIB_EXT_NONE, + FIB_EXT_STANDARD, + FIB_EXT_FULL +}; +// COST ANALYSIS ENUMERATIONS +enum ENUM_COST_MODE { + COST_MODE_FIXED, + COST_MODE_PERCENTAGE, + COST_MODE_PER_LOT +}; +enum ENUM_SLIPPAGE_MODE { + SLIP_MODE_FIXED, + SLIP_MODE_PERCENTAGE, + SLIP_MODE_DYNAMIC +}; +//+==================================================================+ +//| CHART PATTERN ENUMERATIONS | +//+==================================================================+ +// CHART PATTERN TYPES +enum ENUM_CHART_PATTERN_TYPE { + CHART_PATTERN_NONE = 0, + // Reversal Patterns + CHART_PATTERN_HEAD_SHOULDERS_TOP, + CHART_PATTERN_HEAD_SHOULDERS_BOTTOM, + CHART_PATTERN_DOUBLE_TOP, + CHART_PATTERN_DOUBLE_BOTTOM, + CHART_PATTERN_TRIPLE_TOP, + CHART_PATTERN_TRIPLE_BOTTOM, + CHART_PATTERN_DIAMOND_TOP, + CHART_PATTERN_DIAMOND_BOTTOM, + CHART_PATTERN_V_TOP, + CHART_PATTERN_V_BOTTOM, + CHART_PATTERN_RISING_WEDGE, + CHART_PATTERN_FALLING_WEDGE, + // Continuation Patterns + CHART_PATTERN_BULL_FLAG, + CHART_PATTERN_BEAR_FLAG, + CHART_PATTERN_BULL_PENNANT, + CHART_PATTERN_BEAR_PENNANT, + CHART_PATTERN_ASCENDING_TRIANGLE, + CHART_PATTERN_DESCENDING_TRIANGLE, + CHART_PATTERN_SYMMETRICAL_TRIANGLE, + CHART_PATTERN_RECTANGLE +}; +// CHART PATTERN STATUS +enum ENUM_CHART_PATTERN_STATUS { + PATTERN_FORMING = 0, + PATTERN_CONFIRMED = 1, + PATTERN_TRIGGERED = 2, + PATTERN_COMPLETED = 3, + PATTERN_FAILED = 4, + PATTERN_EXPIRED = 5 +}; +// CHART PATTERN QUALITY +enum ENUM_CHART_PATTERN_QUALITY { + PATTERN_QUALITY_PREMIUM = 0, + PATTERN_QUALITY_HIGH = 1, + PATTERN_QUALITY_MEDIUM = 2, + PATTERN_QUALITY_LOW = 3 +}; +// CANDLESTICK PATTERN TYPES (Extended) +enum ENUM_CANDLE_PATTERN_TYPE { + CANDLE_NONE = 0, + // Single Candle Patterns + CANDLE_DOJI, + CANDLE_DOJI_DRAGONFLY, + CANDLE_DOJI_GRAVESTONE, + CANDLE_DOJI_LONG_LEGGED, + CANDLE_HAMMER, + CANDLE_INVERTED_HAMMER, + CANDLE_HANGING_MAN, + CANDLE_SHOOTING_STAR, + CANDLE_SPINNING_TOP, + CANDLE_MARUBOZU_BULL, + CANDLE_MARUBOZU_BEAR, + // Two Candle Patterns + CANDLE_ENGULFING_BULL, + CANDLE_ENGULFING_BEAR, + CANDLE_HARAMI_BULL, + CANDLE_HARAMI_BEAR, + CANDLE_HARAMI_CROSS_BULL, + CANDLE_HARAMI_CROSS_BEAR, + CANDLE_PIERCING_LINE, + CANDLE_DARK_CLOUD_COVER, + CANDLE_TWEEZER_TOP, + CANDLE_TWEEZER_BOTTOM, + // Three Candle Patterns + CANDLE_MORNING_STAR, + CANDLE_EVENING_STAR, + CANDLE_MORNING_DOJI_STAR, + CANDLE_EVENING_DOJI_STAR, + CANDLE_THREE_WHITE_SOLDIERS, + CANDLE_THREE_BLACK_CROWS, + CANDLE_THREE_INSIDE_UP, + CANDLE_THREE_INSIDE_DOWN, + CANDLE_THREE_OUTSIDE_UP, + CANDLE_THREE_OUTSIDE_DOWN, + CANDLE_ABANDONED_BABY_BULL, + CANDLE_ABANDONED_BABY_BEAR +}; +//+==================================================================+ +//| DIVERGENCE ENUMERATIONS | +//+==================================================================+ +enum ENUM_DIVERGENCE_TYPE +{ + DIV_NONE = 0, + DIV_REGULAR_BULLISH = 1, // Price LL, RSI HL (Reversal) + DIV_REGULAR_BEARISH = -1, // Price HH, RSI LH (Reversal) + DIV_HIDDEN_BULLISH = 2, // Price HL, RSI LL (Continuation) + DIV_HIDDEN_BEARISH = -2 // Price LH, RSI HH (Continuation) +}; +enum ENUM_DIVERGENCE_INDICATOR +{ + DIV_IND_RSI = 0, + DIV_IND_MACD = 1, + DIV_IND_STOCH = 2, + DIV_IND_CCI = 3 +}; +enum ENUM_DIVERGENCE_STRENGTH +{ + DIV_WEAK = 1, + DIV_MODERATE = 2, + DIV_STRONG = 3 +}; +//+==================================================================+ +//| TRENDLINE ENUMERATIONS | +//+==================================================================+ +enum ENUM_TRENDLINE_TYPE +{ + TL_NONE = 0, + TL_SUPPORT = 1, // Bullish - connecting lows + TL_RESISTANCE = -1 // Bearish - connecting highs +}; +enum ENUM_TRENDLINE_STRENGTH +{ + TL_WEAK = 1, + TL_MODERATE = 2, + TL_STRONG = 3, + TL_VERY_STRONG = 4 +}; +enum ENUM_TRENDLINE_STATUS +{ + TL_STATUS_ACTIVE = 0, + TL_STATUS_TESTED = 1, + TL_STATUS_BROKEN = 2, + TL_STATUS_RETESTING = 3, + TL_STATUS_EXPIRED = 4 +}; +// [OK] ΠΡΟΣΘΗΚΗ - ΝΕΟΣ ENUM: +enum ENUM_TRADING_STYLE { + STYLE_CUSTOM, // Use Custom Settings Below + STYLE_CONSERVATIVE, // Conservative (Score>=75, EV>=0.25, R:R>=2.0) + STYLE_MODERATE, // Moderate (Score>=65, EV>=0.18, R:R>=1.8) + STYLE_AGGRESSIVE // Aggressive (Score>=55, EV>=0.15, R:R>=1.6) +}; +//+------------------------------------------------------------------+ +//| EA-SPECIFIC INPUT PARAMETERS (Added for Trading) | +//| [OK] v6.40 UPDATED: Added Entry Filters, MTF, Debug sections | +//+------------------------------------------------------------------+ +input group "================ EA SETTINGS ================" +input bool EA_Enabled = true; // Enable Automated Trading +input int EA_MagicNumber = 123450; // EA Magic Number +input string EA_TradeComment = "ICT_EA"; // Trade Comment +// =================================================================== +// * v7.4 UNIFIED RISK MANAGEMENT +// ONE group controls EVERYTHING - position sizing, trade limits, +// and account protection. No pair overrides. No duplicates. +// Works in both Indicator and EA mode. +// =================================================================== +input group "======== [SHIELD] RISK & ACCOUNT PROTECTION ========" +// --- Position Sizing --- +input double EA_RiskPercent = 1.2; // * CAL: 1.20% — ceiling=1.20×1.50=1.80% max effective. Παλιό 1.80%×A+(1.40)×Kelly(1.68)=2.70%→4.41 lots→DD. Νέο: worst case 1.80%→~2.87 lots. +input double EA_FixedLotSize = 0.0; // Fixed Lot Size (0=use Risk%) +input double EA_MinRR = 1.2; // Minimum Risk:Reward Ratio — H1 EURUSD Jan 2026 setups at 1.20-1.35 +// --- Trade Limits (GLOBAL - all pairs) --- +input int EA_MaxDailyTrades = 5; // * v9.03 FTMO: 2000->5 (unlimited trades = uncontrolled DD. 5/day is safe for FTMO) +input int EA_MaxConsecLossHalt = 4; // * FIX#398: Halt new entries after N consecutive losses (resets daily). 0=disabled. +input int EA_MaxOpenTrades = 1; // * v9.03: Max simultaneous same-direction trades. Risk cap: total risk <= daily DD limit +input ENUM_HEDGE_MODE EA_HedgeMode = HEDGE_NO_HEDGE; // * v9.03: NO_HEDGE=block opposite (FTMO safe), CLOSE_REVERSE=flip position, ALLOW=both open +input bool EA_AllowBuy = true; // Allow BUY trades (false = block all longs, e.g. XAUUSD SELL-only) +input bool EA_AllowSell = true; // Allow SELL trades (false = block all shorts, e.g. XAUUSD BUY-only) +// --- FIX#196: D1 CHoCH Hard Gate --- +input bool EA_D1CHoCHGate = false; // D1 CHoCH Hard Gate: only trade WITH Daily direction (ICT HTF bias) — disabled for H1 calibration +input bool EA_D1CHoCH_BlockNeutral = false; // * v10.24 FIX#310b: back to false (global default). M5 uses g_workingBlockNeutral=true via TF_CAT_SCALP. H4/H1/M15 keep false — OBs valid in ranging months. +input int EA_D1CHoCH_SwingBars = 5; // * v9.49 FIX#198: 10→5 — faster D1 direction detection (10-bar lookback = 10 day lag before first signal) +// --- Account Protection (master switch) --- +input bool EA_EnableDrawdownProtection = true; // Enable Account Protection +input double EA_MaxDailyDrawdownPercent = 4.5; // * v9.14 FIX#31: 3.8->8.0% -- 3.8% was blocking all trades after 3-4 SL hits at 1% risk. FIX#15 set 8% in code but input was never updated. For FTMO 5% daily limit: set to 4.5%. +input double EA_MaxWeeklyDrawdownPercent = 8.0; // * v9.03 FTMO: 15.0->8.0 (FTMO limit=10%, 8%=2% safety buffer. If Risk=5% restore to 15.0!) +input double EA_MaxTotalDrawdownPercent = 9.9; // Max Total Drawdown (%) — * FIX#416: 9.5→9.9. Root: 9.5% fired after 2 months on legitimate compound growth (peak=$26829 from $25k). FTMO allows 10%; 9.9% gives real buffer without violation. +input bool EA_StopOnDrawdown = true; // * v9.09 FIX#16: true->false -- pause signals instead of ExpertRemove() (which kills backtest entirely) +input bool EA_EmailOnDrawdown = true; // Email Alert on Drawdown +input group "================ EA STOP LOSS & TAKE PROFIT ================" +input double EA_StopLossATR = 1.8; // Stop Loss (x ATR) [v7.5b: 1.5->1.2 tighter SL for M5] +input double EA_TP1_ATR = 2.8; // * v9.02: reverted 1.3->2.0 (proper TP once SL floor fixed: SL~=9pips, TP1=18pips, R:R=2.0) +input double EA_TP2_ATR = 4.0; // * v9.02: reverted 2.0->2.8 (correct after SL floor fix) +input double EA_TP3_ATR = 5.5; // * v9.03: 3.0->3.8 (runner TP needs distance for R:R. SL=1.2, TP1=2.0, TP2=2.8, TP3=3.8) +input double EA_TP1_Percent = 25.0; // * v10.24 FIX#310a: back to 25 (default). M5 uses g_workingTP1_Pct=60 via TF_CAT_SCALP. H4/H1/M15 keep 25 (H4 proven TP3 reachable = runner needed). +input double EA_TP2_Percent = 25.0; // * v10.24 FIX#310a: unchanged — 25% at TP2 (all TFs) +input double EA_TP3_Percent = 50.0; // * v10.24 FIX#310a: back to 50. M5 uses g_workingTP3_Pct=15 (0 TP3 hits on M5). H4/H1 keep 50 (TP3 reachable on longer TFs). +input bool EA_UseMultipleTP = true; // * v9.03: false->true (50% close at TP1 locks profit, 30% TP2, 20% runner for TP3) +input bool EA_MoveToBreakEven = true; // Move to BE after TP1 +input double EA_BreakEven_RR = 0.5; // * v9.49 FIX#200: 0.3→0.5R — was moving SL to BE at 0.3R (too early), causing premature exits before momentum developed. FIX#100 perTrade floor already raises this further per-trade. +input double EA_BE_Buffer_Pips = 2.0; // * v9.04 FIX#9: 5.0->2.0 (smaller buffer = less profit given back) +input double EA_TP1_BE_Threshold = 1.5; // * v9.04 FIX#9: 0.9->1.5 (let price reach TP1 area before protecting) +input double EA_TP2_BE_Threshold = 2.5; // * v7.5c: 1.8->2.5 (secure profit closer to TP2) +input double EA_BE_Ranging_RR = 1.5; // * v7.5c: 1.0->1.5 (ranging still needs room to breathe) +input double EA_BE_Volatile_RR = 1.8; // * v9.04 FIX#9: 1.6->1.8 (volatile needs even more room) +input bool EA_TrailAfter_TP2 = false; // Enable Trailing After TP2 +input group "================ EA TRAILING STOP ================" +input bool EA_UseTrailing = true; // Use Trailing Stop +input double EA_TrailStart_RR = 0.3; // * v9.29 FIX#98b: 2.0->0.3R | H4: trail starts at 0.3R favorable excursion +input double EA_TrailStop_ATR = 1.0; // Trail Distance (x ATR) (legacy fallback) +input double EA_Trail_Volatile_Start = 2.2; // * v6.39: Trail start in volatile (was hardcoded 2.5) +input double EA_Trail_Volatile_Dist = 0.9; // * v6.39: Trail distance in volatile (was hardcoded 1.0) +input double EA_Trail_Trending_Start = 1.8; // * v6.39: Trail start in trending (was hardcoded 2.0) +input double EA_Trail_Trending_Dist = 1.1; // * v6.39: Trail distance in trending (was hardcoded 1.2) +// =================================================================== +// * v9.16 UNIVERSAL PER-TRANCHE TRAILING (not AutoOpt controlled!) +// TP1: Fixed TP + aggressive trail from 0.3R -- locks early profit +// TP2: NO fixed TP, trail only -- medium distance for good swing capture +// TP3: NO fixed TP, trail only -- loose distance for maximum runner +// Cascading SL: TP1 hit -> SL floor = TP1 price -> TP2 close -> SL floor = TP2 close +// =================================================================== +input double EA_Trail_Activation_RR = 0.3; // * v9.29 FIX#98b: 1.2->0.3R | H4: trail activation at 0.3R (matches favorable excursion seen in losing trades) +input double EA_Trail_TP1_ATR = 1.5; // * v9.23 FIX#66: 0.8->1.5 (tight 0.8ATR killed runners in M15 noise. Avg win was $29 = 3 pips. Need wider buffer.) +input double EA_Trail_TP2_ATR = 1.8; // * v9.23 FIX#66: 1.3->1.8 (TP2 needs room to run after TP1 cascade) +input double EA_Trail_TP3_ATR = 2.5; // * v9.23 FIX#66: 2.0->2.5 (runner needs maximum room. 0 TP3 hits in 64 trades!) +input bool EA_Trail_CascadingSL = true; // * v9.16: TP1 hit -> SL=TP1 price, TP2 close -> SL=TP2 close +// =========================================================================== +// * FULL HYBRID SYSTEM - Regime-Adaptive Position Management +// Auto-switches between Option D (Safe) and Option E (Aggressive) +// based on real-time market regime detection +// =========================================================================== +input group "================ EA FULL HYBRID SYSTEM ================" +input bool EA_UseFullHybrid = true; // Enable Full Hybrid (Auto D/E switching) +input double EA_Hybrid_TrendThreshold = 0.6; // Min regime strength for trailing (0.0-1.0) +input double EA_Hybrid_TrailDist_Trend = 1.0; // Trailing distance in trending (x ATR) +input double EA_Hybrid_TrailDist_Breakout = 0.8; // Trailing distance in breakout (x ATR) +input bool EA_Hybrid_AllowModeSwitch = true; // Allow mode switch during trade +input bool EA_Hybrid_UseADX = true; // Use ADX for regime confirmation +input double EA_Hybrid_MinADX = 25.0; // Minimum ADX for trending (if enabled) +// * v9.16 FIX#40: Profit-Guaranteed Progressive Trailing Stop +input bool EA_Trail_GuaranteeProfit = true; // * Guarantee profit on trail (never trail below entry) +input double EA_Trail_BE_Trigger = 1.0; // * v9.23 FIX#67: 0.8->1.0 (floor now also prevents negative locks at activation) +input double EA_Trail_BE_LockPips = 2.0; // * Pips above entry at breakeven +input double EA_Trail_Lock1_RR = 1.5; // * v9.23 FIX#67: 1.0->1.5 (at 1.0R locked +0.3R=$9 while SL=-$84. Need more profit before locking) +input double EA_Trail_Lock2_RR = 2.5; // * v9.23 FIX#67: 2.0->2.5 (lock +0.7R at real profit milestone) +input double EA_Trail_Lock3_RR = 3.5; // * v9.23 FIX#67: 3.0->3.5 (lock +1.2R only when well into runner territory) +input double EA_Trail_MinProfitPips = 5.0; // * v9.49 FIX#200: 1.0→5.0 — 1.0 pip buffer = trail moves SL to entry+1p → any spread/noise = loss. 5.0 ensures real profit locked before trail is active. +//| [OK] v6.40 NEW: EA POSITION MANAGEMENT | +//| Proximity close & RSI momentum fading settings | +//+------------------------------------------------------------------+ +input group "================ EA POSITION MANAGEMENT ================" +input double EA_Proximity_Close_Start = 0.97; // Proximity Close Start (%) +input double EA_Proximity_Force_Close = 0.99; // Force Close at (%) +input int EA_RSI_Overbought = 78; // RSI Overbought Level +input int EA_RSI_Oversold = 22; // RSI Oversold Level +// * v7.5c: SMART EXIT -- Reversal Detection & Early Close +input bool EA_SmartExit_Enable = true; // Smart Exit: Enable reversal detection +input double EA_SmartExit_MinProfit_RR = 0.3; // * v9.29 FIX#98b: 1.1->0.3R | H4: favorable excursion max 0.3R on losing trades -- must fire here on reversal signals +input int EA_SmartExit_Signals = 3; // Smart Exit: Reversal signals needed (2-5) +//+------------------------------------------------------------------+ +//| [OK] v6.40 NEW: EA STRUCTURE CLOSE SETTINGS | +//| Strength filters and tranche-specific thresholds | +//+------------------------------------------------------------------+ +input group "================ EA STRUCTURE CLOSE SETTINGS ================" +input double EA_OB_MinStrength_StructClose = 0.65; // OB Min Strength for Structure Close +input ENUM_FVG_QUALITY EA_FVG_MinQuality_StructClose = FVG_QUALITY_HIGH; // FVG Min Quality for Structure Close +input double EA_Breaker_MinStrength_StructClose = 0.5; // Breaker Min Strength for Structure Close +input double EA_TP2_Structure_MinRR = 1.5; // TP2 Structure Close Min R:R [v7.5b: 2.2->1.5] +input double EA_TP3_Structure_MinStrength = 0.85; // TP3 Structure Min Strength (0.0-1.0) +input double EA_TP3_Runner_BaseThreshold = 2.5; // TP3 Runner Base Threshold (R:R) +input double EA_Single_Position_BaseThreshold = 1.2; // Single Position Base Threshold (R:R) +//+------------------------------------------------------------------+ +//| [OK] v6.40 NEW: EA ENTRY FILTERS & SCORING | +//| Signal quality and confirmation requirements | +//+------------------------------------------------------------------+ +input group "================ EA ENTRY FILTERS & SCORING ================" +input double EA_MinEntryScore = 44.0; // Min raw signal score (0-85). H1 EURUSD calibrated: OB/FVG score 44-49/85. +input bool EA_RequireKillzone = false; // Require Killzone Entry +input bool EA_RequireTrend = false; // Require HTF Trend Alignment +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| EA GLOBAL OBJECTS | +//+------------------------------------------------------------------+ +#ifdef COMPILE_AS_EA +CTrade g_ea_trade; +CPositionInfo g_ea_position; +CAccountInfo g_ea_account; +#endif +#ifdef COMPILE_AS_EA +CSymbolInfo g_ea_symbol; +#endif +struct EA_Signal { + bool isValid; + bool isBullish; + double entryPrice; + double stopLoss; + double tp1, tp2, tp3; + int score; + string type; + bool isCounterTrend; // * v9.31 FIX#104c: set by EvaluateSmartEntry + // * v9.41 FIX#177: Zone reference passed from winning candidate to MultiTPEntry + double zoneTop; + double zoneBottom; + string zoneType; +}; +EA_Signal g_ea_signal; +struct EA_Stats { + datetime date; + int trades; + double risk; +}; +EA_Stats g_ea_stats; +// =========================================================================== +// * FULL HYBRID TRACKING - Regime-Adaptive Position Management +// Extended tracking for automatic strategy switching (Option D <-> Option E) +// =========================================================================== +// Hybrid management modes +enum HYBRID_MODE { + HYBRID_MODE_SAFE, // Using Option D (Risk Ladder - fixed locks) + HYBRID_MODE_AGGRESSIVE // Using Option E (Aggressive Trailing) +}; +// Enhanced tracker structure for Full Hybrid system +struct HybridTracker { + // Basic tracking (same as Risk Ladder) + string baseComment; // Trade identifier (e.g., "ICT_EA_FVG") + datetime tp1CloseTime; // Timestamp when TP1 closed (0 = not closed) + datetime tp2CloseTime; // Timestamp when TP2 closed (0 = not closed) + bool tp1Processed; // TP1 stage applied (lock +1R or activate trailing) + bool tp2Processed; // TP2 stage applied (lock +2R or continue trailing) + double entryPrice; // Original entry price + double originalSL; // Original stop loss price + // Hybrid-specific tracking + HYBRID_MODE currentMode; // Current management mode (SAFE or AGGRESSIVE) + ENUM_MARKET_REGIME regimeAtTP1;// Market regime when TP1 closed + ENUM_MARKET_REGIME regimeAtTP2;// Market regime when TP2 closed + double regimeStrengthTP1; // Regime strength at TP1 (0.0-1.0) + double regimeStrengthTP2; // Regime strength at TP2 (0.0-1.0) + bool trailingActive; // Is trailing stop currently active + datetime lastModeCheck; // Last regime check timestamp (for mode switching) +}; +// Global arrays for tracking active trades +#define MAX_HYBRID_TRACKERS 100 +HybridTracker g_hybridTrackers[MAX_HYBRID_TRACKERS]; +int g_hybridTrackerCount = 0; +//+------------------------------------------------------------------+ +//| * SMART TECHNIQUE SELECTOR v7.0 - Structures & Enums | +//+------------------------------------------------------------------+ +// Entry technique identifiers +enum ENUM_ENTRY_TECHNIQUE { + TECH_FVG = 0, + TECH_OB = 1, + TECH_TBS = 2, + TECH_CRT = 3, + TECH_LIQ_SWEEP = 4, + TECH_JUDAS = 5, + TECH_SILVER_BULLET = 6, + TECH_BREAKER = 7, + TECH_OTE = 8, + TECH_BOS_RETEST = 9, + TECH_TREND_CONT = 10, // * v7.8 NEW: Trend Continuation (momentum pullback into EMA) + TECH_MEAN_REV = 11, // * FIX#373: NEW: Mean Reversion (range boundary + RSI extreme + liquidity sweep) + TECH_TOTAL = 12 +}; +enum ENUM_CONFIRMATION_SOURCE { + CONF_STRUCTURE = 0, + CONF_FVG = 1, + CONF_OB = 2, + CONF_KILLZONE = 3, + CONF_HTF_TREND = 4, + CONF_PREMIUM_DISC = 5, + CONF_VSA = 6, + CONF_DIVERGENCE = 7, + CONF_TRENDLINE = 8, + CONF_AMD_PHASE = 9, + CONF_RSI = 10, + CONF_ML = 11, + CONF_REGIME = 12, + CONF_LIQ_SWEEP = 13, // * FIX-A: Liquidity sweep confirmation — swept SSL near BUY / BSL near SELL + CONF_TOTAL = 14 +}; +enum ENUM_TF_STRATEGY { + TF_SCALPING = 0, + TF_INTRADAY = 1, + TF_SWING = 2, + TF_POSITION = 3 +}; + +// ── FIX#502: SCENARIO SYSTEM ENUMS ─────────────────────────────────── +// Must be defined here (before CandidateSignal struct that uses them). +// Maps ENUM_MARKET_PHASE → concrete trading scenario with entry/SL/TP. +// Populated by DeriveScenarioProfile() after ComputeMarketContext(). +enum ENUM_SCENARIO { + SCEN_UNKNOWN = 0, // Unclassified — no entry + SCEN_TREND_PULLBACK = 1, // S3/S4: Trend → Pullback → Continue + SCEN_BREAKOUT_RETEST = 2, // S1/S2: Consolidation → Breakout → Retest + SCEN_REVERSAL = 3, // S7/S8: Trend → Reversal → New trend + SCEN_RANGE_FADE = 4, // Ranging → Fade at boundary + SCEN_BULL_FLAG = 5, // S5: Uptrend → Flag → Breakout up + SCEN_BEAR_FLAG = 6, // S6: Downtrend → Flag → Breakout down + SCEN_FAKEOUT = 7, // S9: Range → False breakout → Fade + SCEN_FAILED_BREAKOUT = 8, // S10: Breakout → Retest fails → Reversal + SCEN_COMPRESSION = 9, // S12: Range compression → Explosive breakout + SCEN_CHOPPY = 10 // S11: No trade — market has no structure +}; + +// Entry style — how to enter once scenario is known +enum ENUM_ENTRY_STYLE { + ES_RETEST = 0, // Wait for retest of broken level (safest) + ES_FIB = 1, // Enter at Fibonacci retracement zone (38-62%) + ES_BREAKOUT = 2, // Enter at breakout candle close + ES_FADE = 3 // Fade — enter opposite to price poke +}; + +// SL method — where to place stop loss based on scenario structure +enum ENUM_SL_METHOD { + SL_ATR = 0, // Generic: entry ± N×ATR (fallback) + SL_CONSOLIDATION = 1, // Below/above consolidation range ±1×ATR + SL_FIB78 = 2, // Below Fib 78.6% (pullback invalidation) + SL_WICK = 3, // Beyond fakeout wick ±0.5×ATR + SL_FLAG = 4, // Beyond flag boundary ±1×ATR + SL_STRUCTURE = 5 // Beyond last swing point (LH/HL) ±1×ATR +}; + +// TP method — where to take profit based on scenario geometry +enum ENUM_TP_METHOD { + TP_FIXED_RR = 0, // Generic: entry ± N×risk (fallback) + TP_PREV_HIGHLOW = 1, // Previous swing high/low (trend continuation) + TP_RANGE_PROJ = 2, // Range height projected (breakout/range fade) + TP_FLAGPOLE = 3, // Flagpole height added to breakout point + TP_FIB_EXT = 4 // Fibonacci extension 127.2% / 161.8% +}; +struct TechniquePriority { + ENUM_ENTRY_TECHNIQUE technique; + int priority; + double confidenceBoost; + double minQuality; + bool enabled; + string name; +}; +struct ConfirmationResult { + ENUM_CONFIRMATION_SOURCE source; + bool confirmed; + double strength; + int points; + string detail; +}; +struct ConfirmationCascade { + ConfirmationResult confirmations[14]; // * FIX-A: was 13, added CONF_LIQ_SWEEP + int totalConfirmed; + int totalChecked; + int totalPoints; + double avgStrength; + bool passesMinimum; + int requiredMin; + string summary; +}; +// =================================================================== +// v10.06 FIX#275: ActiveGateParams — SINGLE SOURCE OF TRUTH for all +// trade-entry thresholds. ComputeActiveGates() fills this once per +// bar (called at the end of ApplyPairTFProfile). Every gate reads +// from here — no hardcoded values anywhere else. +// Gates: MeetsMinimumEntryScore / SelectBestCandidate / +// EvaluateSmartEntry / EvaluateCascadeConfirmations +// =================================================================== +struct ActiveGateParams +{ + // --- Score thresholds (same value used by all 3 score gates) --- + double minScore; // The ONE threshold every gate checks against totalScore + double minScoreFloor; // Absolute lower bound (never below this) + + // --- Confirmation threshold (cascade gate) -------------------- + int minConfirmations; // EvaluateCascadeConfirmations requiredMin + + // --- Counter-structure (SmartEntry only) ---------------------- + int counterFloor; // minConfRequired floor when structure not aligned + int counterPenalty; // penalty added on top of minScore for CT (H4+=0) + + // --- HTF gate thresholds (SelectBestCandidate FIX#16c) -------- + // FIX#276: these were hardcoded 40/45 — now derived from minScore so + // H4 with minScore=36 uses 36/41 instead of always 40/45. + int htfMinScoreNeutral; // MTF=NEUTRAL (not confirmed): minScore floor + int htfMinScoreOpposed; // MTF directly opposed: minScore+small penalty + + // --- Weak regime penalty (SelectBestCandidate FIX#56) --------- + // FIX#276: H4 uses +5, M15 uses +15. Stored here for single-source. + int weakPenalty; // score penalty added in WEAK/CHOPPY regimes + double minEV; // * v10.09 FIX#282: Min EV (from pair table cfg.min_ev, 0=category default) + int divBlockThreshold; // * v10.09 FIX#280: Div strength needed to block (H4=80, M15=65) + int patternBlockCapHours; // * v10.09 FIX#281: Pattern block max hours (H4=24h, M15=72h) + + // --- Metadata (for logging) ----------------------------------- + string source; // e.g. "PairTable(36)" or "Fallback(36)" + bool computed; // false until ComputeActiveGates() has run +}; +ActiveGateParams g_gates; // global — filled once per bar, read by all gates + +struct CandidateSignal { + bool valid; + bool isBullish; + string type; + ENUM_ENTRY_TECHNIQUE technique; + double entryPrice; + double stopLoss; + double tp1, tp2, tp3; + int baseScore; + int confirmationScore; + int regimeBonus; + int pairBonus; + int totalScore; + double rr; + double winProbability; + double expectedValue; + ConfirmationCascade cascade; + int priority; + int sourceIndex; + // * v9.41 FIX#177: Zone reference — links entry reason to exit monitoring + // Without this, exit management was blind to the zone that generated the signal. + // Now zone boundaries travel with the trade from scanner → candidate → open position. + double zoneTop; // Upper boundary of source zone (FVG top, OB top, etc.) + double zoneBottom; // Lower boundary of source zone + string zoneType; // "FVG","OB","BREAKER","OTE","BOS","TBS","CRT","LIQ","JUDAS","SB","TC" + // ── FIX#502: Scenario fields — candidate carries scenario context ── + // Enables OpenTrade to compute SL/TP from scenario geometry, not just ATR. + // Set in AddCandidate() from g_scenarioProfile at candidate creation time. + ENUM_SCENARIO scenario; // Which scenario generated this candidate + ENUM_ENTRY_STYLE entryStyle; // Entry method for this candidate + ENUM_SL_METHOD slMethod; // SL placement method + ENUM_TP_METHOD tpMethod; // TP projection method +}; +struct SmartTechniqueState { + TechniquePriority priorities[12]; + ENUM_TF_STRATEGY tfStrategy; + ENUM_MARKET_REGIME activeRegime; + int candidateCount; + int selectedIndex; + string selectionReason; + datetime lastUpdate; +}; +// === SMART TECHNIQUE GLOBALS === +SmartTechniqueState g_techState; +CandidateSignal g_candidates[20]; +int g_candidateCount = 0; + +// * v9.41 FIX#177: Pending zone data — set before AddCandidate(), read inside it +double g_pendingZoneTop = 0; +double g_pendingZoneBottom = 0; +string g_pendingZoneType = ""; +int g_pendingMRScore = 0; // * FIX#373: MR module score, reset per candidate +//+------------------------------------------------------------------+ +//| END OF SECTION 1: ENUMERATIONS | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| SECTION 2: CONSTANTS | +//+------------------------------------------------------------------+ +// ARRAY LIMITS +#define MAX_FVG_ARRAY 500 +#define MAX_OB_ARRAY 200 +#define MAX_LIQ_ARRAY 100 +#define MAX_STRUCT_ARRAY 200 +#define MAX_OTE_ARRAY 50 +#define MAX_BREAKER_ARRAY 100 +#define MAX_MITIGATION_ARRAY 100 +#define MAX_OFI_ARRAY 100 +#define MAX_VP_ARRAY 50 +#define MAX_MM_ARRAY 50 +#define MAX_ML_ARRAY 100 +#define MAX_SIGNAL_ARRAY 50 +#define MAX_ML_PREDICTIONS 100 +#define MAX_TRADE_HISTORY 1000 +#define MAX_BACKTEST_TRADES 10000 +#define MAX_STRATEGY_STATS 15 +#define MAX_DAILY_STATS 365 +// NEURAL NETWORK LIMITS +#define NN_MAX_WEIGHTS 256 +#define NN_MAX_NEURONS 128 +#define NN_MAX_LAYERS 6 +#define NN_MAX_FEATURES 64 +// NEURAL NETWORK ARCHITECTURE +#define NN_INPUT_FEATURES 56 // * v10.29 FIX#316: 42→56 (+14 ICT features: OB/FVG/regime/MTF/session/structure depth) +#define NN_HIDDEN1_NODES 128 +#define NN_HIDDEN2_NODES 64 +#define NN_HIDDEN3_NODES 32 +#define NN_OUTPUT_NODES 3 +// NEURAL NETWORK HYPERPARAMETERS +#define NN_DEFAULT_LR 0.001 +#define NN_BETA1 0.9 +#define NN_BETA2 0.999 +#define NN_EPSILON 1e-8 +#define NN_WEIGHT_DECAY 0.0001 +#define NN_DROPOUT_RATE 0.3 +#define NN_GRADIENT_CLIP 1.0 +#define NN_BATCH_SIZE 32 +#define NN_MAX_EPOCHS 200 +#define NN_EARLY_STOP_PATIENCE 20 +#define NN_MIN_IMPROVEMENT 0.0001 +#define NN_LOOKBACK_BARS 500 +#define NN_PREDICTION_HORIZON 5 +#define NN_SEQUENCE_LENGTH 20 +// MARKET PHASES +#define PHASE_BPR "BPR" +#define PHASE_BISI "BISI" +#define PHASE_SIBI "SIBI" +#define PHASE_CHOCH "CHoCH" +#define PHASE_NONE "NONE" +#define PHASE_ACCUMULATION "ACCUMULATION" +#define PHASE_MARKUP "MARKUP" +#define PHASE_DISTRIBUTION "DISTRIBUTION" +#define PHASE_MARKDOWN "MARKDOWN" +// KILLZONE TIMES (UTC - Will be adjusted for DST) +#define KZ_ASIAN_START_HOUR 23 +#define KZ_ASIAN_START_MIN 0 +#define KZ_ASIAN_END_HOUR 8 +#define KZ_ASIAN_END_MIN 0 +#define KZ_LONDON_OPEN_START_HOUR 7 +#define KZ_LONDON_OPEN_START_MIN 0 +#define KZ_LONDON_OPEN_END_HOUR 10 +#define KZ_LONDON_OPEN_END_MIN 0 +#define KZ_LONDON_CLOSE_START_HOUR 15 +#define KZ_LONDON_CLOSE_START_MIN 0 +#define KZ_LONDON_CLOSE_END_HOUR 17 +#define KZ_LONDON_CLOSE_END_MIN 0 +#define KZ_NY_OPEN_START_HOUR 12 +#define KZ_NY_OPEN_START_MIN 0 +#define KZ_NY_OPEN_END_HOUR 15 +#define KZ_NY_OPEN_END_MIN 0 +#define KZ_NY_LUNCH_START_HOUR 16 +#define KZ_NY_LUNCH_START_MIN 0 +#define KZ_NY_LUNCH_END_HOUR 18 +#define KZ_NY_LUNCH_END_MIN 0 +#define KZ_NY_CLOSE_START_HOUR 19 +#define KZ_NY_CLOSE_START_MIN 0 +#define KZ_NY_CLOSE_END_HOUR 21 +#define KZ_NY_CLOSE_END_MIN 0 +#define KZ_SILVER_BULLET_LDN_START_HOUR 10 +#define KZ_SILVER_BULLET_LDN_START_MIN 0 +#define KZ_SILVER_BULLET_LDN_END_HOUR 11 +#define KZ_SILVER_BULLET_LDN_END_MIN 0 +#define KZ_SILVER_BULLET_NY_AM_START_HOUR 14 +#define KZ_SILVER_BULLET_NY_AM_START_MIN 0 +#define KZ_SILVER_BULLET_NY_AM_END_HOUR 15 +#define KZ_SILVER_BULLET_NY_AM_END_MIN 0 +#define KZ_SILVER_BULLET_NY_PM_START_HOUR 19 +#define KZ_SILVER_BULLET_NY_PM_START_MIN 0 +#define KZ_SILVER_BULLET_NY_PM_END_HOUR 20 +#define KZ_SILVER_BULLET_NY_PM_END_MIN 0 +// FILE NAMES +#define NN_MODEL_FILE "ICT_NeuralNet_v5.bin" +#define NN_WEIGHTS_FILE "ICT_NN_Weights_v5.bin" +#define NN_NORMALIZER_FILE "ICT_NN_Normalizer_v5.bin" +#define PERF_DATA_FILE "ICT_Performance_v5.dat" +#define TRADE_HISTORY_FILE "ICT_TradeHistory_v5.csv" +#define ML_HISTORY_FILE "ICT_ML_History_v5.csv" +#define BACKTEST_RESULTS_FILE "ICT_Backtest_v5.csv" +#define SETTINGS_FILE "ICT_Settings_v5.ini" +// PERSISTENCE CONSTANTS +#define PERSISTENCE_VERSION 1 +// TIME INTERVALS (Seconds) +// [v6.41] Now uses ObjectCleanupInterval input instead of hardcoded +#define CLEANUP_INTERVAL_SECONDS ObjectCleanupInterval +#define OBJECT_CLEANUP_INTERVAL_SECONDS 1800 +#define OBJECT_CLEANUP_INTERVAL 1800 +#define ARRAY_COMPACT_INTERVAL_SECONDS 7200 +#define ARRAY_COMPACT_INTERVAL 7200 +#define PERSISTENCE_SAVE_INTERVAL 300 +#define ML_UPDATE_INTERVAL 60 +// OTHER CONSTANTS +#define CONST_OBJECT_MAX_AGE 30 +#define CONST_ML_UPDATE_INTERVAL 60 +#define CONST_SIGNAL_EXPIRY_BARS 24 +#define CONST_ML_OPTIMIZE_BARS 1000 +// MULTI-TP CONSTANTS +#define MAX_MULTITP_ENTRIES 10 +#define TP_HIT_TOLERANCE 5 +//+------------------------------------------------------------------+ +//| SECTION 3: STRUCTURES | +//+------------------------------------------------------------------+ +// ICT CORE STRUCTURES +struct FVG_Struct { + long id; + datetime time; + int barIndex; + double high; + double low; + double top; + double bottom; + double ce; + double premium; + double discount; + ENUM_FVG_TYPE type; + ENUM_FVG_STATUS status; + ENUM_FVG_QUALITY quality; + bool isBullish; + bool isInverse; + bool active; // Added for EA compatibility + bool filled; + double sizePoints; + double sizePips; + double strength; + double fillPercentage; + int age; + double formationVolume; + double avgVolume; + double volumeRatio; + double impulsiveBody; + double displacement; + datetime lastUpdate; + datetime fillTime; + double fillPrice; + int touchCount; + string objNameRect; + string objNameLabel; + string objNameCE; +}; +struct OB_Struct { + long id; + datetime time; + double high; + double low; + double top; + double bottom; + bool isBullish; + int type; // 1 = Bullish, -1 = Bearish + bool mitigated; + int age; + double volume; + double strength; + bool isBreakerBlock; + int touchCount; + double bodySize; + double wickRatio; + string status; + bool active; // Is the OB currently active + datetime breakTime; // When was it broken + double breakPrice; // Price at break + datetime lastTouchTime; // Last mitigation touch time +}; +struct LIQ_Struct { + long id; + datetime time; + double price; + bool isBSL; + bool swept; + int age; + double strength; + int touches; + datetime sweepTime; + double sweepPrice; + bool isValid; +}; +struct STRUCT_Struct { + datetime time; + double price; + bool isHigh; + bool broken; + string type; + int age; + double swingStrength; + bool isValid; +}; +struct OTE_Struct { + datetime time; + bool isBullish; + double high; + double low; + double optimal; + double swingHigh; + double swingLow; + bool active; + bool touched; + int age; + double fibLevel; + bool isValid; + double level618; + double level786; + double level705; +}; +struct BREAKER_Struct { + long id; + datetime time; + double high; + double low; + double top; + double bottom; + bool isBullish; + bool active; + bool mitigated; + int touches; + int age; + double strength; +}; +struct MITIGATION_Struct { + long id; + datetime time; + double high; + double low; + double top; + double bottom; + bool isBullish; + bool active; + bool mitigated; + int touches; + int age; + double strength; +}; +struct OFI_Struct { + long id; + datetime time; + double price; + bool isBullish; + bool active; + double strength; + double volume; +}; +struct VP_Level { + double price; + string type; + double volume; + double strength; +}; +struct MM_Phase { + datetime time; + string phase; + double priceLevel; + double confidence; + bool isActive; +}; +// CRT STRUCTURE (CANDLE RANGE THEORY) +struct CRTSetup { + int id; + string objName; + datetime rangeTime; + int rangeBar; + double rangeHigh; + double rangeLow; + double rangeOpen; + double rangeClose; + double rangeSize; + double rangeATR; + double rangeSizeATR; + double projectionUp; + double projectionDown; + double projection50Up; + double projection50Down; + double extensionUp; + double extensionDown; + ENUM_CRT_TYPE type; + ENUM_CRT_STATUS status; + ENUM_CRT_QUALITY quality; + bool breakConfirmed; + bool retraceComplete; + datetime breakTime; + double breakPrice; + int breakBar; + double entryPrice; + double stopLoss; + double takeProfit1; + double takeProfit2; + double takeProfit3; + double riskReward; + bool hasOBConfluence; + bool hasFVGConfluence; + bool hasOTEConfluence; + bool hasSweepConfluence; + int confluenceCount; + bool active; + datetime createdTime; + datetime lastUpdate; + int barsActive; + double maxFavorable; + double maxAdverse; + bool useForTP; // [v6.42] Use CRT projection as TP +}; +struct TBSSetup { + int id; + string objName; + double liquidityLevel; + double falseBreakHigh; + double falseBreakLow; + double falseBreakLevel; + double sweepDistance; + datetime sweepTime; + int sweepBar; + double entryLevel; + double stopLoss; + double stopLevel; + double stopDistance; + double stopATR; + double tp1; + double tp2; + double tp3; + double tpLiquidity; + double tpOB; + ENUM_TBS_TYPE type; + ENUM_TBS_STATUS status; + ENUM_TBS_QUALITY quality; + bool entryTriggered; + bool confirmCandle; + datetime triggerTime; + double triggerPrice; + bool inKillzone; + bool withTrend; + bool hasOBNearby; + bool hasFVGNearby; + bool inOTE; + ENUM_AMD_PHASE amdPhase; + int score; + double winProbability; + double expectedValue; + bool active; + bool triggered; + datetime createdTime; + datetime expiryTime; + int barsActive; + int bar; + datetime time; +}; +// AMD STRUCTURE (ACCUMULATION-MANIPULATION-DISTRIBUTION) +struct AMDPhaseData { + ENUM_AMD_PHASE phase; + ENUM_AMD_PHASE previousPhase; + ENUM_AMD_STAGE stage; + ENUM_AMD_CONFIDENCE confidence; + double confidencePercent; + double accumHigh; + double accumLow; + double accumMid; + double accumRange; + datetime accumStart; + datetime accumEnd; + int accumBars; + int accumTouches; + double manipHigh; + double manipLow; + double sweepLevel; + double sweepDistance; + bool highSwept; + bool lowSwept; + datetime manipStart; + datetime manipEnd; + int manipBars; + double distStart; + double distCurrent; + double distTarget; + double distProgress; + datetime distStartTime; + int distBars; + ENUM_CRT_TYPE distDirection; + datetime phaseStartTime; + datetime lastTransition; + int barsInPhase; + int expectedBarsLeft; + double projectedTarget; + bool isAsianAccum; + bool isLondonManip; + bool isNYDist; + string objNameRange; + string objNameArrow; + string objNameLabel; + bool valid; + bool active; + datetime lastUpdate; + double rangeHigh; + double rangeLow; + datetime startTime; + datetime endTime; + int startBar; + int barCount; + string objName; +}; +// JUDAS STRUCTURE +struct JudasSwingData { + int id; + string objName; + ENUM_JUDAS_TYPE type; + ENUM_JUDAS_STATUS status; + ENUM_JUDAS_SESSION session; + double fakeSwingPrice; + double fakeSwingHigh; + double fakeSwingLow; + double swingPrice; + datetime fakeSwingTime; + int fakeSwingBar; + double fakeDistance; + double reversalPrice; + double reversalConfirm; + datetime reversalTime; + int reversalBar; + bool reversalConfirmed; + bool confirmed; + double entryPrice; + double stopLoss; + double stopDistance; + double tp1; + double tp2; + double tp3; + double tpMax; + double pdh; + double pdl; + double asianHigh; + double asianLow; + bool sweptPDHL; + bool sweptAsian; + bool hasOBSupport; + bool hasFVGSupport; + bool hasStructureConf; + bool hasDivergence; + int confluenceScore; + ENUM_ENTRY_QUALITY quality; + double winProbability; + double expectedValue; + int score; + bool active; + datetime createdTime; + datetime expiryTime; + datetime time; + int barsActive; + int bar; +}; +// SILVER BULLET STRUCTURE +struct SilverBulletSetup { + datetime windowStart; + datetime windowEnd; + int sbType; + double fvgTop; + double fvgBottom; + int direction; + string objName; + bool active; +}; +// MARKET REGIME STRUCTURE +struct MarketRegimeData { + ENUM_MARKET_REGIME regime; + ENUM_MARKET_REGIME previousRegime; + ENUM_MARKET_REGIME prevRegime; + ENUM_REGIME_TRANSITION transition; + double regimeConfidence; + double confidence; + double trendStrength; + double trendDirection; + double trendEfficiency; + double trendAge; + bool isTrending; + double rangeHigh; + double rangeLow; + double rangeWidth; + double rangeWidthATR; + double rangeAge; + bool isRanging; + double currentVolatility; + double avgVolatility; + double volatilityRatio; + double volatilityPercentile; + bool isVolExpanding; + bool isVolContracting; + bool isExpandingVol; + bool isContractingVol; + bool isHighVol; + double momentum; + double momentumMA; + bool momentumRising; + bool momentumFalling; + bool momentumDiverging; + double optimalRR; + double optimalSL; + double confidenceMultiplier; + int requiredConfirmations; + int requiredConfidence; + bool allowCounterTrend; + double positionSizeMultiplier; + bool shouldTrade; + bool preferLongs; + bool preferShorts; + bool useBreakouts; + bool useFades; + bool useMeanReversion; + string strategyHint; + datetime regimeStartTime; + datetime lastTransitionTime; + datetime lastChange; + int barsInRegime; + int avgRegimeDuration; + bool valid; + datetime lastUpdate; +}; +// WIN PROBABILITY STRUCTURE +struct WinProbabilityData { + double rawProbability; + double adjustedProbability; + double finalProbability; + double trendScore; + double structureScore; + double zoneScore; + double confluenceScore; + double timingScore; + double patternScore; + double regimeScore; + double volumeScore; + double historicalWinRate; + int historicalSamples; + double recentWinRate; + double similarSetupWinRate; + double newsAdjustment; + double correlationAdjust; + double timeOfDayAdjust; + double dayOfWeekAdjust; + double regimeAdjustment; + double streakAdjustment; + double confidence; + double standardError; + double lowerBound; + double upperBound; + datetime calculationTime; + string methodology; + int qualityTier; // * v9.16 FIX#47d: 1=LOW, 2=MEDIUM, 3=HIGH (from MinThreshold/HighThreshold) +}; +// EXPECTED VALUE STRUCTURE +struct ExpectedValueData { + double expectedValue; + double winProbability; + double avgWin; + double avgLoss; + double maxWin; + double maxLoss; + double evBestCase; + double evWorstCase; + double evMostLikely; + double kellyFraction; + double optimalRisk; + double maxRisk; + double breakEvenWinRate; + double expectedProfit; + double expectedLoss; + double netExpectancy; + double expectedPer100; + double variance; + double standardDev; + double sharpeRatio; + bool isPositiveEV; + bool isSignificant; + ENUM_ENTRY_QUALITY evQuality; + string recommendation; + datetime calculationTime; + int sampleSize; +}; +// POSITION SIZE STRUCTURE +struct PositionSizeData { + double accountBalance; + double accountEquity; + double baseRiskPercent; + double baseRiskAmount; + double baseLotSize; + double confidenceMultiplier; + double regimeMultiplier; + double streakMultiplier; + double correlationMultiplier; + double evMultiplier; + double drawdownMultiplier; + double adjustedRiskPercent; + double adjustedRiskAmount; + double finalLotSize; + double finalMultiplier; + double minLotSize; + double maxLotSize; + double lotStep; + double maxRiskPercent; + double minRiskPercent; + double stopLossPoints; + double stopLossPrice; + double pipValue; + double riskPerLot; + double kellyFraction; + double halfKelly; + double quarterKelly; + double recommendedKelly; + bool shouldScale; + int scaleInLevels; + datetime calculationTime; + string adjustmentLog; +}; +// Risk Management Structure +struct RiskManagement +{ + double dailyStartBalance; + double weeklyStartBalance; + double sessionStartBalance; + double currentDrawdownPercent; + double maxDrawdownToday; + double maxDrawdownWeek; + int dailyTrades; + int dailyWins; + int dailyLosses; + double dailyProfitLoss; + int weeklyTrades; + int weeklyWins; + int weeklyLosses; + double weeklyProfitLoss; + bool tradingAllowed; + bool drawdownLimitReached; + datetime lastResetDate; + datetime lastWeeklyReset; + string stopReason; +}; +// Spread Information +struct SpreadInfo +{ + double currentSpread; + double currentSpreadPips; + double averageSpread; + double maxSpreadAllowed; + bool isAcceptable; + datetime lastCheck; +}; +// Partial Close Tracking +struct PartialCloseInfo +{ + ulong ticket; + double initialLots; + double remainingLots; + bool tp1Hit; + bool tp2Hit; + bool tp3Hit; + bool movedToBreakeven; + bool trailingActive; + datetime openTime; +}; +// NEWS STRUCTURES +struct NewsEvent { + string title; + string currency; + ENUM_NEWS_CURRENCY currencyEnum; + ENUM_NEWS_IMPACT impact; + datetime eventTime; + datetime eventTimeLocal; + int minutesToEvent; + int minutesSinceEvent; + bool isUpcoming; + bool isPast; + bool isReleased; + string forecast; + string previous; + string actual; + bool hasActual; + bool shouldAvoid; + int avoidMinsBefore; + int avoidMinsAfter; + int minutesBefore; + int minutesAfter; + double expectedVolatility; + string eventName; + string source; + string url; + int id; +}; +struct NewsFilterData { + bool isNewsSafe; + bool hasUpcomingHigh; + bool hasUpcomingMedium; + bool inNewsBlackout; + NewsEvent nearestHighImpact; + NewsEvent nearestMedium; + int minsToNextHigh; + int minsToNextMedium; + int highImpactToday; + int mediumImpactToday; + int lowImpactToday; + bool affectsUSD; + bool affectsEUR; + bool affectsGBP; + bool affectsJPY; + bool affectsCurrentPair; + double maxAllowedRisk; + double positionMultiplier; + bool allowNewTrades; + bool closeExisting; + string recommendation; + int highImpactMins; + int mediumImpactMins; + bool filterEnabled; + bool enabled; + bool avoidHighImpact; + bool avoidMediumImpact; + int minutesBeforeHigh; + int minutesAfterHigh; + int minutesBeforeMed; + int minutesAfterMed; + int eventCount; + datetime lastUpdate; + bool dataAvailable; + bool isTradingBlocked; + string blockReason; + int utcOffset; // [v6.42] Manual UTC offset for broker + bool checkCHF; // [v6.42] Check CHF news + bool checkAUD; // [v6.42] Check AUD news + bool checkCAD; // [v6.42] Check CAD news + bool checkNZD; // [v6.42] Check NZD news +}; +struct CorrelationData { + string symbol; + string baseCurrency; + string quoteCurrency; + double corrEURUSD; + double corrGBPUSD; + double corrUSDJPY; + double corrUSDCHF; + double corrAUDUSD; + double corrUSDCAD; + double corrNZDUSD; + double corrXAUUSD; + double corrDXY; + bool followsDXY; + bool inversesDXY; + int longPositions; + int shortPositions; + double totalLongExposure; + double totalShortExposure; + double netExposure; + double usdExposure; + double eurExposure; + double gbpExposure; + double jpyExposure; + double chfExposure; + double audExposure; + double cadExposure; + double nzdExposure; + ENUM_EXPOSURE_LEVEL exposureLevel; + double diversificationScore; + double concentrationRisk; + bool isOverexposed; + string riskWarning; + double maxAdditionalLots; + bool allowNewPosition; + double positionMultiplier; + string recommendation; + int lookbackPeriod; + datetime lastCalculation; + bool dataValid; +}; +// TIME ANALYSIS STRUCTURES +struct HourlyPerformance { + int hour; + int totalTrades; + int wins; + int losses; + int breakeven; + double winRate; + double avgWin; + double avgLoss; + double avgRR; + double profitFactor; + double expectancy; + double totalProfit; + ENUM_HOUR_QUALITY quality; + double score; + bool isRecommended; + bool shouldAvoid; + double avgVolatility; + double avgSpread; + double avgRange; + string bestSetupType; + double bestSetupWinRate; +}; +struct DayPerformance { + int dayOfWeek; + string dayName; + int totalTrades; + int wins; + int losses; + double winRate; + double avgReturn; + double profitFactor; + double expectancy; + ENUM_DAY_QUALITY quality; + bool isRecommended; + int bestHour; + int worstHour; +}; +struct TimeAnalysisData { + HourlyPerformance hourlyStats[24]; + int bestHours[5]; + int worstHours[5]; + int currentHourRank; + DayPerformance dailyStats[7]; + int bestDays[3]; + int worstDays[2]; + int currentDayRank; + ENUM_HOUR_QUALITY currentHourQuality; + ENUM_DAY_QUALITY currentDayQuality; + bool isOptimalTime; + bool shouldAvoidNow; + double currentTimeScore; + double asianWinRate; + double londonWinRate; + double nyWinRate; + string bestSession; + string worstSession; + string recommendation; + double confidenceMultiplier; + double positionMultiplier; + int totalSamples; + datetime lastUpdate; + bool hasEnoughData; + bool isInBestHours; + bool isInWorstHours; +}; +// SMART ENTRY STRUCTURE +struct SmartEntryDecision { + bool shouldEnter; + bool isCounterTrend; // * v9.31 FIX#104c: was this a counter-trend entry? + int rawScore; + int adjustedScore; + int finalConfidence; + ENUM_ENTRY_QUALITY quality; + double winProbability; + double expectedValue; + double breakEvenWinRate; + bool isPositiveEV; + double recommendedRisk; + double recommendedLots; + double positionMultiplier; + double positionSizeMultiplier; + double kellyFraction; + double trendScore; + double structureScore; + double zoneScore; + double confluenceScore; + double timingScore; + double patternScore; + double sweepScore; + double regimeAdjustment; + double regimeBonus; + double newsAdjustment; + double correlationAdjust; + double timeAdjustment; + double streakAdjustment; + double optimalRR; + double suggestedSL; + double suggestedTP1; + double suggestedTP2; + double suggestedTP3; + string entryReason; + string rejectReason; + string warnings[5]; + int warningCount; + string strengths[5]; + int strengthCount; + ENUM_MARKET_REGIME regime; + int regimeAtEntry; + ENUM_AMD_PHASE amdPhase; + bool inKillzone; + bool newsImpact; + datetime decisionTime; + datetime analysisTime; + int barsUntilExpiry; + bool showInfo; // [v6.42] Show SmartEntry info panel +}; +struct EntryScoreStruct { + // * v9.09 FIX#17: EVIDENCE-BASED SCORING REBUILD + // Old: 11 modules (155pts), 74% "aesthetic" = ZERO predictive power + // New: 4 proven modules (75pts), each has data/logic backing + // -- PROVEN MODULES -- + int trendScore; // 0-25: HTF direction + structure alignment + int timingScore; // 0-15: Killzone + AMD phase + int rrScore; // 0-20: R:R achievability (R:R<1.3=50%WR vs >1.7=16%) + int momentumScore; // 0-10: ATR expansion = fresh move + int mlScore; // -10 to +15: NN TP1 win probability + // -- LEGACY (kept for data collection, NOT in totalScore) -- + int zoneScore; // logged only + int sweepScore; // logged only + int confluenceScore; // logged only + int candleScore; // logged only + int obQualityScore; // logged only + int volumeScore; // logged only + int divergenceScore; // logged only + int trendlineScore; // logged only + int totalScore; // max ~85 (was 155) + string grade; + datetime calcTime; +}; +//+------------------------------------------------------------------+ +//| * v10.59 FIX#369: UnifiedScoreResult — Single source of truth | +//| All three previous scoring systems (CalculateEntryScore 5-arg, | +//| CalculateEntryScore 1-arg dead code, EvaluateSmartEntry inline) | +//| produced different scores for the same trade. | +//| This struct is produced by ComputeUnifiedScore() and consumed | +//| by ALL gates: MeetsMinimumEntryScore, SelectBestCandidate, | +//| EvaluateSmartEntry. One formula, one result, zero drift. | +//+------------------------------------------------------------------+ +struct UnifiedScoreResult +{ + // === MODULE 1: TREND & HTF (0-25) === + // Structure aligned = +12. HTF: strong=+13, normal=+8, opposed=-8, strong_opposed=-12. + int trendScore; + + // === MODULE 2: TIMING (0-15) === + // Killzone = +12, outside = +2. AMD distribution phase = +3 bonus. + int timingScore; + + // === MODULE 3: R:R ACHIEVABILITY (0-20) === + // Single source: FIX#360 TF-aware brackets. H4: 1.6-2.5R=20pts sweet spot. + // FIX#361 hard gate: rrScore<10 = reject for H4+. + int rrScore; + + // === MODULE 4: MOMENTUM (0-10) === + // ATR ratio vs 5-bar average. Expanding = fresh move = bullish score. + int momentumScore; + + // === MODULE 5: ML/NN (-10 to +15) === + // NN TP1 win probability. (g_nnTP1WinProb - 0.50) * 50. + int mlScore; + + // === MODULE 6: CASCADE ZONE (0-14) === + // CONF_PREMIUM_DISC points: discount for BUY, premium for SELL. + int cascadeZoneScore; + + // === MODULE 7: CASCADE CONFLUENCE (0-29) === + // Base 8 (entry tech itself) + FVG=+8, OB=+8, VSA=+5. Max 29. + int cascadeConfluenceScore; + + // === MODULE 8: CASCADE SWEEP (0-20) === + // Judas swing active = 20, else 2. + int cascadeSweepScore; + + // === MODULE 9: CASCADE TIMING (0-15) === + // CONF_KILLZONE from cascade (0 or 15). + int cascadeTimingScore; + + // === MODULE 10: CASCADE PATTERN (0-10) === + // CONF_DIVERGENCE=+5, CONF_TRENDLINE=+5. + int cascadePatternScore; + + // === MODULE 11: TECHNIQUE CONVERGENCE (-5 to +15) === + // How many independent methods (FVG/OB/OTE/Breaker/Structure/Judas/LiqSweep) agree. + int techniqueBonus; + + // === MODULE 12: REGIME ALIGNMENT (-8 to +15) === + // STRONG_TREND=15, TREND=10, WEAK_TREND=5 (FIX#366: no bonus for REGIME_TRENDING on H4+). + // Counter-regime: -8. + int regimeScore; + + // === MODULE 13: HTF CASCADE (0-10) === + // CONF_HTF_TREND strength * 10. + int htfBonus; + + // === MODULE 14: LEGACY BONUS (0-15) === + // 30% weight of: candle, volume, obQuality. Capped at 15. + int legacyBonus; + // === MODULE 15: MEAN REVERSION QUALITY (0-25) === * FIX#373 + // RSI extreme=+20, range boundary=+15, liquidity sweep=+15, barsInRegime>=3=+10, ATR contracting=+10. + // Only nonzero for MEAN_REV candidates. + int mrScore; + + // === TOTALS === + int totalScore; // Sum of all modules above + string grade; // A+/A/B+/B/C/D/F (single calibrated thresholds) + + // === PROBABILITY & EV === + double winProbability; // 0.0-100.0 (from CalculateWinProbability) + double expectedValue; // R multiple (multi-TP formula FIX#368 or simple) + bool isPositiveEV; + bool cascadeAvailable; // true = modules 6-13 computed; false = pre-cascade estimate + + // === METADATA === + string grade_reason; // Debug string summarising key module values + datetime calcTime; + bool isValid; +}; + +// Global cache: populated once per bar per candidate, read by all gates +UnifiedScoreResult g_lastUnifiedScore; + +//+------------------------------------------------------------------+ +//| Confluence Data Structure | +//+------------------------------------------------------------------+ +struct ConfluenceScoreStruct +{ + bool hasOB; // Order Block present + bool hasFVG; // FVG present + bool hasOTE; // In OTE zone + bool hasBB; // Breaker Block present + bool hasMB; // Mitigation Block present + bool hasSB; // Silver Bullet active + bool hasTBS; // TBS active + bool hasCRT; // CRT active + bool hasVolume; // High volume confirmation + bool hasDivergence; // Divergence present + bool hasTrendline; // Trendline confirmation + bool hasLiquidity; // Near liquidity level + int zoneCount; // Total overlapping zones + double score; // Score 0-100 +}; +// CANDLE PATTERN STRUCTURE (ORIGINAL - Keep for compatibility) +struct CandlePatternStruct { + bool isBullishEngulfing; + bool isBearishEngulfing; + bool isBullishPinBar; + bool isBearishPinBar; + bool isBullishInsideBreak; + bool isBearishInsideBreak; + bool isBullishHammer; + bool isBearishShootingStar; + int patternStrength; + string patternName; +}; +// DIVERGENCE STRUCTURE +//+==================================================================+ +//| DIVERGENCE STRUCTURE (ENHANCED) | +//+==================================================================+ +struct DivergenceStruct { + int id; + ENUM_DIVERGENCE_TYPE type; + ENUM_DIVERGENCE_INDICATOR indicator; + ENUM_DIVERGENCE_STRENGTH strength; + // Price pivots + double price1; + double price2; + int bar1; + int bar2; + datetime time1; + datetime time2; + // Indicator pivots + double rsi1; + double rsi2; + // Additional data + double priceMove; + double indMove; + double divergenceAngle; + // Status + bool active; + bool confirmed; + bool broken; + bool triggered; + datetime createdTime; + datetime expiryTime; + // Entry data + double entryPrice; + double stopLoss; + double takeProfit; + double score; + // Visual + string priceLine; + string indLine; + string arrowObj; + string labelObj; +}; +struct TrendlineStruct { + int id; + ENUM_TRENDLINE_TYPE type; + ENUM_TRENDLINE_STRENGTH strength; + ENUM_TRENDLINE_STATUS status; + // Pivot points + double startPrice; + double endPrice; + double currentPrice; + int startBar; + int endBar; + datetime startTime; + datetime endTime; + // Line properties + double slope; + double angle; + double zoneTop; + double zoneBottom; + // Touch data + int touches; + datetime lastTouchTime; + // Break data + bool broken; + datetime breakTime; + double breakPrice; + double breakDistance; + // Retest data + bool retested; + int retestCount; + datetime lastRetestTime; + // Score + double score; + // Entry levels + double entryPrice; + double stopLoss; + double takeProfit; + // Visual + string objName; + string zoneObj; + string labelObj; + string breakObj; + // Status + bool active; + datetime createdTime; + datetime expiryTime; +}; +// FIBONACCI STRUCTURES +struct FIB_Level { + double level; + double price; + string label; + color lineColor; + int lineStyle; + int lineWidth; + bool isICT; + bool isExtension; +}; +struct FIB_Struct { + datetime swingHighTime; + datetime swingLowTime; + double swingHigh; + double swingLow; + double range; + bool isUptrend; + bool isValid; + int age; + datetime lastUpdate; + FIB_Level levels[]; +}; +struct SpreadAnalysis { + double currentSpread; + double avgSpread; + double minSpread; + double maxSpread; + double spreadVolatility; + bool isSpreadNormal; + datetime lastUpdate; +}; +// NEURAL NETWORK STRUCTURES +struct NeuronStruct { + double weights[NN_MAX_WEIGHTS]; + int numWeights; + double bias; + double output; + double delta; + double weightGradients[NN_MAX_WEIGHTS]; + double biasGradient; + double m_weights[NN_MAX_WEIGHTS]; + double v_weights[NN_MAX_WEIGHTS]; + double m_bias; + double v_bias; +}; +struct LayerStruct { + int numNeurons; + NeuronStruct neurons[NN_MAX_NEURONS]; + ENUM_NN_ACTIVATION activation; + double dropoutRate; + bool dropoutMask[NN_MAX_NEURONS]; +}; +struct NeuralNetwork { + int numLayers; + LayerStruct layers[NN_MAX_LAYERS]; + ENUM_NN_OPTIMIZER optimizer; + ENUM_NN_LOSS lossFunction; + double learningRate; + double beta1; + double beta2; + double epsilon; + double weightDecay; + int timestep; + bool isTraining; + double lastLoss; + double bestLoss; + int epochsWithoutImprovement; +}; +struct ML_Prediction { + datetime timestamp; + double prediction; + double confidence; + string direction; + string category; + double bullishProb; + double bearishProb; + double neutralProb; + double targetPrice; + double expectedMove; + string actualDirection; + bool wasCorrect; +}; +struct FeatureNormalizer { + double means[NN_MAX_FEATURES]; + double stds[NN_MAX_FEATURES]; + double mins[NN_MAX_FEATURES]; + double maxs[NN_MAX_FEATURES]; + int numFeatures; + bool useMeanStd; + bool initialized; +}; +// KILLZONE STRUCTURES +struct KillzoneDefinition { + ENUM_KILLZONE_TYPE type; + string name; + int startHourUTC; + int startMinuteUTC; + int endHourUTC; + int endMinuteUTC; + int dstOffsetUS; + int dstOffsetEU; + color zoneColor; + bool enabled; + double historicalVolatility; + double historicalWinRate; +}; +struct ActiveKillzone { + ENUM_KILLZONE_TYPE type; + datetime startTime; + datetime endTime; + double openPrice; + double highPrice; + double lowPrice; + double closePrice; + double open; + double high; + double low; + double close; + double range; + double direction; + bool isActive; + bool breakoutOccurred; + string objectName; +}; +struct DSTInfo { + bool isUSDST; + bool isEUDST; + bool isUKDST; + int utcOffsetBroker; + int utcOffsetLocal; + int brokerUTCOffset; + int localUTCOffset; + datetime nextDSTChange; + string currentTimezone; +}; +// PERFORMANCE & PERSISTENCE STRUCTURES +struct StrategyPerformance { + string name; + int totalTrades; + int wins; + int losses; + int breakeven; + double winRate; + double avgWin; + double avgLoss; + double profitFactor; + double expectancy; + double maxDrawdown; + int maxConsecutiveLosses; + double totalProfitPips; + double totalLossPips; + double sharpeRatio; + double sortinoRatio; + datetime lastTradeTime; + // * v10.29 FIX#316: per-regime breakdown (6 regimes: trending/ranging/choppy/weak/volatile/breakout) + // wins_regime[0]=trending, [1]=ranging, [2]=choppy, [3]=weak_trend, [4]=volatile, [5]=breakout + int wins_regime[6]; + int total_regime[6]; + double wr_regime[6]; // win rate per regime (updated after each trade) +}; +struct DailyPerformance { + datetime date; + int trades; + int wins; + int losses; + double profitPips; + double profitMoney; + double maxDrawdown; + double peakEquity; + string bestStrategy; + string worstStrategy; + string bestKillzone; +}; +struct TradeRecord { + long id; + datetime entryTime; + datetime exitTime; + double entryPrice; + double exitPrice; + double stopLoss; + double takeProfit; + double lotSize; + bool isBullish; + string direction; + string strategy; + double entryQuality; + double quality; + string result; + double pnlPips; + double pnlMoney; + double riskReward; + double maxDrawdown; + double maxProfit; + string killzone; + ENUM_KILLZONE_TYPE killzoneType; + string marketPhase; +}; +struct PerformancePersistence { + int totalTrades; + int totalWins; + int totalLosses; + int totalBreakeven; + double totalProfitPips; + double totalLossPips; + double overallWinRate; + double overallExpectancy; + double avgWinPips; + double avgLossPips; + double profitFactor; + double expectancy; + double maxDrawdown; + double peakEquity; + int maxConsecutiveLosses; + double sharpeRatio; + double sortinoRatio; + StrategyPerformance strategies[15]; + StrategyPerformance strategyStats[15]; + int numStrategies; + int highQualityWins; + int highQualityLosses; + int mediumQualityWins; + int mediumQualityLosses; + int lowQualityWins; + int lowQualityLosses; + DailyPerformance dailyStats[365]; + int numDays; + double mlAccuracy; + int mlCorrectPredictions; + int mlTotalPredictions; + datetime firstTradeDate; + datetime lastTradeDate; + datetime lastSaveTime; + string version; +}; +struct TradePerformanceStruct { + datetime entryTime; + int direction; + double entryPrice; + double exitPrice; + double plannedRR; + double actualRR; + int confidenceAtEntry; + ENUM_MARKET_REGIME regimeAtEntry; + bool isWinner; + string exitReason; + double maxFavorable; + double maxAdverse; +}; +// BACKTESTING STRUCTURES +struct BacktestTrade { + long id; + datetime entryTime; + datetime exitTime; + double entryPrice; + double exitPrice; + double stopLoss; + double takeProfit; + double lotSize; + bool isBullish; + string direction; + string strategy; + double entryQuality; + double quality; + string exitReason; + double pnl; + double pnlPips; + double pnlMoney; + double pnlPercent; + double runningEquity; + double drawdown; +}; +struct BacktestResults { + datetime startDate; + datetime endDate; + int totalBars; + int totalTrades; + int winningTrades; + int losingTrades; + int breakevenTrades; + double netProfit; + double grossProfit; + double grossLoss; + double profitFactor; + double returnPercent; + double maxDrawdown; + double maxDrawdownPercent; + double avgDrawdown; + double sharpeRatio; + double sortinoRatio; + double calmarRatio; + double winRate; + double avgWin; + double avgLoss; + double avgTrade; + double expectancy; + double payoffRatio; + int maxConsecutiveWins; + int maxConsecutiveLosses; + double avgTradesPerDay; + double equityCurve[]; + double drawdownCurve[]; + datetime equityTimes[]; + datetime equityDates[]; + int equityPoints; + StrategyPerformance strategyStats[15]; + StrategyPerformance strategyBreakdown[15]; + int numStrategies; +}; +struct MonteCarloResults { + int numSimulations; + double medianReturn; + double meanReturn; + double stdReturn; + double percentile5; + double percentile25; + double percentile75; + double percentile95; + double worstCase; + double bestCase; + double probabilityOfProfit; + double probabilityOfRuin; + double confidenceLevel; +}; +// SIGNAL & RISK STRUCTURES +struct SIGNAL_Struct { + long id; + datetime time; + double price; + double entryPrice; + bool isBullish; + string strategy; + double confluence; + double stopLoss; + double takeProfit; + double riskReward; + double lotSize; + bool active; + double entryQuality; + string result; + string status; + datetime exitTime; + datetime expiryTime; + double exitPrice; + double pnlPips; + double pnlMoney; + string killzoneName; + ENUM_KILLZONE_TYPE killzone; + double mlConfidence; + bool structureAligned; + bool pdZoneAligned; + string pdZone; + string structureBias; + string marketPhase; +}; +struct RISK_Struct { + double currentRisk; + double dailyPL; + int tradesCount; + double winRate; + double maxDrawdown; + datetime lastResetTime; + double equityHigh; + double currentDrawdown; + bool dailyLimitReached; + bool maxTradesReached; +}; +// COST ANALYSIS STRUCTURES +struct TradingCosts { + double spreadCost; + double commissionCost; + double slippageCost; + double totalCost; + double costPerPoint; + double breakEvenPoints; + double swapCostDaily; + double costAsPercentOfSL; + double effectiveRR; + bool isCostAcceptable; + string rejectReason; +}; +struct BrokerConfig { + double commissionPerLot; + double commissionPerSide; + bool isCommissionInCurrency; + double avgSlippagePoints; + double maxAcceptableSpread; + double maxAcceptableCost; + string brokerName; + double swapLong; + double swapShort; + bool isECN; +}; +// PAIR OPTIMIZATION STRUCTURES +struct PairCharacteristics { + double avgDailyRange; + double avgHourlyRange; + double volatilityRank; + double trendStrength; + double meanReversionScore; + int bestTimeframeIndex; + string bestSession; + double correlationEURUSD; +}; +struct SessionPerformance { + string sessionName; + int totalTrades; + int winningTrades; + double winRate; + double avgRR; + double profitFactor; + double bestHour; + double worstHour; +}; +struct PairProfile { + string symbol; + string category; + double avgSpread; + double optimalSL_Mult; + double optimalTP_Mult; + double optimalRR; + ENUM_TP_MODE optimalTP; + double minConfluence; + double minEntryQuality; + bool allowScalping; + bool allowSwing; + bool useLiquidityGrab; + bool useBOSRetest; + bool useOTE; + int maxDailyTrades; + double riskPercent; + PairCharacteristics chars; +}; +struct PriceLevel { + double price; + double probability; + double strength; + string type; + color zoneColor; + datetime startTime; + datetime endTime; +}; +//+==================================================================+ +//| NEW CHART PATTERN STRUCTURES | +//+==================================================================+ +// HEAD & SHOULDERS STRUCTURE +struct HeadShouldersPattern { + int id; + string objName; + ENUM_CHART_PATTERN_TYPE type; + ENUM_CHART_PATTERN_STATUS status; + ENUM_CHART_PATTERN_QUALITY quality; + double leftShoulder; + double head; + double rightShoulder; + double necklineLeft; + double necklineRight; + double necklineSlope; + datetime leftShoulderTime; + datetime headTime; + datetime rightShoulderTime; + datetime necklineLeftTime; + datetime necklineRightTime; + int leftShoulderBar; + int headBar; + int rightShoulderBar; + double entryPrice; + double stopLoss; + double takeProfit1; + double takeProfit2; + double patternHeight; + double riskReward; + bool necklineBroken; + datetime breakTime; + double breakPrice; + bool retested; + datetime retestTime; + bool isValid; + bool active; + datetime createdTime; + datetime expiryTime; + int barsActive; + int score; + double symmetryScore; +}; +// DOUBLE/TRIPLE TOP/BOTTOM STRUCTURE +struct MultipleTopBottomPattern { + int id; + string objName; + ENUM_CHART_PATTERN_TYPE type; + ENUM_CHART_PATTERN_STATUS status; + ENUM_CHART_PATTERN_QUALITY quality; + double peak1; + double peak2; + double peak3; + datetime peak1Time; + datetime peak2Time; + datetime peak3Time; + int peak1Bar; + int peak2Bar; + int peak3Bar; + double neckline; + double necklinePrice; + datetime necklineTime; + double entryPrice; + double stopLoss; + double takeProfit1; + double takeProfit2; + double patternHeight; + double riskReward; + bool necklineBroken; + datetime breakTime; + double breakPrice; + bool retested; + bool isValid; + bool active; + datetime createdTime; + int barsActive; + int score; + double peakTolerance; +}; +// TRIANGLE PATTERN STRUCTURE +struct TrianglePattern { + int id; + string objName; + ENUM_CHART_PATTERN_TYPE type; + ENUM_CHART_PATTERN_STATUS status; + ENUM_CHART_PATTERN_QUALITY quality; + double upperStart; + double upperEnd; + double upperSlope; + datetime upperStartTime; + datetime upperEndTime; + double lowerStart; + double lowerEnd; + double lowerSlope; + datetime lowerStartTime; + datetime lowerEndTime; + double apexPrice; + datetime apexTime; + double patternHeight; + int patternBars; + int touchesUpper; + int touchesLower; + double entryPrice; + double stopLoss; + double takeProfit1; + double takeProfit2; + double riskReward; + bool brokenUp; + bool brokenDown; + datetime breakTime; + double breakPrice; + bool isValid; + bool active; + datetime createdTime; + int barsActive; + int score; +}; +// FLAG & PENNANT STRUCTURE +struct FlagPennantPattern { + int id; + string objName; + ENUM_CHART_PATTERN_TYPE type; + ENUM_CHART_PATTERN_STATUS status; + ENUM_CHART_PATTERN_QUALITY quality; + double poleStart; + double poleEnd; + double poleHeight; + datetime poleStartTime; + datetime poleEndTime; + int poleBars; + double flagHigh; + double flagLow; + double flagUpperSlope; + double flagLowerSlope; + datetime flagStartTime; + datetime flagEndTime; + int flagBars; + double entryPrice; + double stopLoss; + double takeProfit1; + double takeProfit2; + double riskReward; + bool breakoutConfirmed; + datetime breakTime; + double breakPrice; + bool isValid; + bool active; + datetime createdTime; + int barsActive; + int score; +}; +// WEDGE PATTERN STRUCTURE +struct WedgePattern { + int id; + string objName; + ENUM_CHART_PATTERN_TYPE type; + ENUM_CHART_PATTERN_STATUS status; + ENUM_CHART_PATTERN_QUALITY quality; + double upperStart; + double upperEnd; + double upperSlope; + datetime upperStartTime; + datetime upperEndTime; + int upperTouches; + double lowerStart; + double lowerEnd; + double lowerSlope; + datetime lowerStartTime; + datetime lowerEndTime; + int lowerTouches; + double patternHeight; + int patternBars; + double convergenceRate; + double entryPrice; + double stopLoss; + double takeProfit1; + double takeProfit2; + double riskReward; + bool brokenUp; + bool brokenDown; + datetime breakTime; + double breakPrice; + bool isValid; + bool active; + datetime createdTime; + int barsActive; + int score; +}; +// DIAMOND PATTERN STRUCTURE +struct DiamondPattern { + int id; + string objName; + ENUM_CHART_PATTERN_TYPE type; + ENUM_CHART_PATTERN_STATUS status; + ENUM_CHART_PATTERN_QUALITY quality; + double expandHigh1; + double expandLow1; + double expandHigh2; + double expandLow2; + double contractHigh1; + double contractLow1; + double contractHigh2; + double contractLow2; + double patternHigh; + double patternLow; + double patternMid; + datetime startTime; + datetime endTime; + int patternBars; + double entryPrice; + double stopLoss; + double takeProfit1; + double takeProfit2; + double riskReward; + bool brokenUp; + bool brokenDown; + datetime breakTime; + double breakPrice; + bool isValid; + bool active; + datetime createdTime; + int barsActive; + int score; +}; +// V-PATTERN STRUCTURE +struct VPattern { + int id; + string objName; + ENUM_CHART_PATTERN_TYPE type; + ENUM_CHART_PATTERN_STATUS status; + ENUM_CHART_PATTERN_QUALITY quality; + double startPrice; + double apexPrice; + double endPrice; + datetime startTime; + datetime apexTime; + datetime endTime; + int startBar; + int apexBar; + int endBar; + double dropDistance; + double riseDistance; + double symmetryRatio; + int dropBars; + int riseBars; + double dropAngle; + double riseAngle; + double entryPrice; + double stopLoss; + double takeProfit1; + double takeProfit2; + double riskReward; + bool isValid; + bool active; + datetime createdTime; + int barsActive; + int score; +}; +// EXTENDED CANDLE PATTERN STRUCTURE (NEW) +struct CandlePatternStructExtended { + // Existing patterns (compatibility) + bool isBullishEngulfing; + bool isBearishEngulfing; + bool isBullishPinBar; + bool isBearishPinBar; + bool isBullishInsideBreak; + bool isBearishInsideBreak; + // Single candle patterns + bool isDoji; + bool isDojiDragonfly; + bool isDojiGravestone; + bool isDojiLongLegged; + bool isHammer; + bool isInvertedHammer; + bool isHangingMan; + bool isShootingStar; + bool isSpinningTop; + bool isMarubozuBull; + bool isMarubozuBear; + // Two candle patterns + bool isBullishHarami; + bool isBearishHarami; + bool isBullishHaramiCross; + bool isBearishHaramiCross; + bool isPiercingLine; + bool isDarkCloudCover; + bool isTweezerTop; + bool isTweezerBottom; + // Three candle patterns + bool isMorningStar; + bool isEveningStar; + bool isMorningDojiStar; + bool isEveningDojiStar; + bool isThreeWhiteSoldiers; + bool isThreeBlackCrows; + bool isThreeInsideUp; + bool isThreeInsideDown; + bool isThreeOutsideUp; + bool isThreeOutsideDown; + bool isAbandonedBabyBull; + bool isAbandonedBabyBear; +// Pattern metadata + ENUM_CANDLE_PATTERN_TYPE patternType; + int patternStrength; + string patternName; + bool isBullish; + bool isBearish; + bool isReversal; + bool isContinuation; + int candlesInPattern; + double reliability; + datetime patternTime; + int patternBar; + bool isValid; // <- ADD THIS LINE! +}; +// MASTER PATTERN CONTAINER +struct AllPatternsData { + int hsCount; + int mtbCount; + int triangleCount; + int fpCount; + int wedgeCount; + int diamondCount; + int vCount; + int candleCount; + int totalPatterns; + datetime lastUpdate; + bool hasActivePattern; + string strongestPattern; + int strongestScore; + datetime strongestPatternTime; // * v9.31 FIX#108: for stale check in FIX#99a +}; +// VSA Pattern Structure +struct VSA_Pattern +{ + ENUM_VSA_PATTERN type; + ENUM_VSA_SIGNAL signal; + datetime time; + int barIndex; + double price; + double volume; + double volumeRatio; + double strength; + bool isBullish; + bool isBearish; + string description; + string objName; +}; +// MTF Analysis Structure +struct MTF_Analysis +{ + int bullishTFs; + int bearishTFs; + int neutralTFs; + int totalTFs; + double overallConfidence; + ENUM_MTF_DIRECTION overallDirection; + string alignment; + string details; + datetime lastUpdate; +}; +// Timeframe Bias Structure +struct TF_Bias +{ + ENUM_TIMEFRAMES timeframe; + double maValue; + double currentPrice; + int direction; // 1=bull, -1=bear, 0=neutral + double strength; + bool isValid; +}; +// Multi-TP Entry Structure +struct MultiTPEntry { + datetime entryTime; + double entryPrice; + double stopLoss; + double riskPoints; + double tp1Price; + double tp2Price; + double tp3Price; + bool tp1Hit; + bool tp2Hit; + bool tp3Hit; + datetime tp1HitTime; + datetime tp2HitTime; + datetime tp3HitTime; + bool slMovedToBE; + double currentSL; + int direction; // 1 = long, -1 = short + bool active; + string objName; + int id; + double initialLotSize; + double remainingLots; + double tp1Lots; + double tp2Lots; + // * v9.30 FIX#100: Per-trade dynamic thresholds (calculated at open from TP1/SL ratio) + double perTrade_BE_RR; // BE threshold for THIS trade + double perTrade_Trail_RR; // Trail start for THIS trade + double perTrade_SmartExit_RR; // SmartExit min profit for THIS trade + double tp1_R_ratio; // TP1 distance / SL distance (e.g. 0.80R on H4) + double tp3Lots; + bool isAutoAdjusted; + string adjustmentReason; + long ticket; // * FIX#17b: Real MT5 position ticket for sync (0=unmatched) + bool isCounterTrend; // * v9.31 FIX#104c: True if trade was counter-trend at entry + datetime entryConfirmTime; // * v9.31 FIX#104c: Time trade was opened (for early cut timing) + double atr_at_entry; // * v9.36 FIX#138: ATR cached at entry -- used for trail/BE/SmartExit (NOT live ATR) + double sl_dist_cached; // * v9.36 FIX#138: Absolute SL distance cached at entry (in price units) + double peakRR; // Highest RR reached since entry + // Legacy fields from old cooperative SmartExit system — unused, kept for struct alignment + int se_shrinkBars; + double se_barSizeRef; + // ZoneInvalidation() reads these every bar to detect if the entry reason is gone. + // If price closes beyond zone boundary → trade is invalid → close immediately. + double zoneTop; // Upper boundary of the zone that triggered entry + double zoneBottom; // Lower boundary of the zone that triggered entry + string zoneType; // "FVG","OB","BREAKER","OTE","BOS","TBS","CRT","LIQ","JUDAS","SB","TC" + bool zoneInvalidated; // Guard flag: once invalidated, don't re-check + bool partialCloseDone; // * FIX#384: true once momentum-fade partial close fired + // * FIX#373: AdverseClose pre-existing signal exclusion. + // Signals already present AT ENTRY were priced in by EvaluateSmartEntry. + // AdverseClose must only react to signals that changed AFTER entry. + ENUM_MARKET_REGIME entryRegime; // Regime at trade open + bool entryStructureBull; // Structure direction at open (true=bullish) + bool entryMTFBull; // MTF direction at open (true=bullish or neutral) +}; +// ============================================================ +// FIX#378-382: MARKET CONTEXT + SMART EXIT 2.0 — v10.64 +// ============================================================ +enum ENUM_MARKET_PHASE { + MPHASE_UNKNOWN = 0, + MPHASE_ACCUMULATION = 1, + MPHASE_MARKUP = 2, + MPHASE_DISTRIBUTION = 3, + MPHASE_MARKDOWN = 4, + MPHASE_RANGING = 5, + MPHASE_BREAKOUT_BULL = 6, + MPHASE_BREAKOUT_BEAR = 7 +}; +struct MarketContext { + ENUM_MARKET_PHASE phase; + bool allowBuy; + bool allowSell; + bool useFVG; + bool useOB; + bool useTC; + bool useBOS; + bool useOTE; + bool useLIQ; + double fvgScoreMult; + double obScoreMult; + double tcScoreMult; + double bosScoreMult; + double oteScoreMult; + double liqScoreMult; + int contextConfidence; + double nearestResistance; + double nearestSupport; + double htfResistance; + double htfSupport; + string phaseDescription; + datetime computedAt; + bool valid; +}; + +// Multi-TP Statistics Structure +struct MultiTPStats { + int totalEntries; + int tp1HitCount; + int tp2HitCount; + int tp3HitCount; + int slHitCount; + int beHitCount; + double totalProfitTP1; + double totalProfitTP2; + double totalProfitTP3; + double totalLoss; + double avgTP1RR; + double avgTP2RR; + double avgTP3RR; + double winRate; + double avgWinRR; + double profitFactor; + datetime lastUpdate; +}; + +// ── FIX#502: SCENARIO PROFILE STRUCT ───────────────────────────────── +// Output of DeriveScenarioProfile(). Populated once per bar in EA_CheckSignals. +// Carries: which scenario, how to enter, where SL/TP go, which techniques allowed. +// isValid=false → EA_CheckSignals returns immediately (choppy/unknown market). +struct ScenarioProfile { + ENUM_SCENARIO scenario; // Which of the 12 scenarios is active + ENUM_ENTRY_STYLE entryStyle; // How to enter (retest/fib/breakout/fade) + ENUM_SL_METHOD slMethod; // Where to place SL + ENUM_TP_METHOD tpMethod; // Where to take profit + // Technique gates — only allowed techniques generate candidates + bool allowFVG; // FVG entries allowed in this scenario + bool allowOB; // Order Block entries allowed + bool allowOTE; // OTE (Fibonacci) entries allowed + bool allowBOS; // BOS Retest entries allowed + bool allowTC; // Trend Continuation entries allowed + bool allowLIQ; // Liquidity grab entries allowed + bool allowMEANREV; // Mean Reversion entries allowed + // Scenario parameters + int direction; // +1=long only, -1=short only, 0=both + double minRR; // Minimum R:R for this scenario + int confidence; // 0-100: how confident is this scenario + string description; // Human-readable for logs + bool isValid; // false=choppy/unknown → no entry +}; + +// Global scenario profile — written by DeriveScenarioProfile(), read everywhere +ScenarioProfile g_scenarioProfile; +//+------------------------------------------------------------------+ +//| END OF SECTION 3: STRUCTURES | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| SECTION 4: INPUT PARAMETERS | +//+------------------------------------------------------------------+ +// [OK] ΠΡΟΣΘΗΚΗ - ΝΕΟΣ INPUT: +input group "======== TRADING STYLE PRESET ========" +input ENUM_TRADING_STYLE TradingStylePreset = STYLE_CUSTOM; // Trading Style Preset +// GENERAL SETTINGS +input group "======== GENERAL SETTINGS ========" +input bool ShowMA = false; // Show Moving Average +input int MAPeriod = 50; // MA Period +input ENUM_MA_METHOD MAMethod = MODE_SMA; // MA Method +input ENUM_APPLIED_PRICE MAPrice = PRICE_CLOSE; // MA Applied Price +input int MaxBarsToCalculate = 1000; // Max Bars to Calculate +input int RefreshRate = 3; // Refresh Rate (ticks) +input bool EnableCache = true; // Enable Caching +input bool EnableDebugMode = false; // Enable Debug Mode +// FEATURE ENABLES +input group "======== FEATURE ENABLES ========" +input bool EnableFVG = true; // * FIX#322b: H4 FVG now user-controlled (removed hard architectural block). Enable/disable affects all TFs including H4. M5 FVG disabled in EA_CheckSignals (noise-level gaps). +input bool EnableOB = true; // Enable Order Blocks +input bool EnableLiquidity = true; // Enable Liquidity +input bool EnableStructure = true; // Enable Structure +input bool EnableOTE = true; // Enable OTE Zones +input bool EnableBreakerBlocks = true; // Enable Breaker Blocks +input bool EnableTrendCont = true; // * v7.8 Enable Trend Continuation +input int TC_EMA_Fast = 21; // TC: Fast EMA period +input int TC_EMA_Slow = 50; // TC: Slow EMA period +input int TC_RSI_Period = 14; // TC: RSI period +input double TC_RSI_Min = 45.0; // TC: Min RSI for bullish (50-65 = momentum) +input double TC_RSI_Max = 55.0; // TC: Max RSI for bearish (35-50 = momentum) +input double TC_MinSlopeATR = 0.05; // TC: Min EMA slope (x ATR) for trend validation +input int TC_PullbackBars = 3; // TC: Max bars for pullback retest to EMA +input double TC_PullbackRatio = 0.30; // TC: Max pullback depth (0.30 = max 30% retrace) +input double TC_BaseScore = 25; // TC: Base signal score +input bool EnableMitigationBlocks = true; // Enable Mitigation Blocks +input bool EnableOFI = true; // Enable Order Flow +input bool EnableVolumeProfile = true; // Enable Volume Profile +input bool EnableMarketMaker = true; // Enable Market Maker +input bool EnableHTFConfirmation = true; // Enable HTF Confirmation +input bool EnableSignals = true; // Enable Signals +// FAIR VALUE GAPS +input group "======== FAIR VALUE GAPS ========" +input bool ShowFVG = true; // Show FVG +input double FVG_MinSize = 20; // Min Size (points) +input double FVG_MinSizePips = 3.0; // * v9.49 FIX#203: 2.0→3.0 — quality gate for re-enabled FVG. 2.0p allowed micro-gaps that fill as noise. 3.0p = structurally significant gap (ICT: clean imbalance) +input bool FVG_UsePips = true; // Use Pips for Size +input color FVG_BullColor = clrDodgerBlue; // Bullish FVG Color +input color FVG_BearColor = clrTomato; // Bearish FVG Color +input color FVG_InverseBullColor = clrLime; // Inverse Bullish Color +input color FVG_InverseBearColor = clrOrange; // Inverse Bearish Color +input bool FVG_ShowInverse = true; // Show Inverse FVG +input bool FVG_ConvertToInverse = true; // Convert to Inverse +input bool FVG_AutoExpiry = true; // Auto Expiry +input int FVG_MaxAge = 300; // Max Age (bars) — H4 default; AdaptToTF overrides per TF +input int FVG_MaxCount = 50; // Max Count +input bool FVG_RequireDisplacement = false; // Require Displacement +input double FVG_MinStrength = 0.30; // Min Strength | * FIX#75: 0.35->0.30 (FVG 100% WR in Feb backtest -- allow more setups) +input int FVG_Transparency = 70; // Transparency (0-100) +input bool FVG_ShowCE = true; // Show CE Line +input bool FVG_ShowLabels = true; // Show Labels +input int FVG_ExtendBars = 30; // Extend Bars +input int FVG_FontSize = 8; // Font Size +input bool FVG_AlertNew = false; // Alert on New FVG +input bool FVG_AlertFill = false; // Alert on Fill +input bool FVG_AlertMitigation = false; // Alert on Mitigation +// ORDER BLOCKS +input group "======== ORDER BLOCKS ========" +input bool ShowOB = false; // Show Order Blocks +input color OB_BullColor = clrMediumSeaGreen; // Bullish OB Color +input color OB_BearColor = clrCrimson; // Bearish OB Color +input bool OB_RequireVolume = true; // Require Volume +input double OB_VolumeMultiplier = 1.4; // Volume Multiplier [v7.5b: 1.8->1.4, was killing ALL OB detection on Gold M5] +input bool OB_ShowStrength = false; // Show Strength +input int OB_MaxAge = 100; // * v9.52b: 80→100 — H4: 80 bars=13days too short for swing OBs +input bool OB_AutoExpire = true; // Auto Expire Old OBs +input bool OB_ShowBreak = false; // Show Break Markers +input bool OB_AlertMitigation = false; // Alert on Mitigation +input bool OB_ExpireOnMitigation = true; // * v9.36 FIX#154: false->true (Excel M15: OBs expire on mitigation) // Expire on Mitigation +// STRUCTURE +input group "======== STRUCTURE ========" +input bool ShowBOS = false; // Show BOS +input bool STRUCT_ShowBOS = false; // Show BOS Lines +input bool STRUCT_ShowCHoCH = false; // Show CHoCH Lines +input color STRUCT_BOSColor = clrLime; // BOS Color +input color STRUCT_CHoCHColor = clrOrange; // CHoCH Color +input int STRUCT_SwingStrength = 3; // * v9.36 FIX#154: 5->3 (Excel M15=3, was missing BOS/CHoCH signals) // Swing Strength +// LIQUIDITY +input group "======== LIQUIDITY ========" +input bool ShowLiquidity = false; // Show Liquidity +input color LIQ_BSLColor = clrRed; // BSL Color +input color LIQ_SSLColor = clrLime; // SSL Color +input int LIQ_SwingStrength = 7; // * v9.36 FIX#154: 9->7 (Excel M15=7, was missing liquidity sweeps) // Swing Strength +input bool LIQ_ShowSweeps = false; // Show Sweeps +input int LIQ_SweepBuffer = 20; // Sweep Buffer (points) +input int LIQ_MaxAge = 50; // Max Age (bars) +// OTE ZONES +input group "======== OPTIMAL TRADE ENTRY ========" +input bool ShowOTE = false; // Show OTE Zones +input int OTE_MaxAge = 150; // Max Age (bars) +input int OTE_ExtendBars = 80; // Extend Bars +input int OTE_LookbackBars = 80; // Lookback Bars +// BREAKER BLOCKS +input group "======== BREAKER BLOCKS ========" +input bool ShowBreakerBlocks = false; // Show Breaker Blocks +input int BB_MaxAge = 100; // Max Age (bars) +input int BREAKER_ExtendBars = 100; // Extend Bars +input color BREAKER_BullColor = clrTeal; // Bullish Color +input color BREAKER_BearColor = clrMaroon; // Bearish Color +// MITIGATION BLOCKS +input group "======== MITIGATION BLOCKS ========" +input bool ShowMitigationBlocks = false; // Show Mitigation Blocks +input int MB_MaxAge = 80; // Max Age (bars) +input int MITIGATION_ExtendBars = 80; // Extend Bars +input color MITIGATION_BullColor = clrMediumPurple; // Bullish Color +input color MITIGATION_BearColor = clrDarkOrange; // Bearish Color +// ORDER FLOW & VOLUME +input group "======== ORDER FLOW & VOLUME ========" +input bool ShowOrderFlowImb = false; // Show Order Flow Imbalance +input bool ShowVolumeProfile = false; // Show Volume Profile +input int VP_Period = 100; // VP Period +input int VP_Rows = 24; // VP Rows +input bool ShowMMModels = false; // Show MM Models +input bool ShowMMPhases = false; // Show MM Phases +input int MM_LookbackPeriod = 20; // MM Lookback Period +input bool MM_ShowConfidence = false; // Show MM Confidence +// PREMIUM/DISCOUNT +input group "======== PREMIUM/DISCOUNT ========" +input bool ShowPremiumDiscount = true; // Show Premium/Discount +input int PD_LookbackBars = 100; // Lookback Bars +input color PD_PremiumColor = clrMaroon; // Premium Color +input color PD_DiscountColor = clrDarkGreen; // Discount Color +// NEURAL NETWORK ML (v5.0) +input group "======== NEURAL NETWORK ML v5.0 ========" +input bool EnableML = true; // Enable ML (* v10.27 FIX#314: enabled for large backtest. NN trains on 500 H4 bars = ~83 days. Underfitting on short runs; reliable after 100+ trades.) +input ENUM_NN_OPTIMIZER NN_Optimizer = OPTIMIZER_ADAM; // Optimizer +input ENUM_NN_ACTIVATION NN_HiddenActivation = ACTIVATION_LEAKY_RELU; // Hidden Activation +input ENUM_NN_ACTIVATION NN_OutputActivation = ACTIVATION_SOFTMAX; // Output Activation +input ENUM_NN_LOSS NN_LossFunction = NN_LOSS_CATEGORICAL_CROSSENTROPY; // Loss Function +input double NN_LearningRate = 0.001; // Learning Rate +input double NN_DropoutRate = 0.3; // Dropout Rate +input double NN_L2Regularization = 0.0001; // L2 Regularization +input int NN_BatchSize = 32; // Batch Size +input int NN_MaxEpochs = 100; // Max Epochs +input int NN_EarlyStopPatience = 15; // Early Stop Patience +input bool NN_UseGradientClipping = true; // Use Gradient Clipping +input double NN_GradientClipValue = 1.0; // Gradient Clip Value +input int MLLookbackPeriod = 500; // ML Lookback Period +input double MLConfidenceThreshold = 0.60; // ML Confidence Threshold +input bool ML_AutoOptimize = true; // Auto Optimize +input int ML_OptimizeInterval = 1000; // Optimize Interval (bars) +input bool ML_SaveModel = true; // Save Model +input bool ML_LoadModel = true; // Load Model +// ML PREDICTION VISUALIZATION +input group "======== ML PREDICTION DISPLAY ========" +input bool ShowPricePrediction = false; // Show Price Prediction +input int PRED_Bars = 20; // Prediction Bars +input bool PRED_ShowBands = false; // Show Prediction Bands +input bool PRED_ShowTargets = false; // Show Targets +input bool PRED_ShowArrow = false; // Show Arrow +input int PRED_UpdateInterval = 30; // Update Interval (sec) +input color PRED_BullColor = clrLime; // Bullish Color +input color PRED_BearColor = clrRed; // Bearish Color +// PROBABILITY HEATMAP +input group "======== PROBABILITY HEATMAP ========" +input bool ShowProbabilityHeatmap = false; // Show Heatmap +input int HEATMAP_Rows = 20; // Heatmap Rows +input int HEATMAP_Bars = 15; // Heatmap Bars +input bool HEATMAP_ShowLegend = false; // Show Legend +input int HEATMAP_Transparency = 70; // Transparency +input color HEAT_VeryHigh = clrLime; // Very High Color +input color HEAT_High = clrYellow; // High Color +input color HEAT_Medium = clrOrange; // Medium Color +input color HEAT_Low = clrRed; // Low Color +input color HEAT_VeryLow = clrDarkGray; // Very Low Color +// ICT KILLZONES (v5.0) +input group "======== ICT KILLZONES v5.0 ========" +input bool EnableKillzones = true; // Enable Killzones +input bool KZ_EnableAsian = false; // Enable Asian +input bool KZ_EnableLondonOpen = true; // Enable London Open +input bool KZ_EnableLondonClose = true; // Enable London Close +input bool KZ_EnableNYOpen = true; // Enable NY Open +input bool KZ_EnableNYLunch = true; // Enable NY Lunch +input bool KZ_EnableNYClose = true; // Enable NY Close +input bool KZ_EnableSilverBullet = true; // Enable Silver Bullet +input bool KZ_ShowAsianSession = false; // Show Asian Session +input bool KZ_ShowLondonOpen = true; // Show London Open +input bool KZ_ShowLondonClose = false; // Show London Close +input bool KZ_ShowNYOpen = false; // Show NY Open +input bool KZ_ShowNYLunch = false; // Show NY Lunch +input bool KZ_ShowNYClose = false; // Show NY Close +input bool KZ_ShowSilverBullet = false; // Show Silver Bullet +input bool KZ_ShowBoxes = false; // Show Boxes +input bool KZ_ShowLabels = false; // Show Labels +input color KZ_AsianColor = clrMidnightBlue; // Asian Color +input color KZ_LondonColor = clrDarkGreen; // London Color +input color KZ_NewYorkColor = clrMaroon; // NY Color +input color KZ_NYColor = clrMaroon; // NY Color (alias) +input color KZ_SilverBulletColor = clrGold; // Silver Bullet Color +input int KZ_Transparency = 80; // Transparency +input bool KZ_AlertOnEntry = true; // Alert on Entry +input bool KZ_AlertOnBreakout = true; // Alert on Breakout +input bool KZ_OnlyTradeInKillzones = false; // Only Trade in Killzones +input bool SB_EnableLondon = true; // Silver Bullet: London (03:00-04:00 NY) +input bool SB_EnableAMNY = true; // Silver Bullet: AM NY (10:00-11:00 NY) +input bool SB_EnablePMNY = true; // Silver Bullet: PM NY (14:00-15:00 NY) +input int SB_NYOffset = -5; // NY Time Offset (-5 EST, -4 EDT) +input int SB_MaxAge = 30; // Silver Bullet Max Age (bars) +input color SB_ZoneColor = clrGold; // Silver Bullet Zone Color +// DST & TIMEZONE (v5.0) +input group "======== TIMEZONE & DST ========" +input ENUM_DST_MODE DST_Mode = DST_AUTO_DETECT; // DST Mode +input ENUM_TIMEZONE BrokerTimezone = TZ_BROKER_TIME; // Broker Timezone +input int ManualUTCOffset = 0; // Manual UTC Offset +input bool AutoDetectBrokerOffset = true; // Auto Detect Offset +input bool ShowTimezoneInfo = false; // Show Timezone Info +// COST ANALYSIS +input group "======== [MONEY] COST ANALYSIS ========" +input bool EnableCostAnalysis = true; // Enable Cost Analysis +input ENUM_COST_MODE COST_Mode = COST_MODE_PER_LOT; // Commission Mode +input ENUM_SLIPPAGE_MODE COST_SlippageMode = SLIP_MODE_FIXED; // Slippage Mode +input double COST_CommissionPerLot = 7.0; // Commission per lot ($) +input bool COST_CommissionRoundTrip = true; // Commission is round-trip +input double COST_ExpectedSlippage = 3.0; // Expected slippage (points) +input double COST_MaxSpreadPoints = 500.0; // Max acceptable spread (points) - 50 pips for XAU/USD +input double COST_MaxCostPercent = 25.0; // Max cost as % of SL +input bool COST_IncludeSwap = true; // Include swap in cost +input bool COST_AdjustTP = true; // Adjust TP for costs +input bool COST_RejectHighCost = true; // Reject high cost trades +input int SPREAD_SampleSize = 100; // Spread sample size +input double SPREAD_AlertMultiplier = 2.0; // Spread alert multiplier +input bool SPREAD_Monitor = true; // Monitor spread +input bool SPREAD_BlockHighSpread = true; // Block trades on high spread +// PAIR OPTIMIZATION +input group "======== [TARGET] PAIR OPTIMIZATION ========" +input bool PAIR_OptimizationEnabled = true; // Pair Optimization Active +input bool PAIR_AutoDetect = true; // Auto Detect Pair Profile +input bool PAIR_UseOptimalSettings = true; // Use Optimal Settings +input bool PAIR_OverrideStrategies = true; // Override Strategy Settings +input bool PAIR_SessionFilter = true; // Filter by Optimal Session +input bool PAIR_AdaptToVolatility = true; // Adapt to Volatility +input double PAIR_ManualMinConf = 0; // Manual Min Confluence (0=auto) +input string PAIR_ManualCategory = "AUTO"; // Manual Category +// SIGNAL QUALITY +input group "======== SIGNAL QUALITY SYSTEM ========" +input bool EnablePerformanceTracking = true; // Enable Performance Tracking +// SIGNALS +input group "======== SIGNALS ========" +input bool ShowSignals = false; // Show Signals +input double MinConfluence = 4; // Min Confluence +input color SIGNAL_BuyColor = clrLime; // Buy Color +input color SIGNAL_SellColor = clrRed; // Sell Color +input bool SIGNAL_ShowLevels = false; // Show Levels +input bool EnableAlerts = true; // Enable Alerts +// SIGNAL MANAGEMENT +input group "======== SIGNAL MANAGEMENT ========" +input int SignalExpiryHours = 8; // * v9.36 FIX#154: 24->8 (Excel M15=8, stale signals causing bad entries) // Signal Expiry (hours) +input int SignalExpiryBars = 16; // * v9.36 FIX#154: 24->16 (Excel M15=16, stale signals causing bad entries) // Signal Expiry (bars) +input bool AutoCleanExpiredSignals = true; // Auto Clean Expired +input bool ShowSignalButtons = false; // Show Buttons +input bool ShowSignalAge = false; // Show Age // [v6.42] Signal age display +input color ExpiredSignalColor = clrGray; // Expired Color +// STRATEGIES +input group "======== STRATEGIES ========" +input bool STRATEGY_FVG_Entry = true; // FVG Entry +input bool STRATEGY_OB_Entry = true; // OB Entry +input bool STRATEGY_BOS_Retest = true; // BOS Retest +input bool STRATEGY_LIQ_Grab = true; // Liquidity Grab +input bool STRATEGY_OTE_Entry = true; // OTE Entry +input bool STRATEGY_BB_Entry = true; // Breaker Block Entry +input bool STRATEGY_MB_Entry = true; // Mitigation Block Entry +input bool STRATEGY_MM_Model = true; // Market Maker Model +input bool STRATEGY_ML_Signal = true; // ML Signal +input bool STRATEGY_Killzone = true; // Killzone Strategy +// FILTERS +input group "======== FILTERS ========" +input bool UseATRFilter = false; // Use ATR Filter (* v9.09: false -- AutoOpt handles volatility. Old values in wrong units) +input double ATRMinValue = 0.0001; // ATR Min Value +input double ATRMaxValue = 0.01; // ATR Max Value +input bool UseRSIFilter = true; // Use RSI Filter +input int RSIPeriod = 14; // RSI Period +input bool UseCandleFilter = true; // Use Candle Filter +input bool UseSessionFilter = true; // Use Session Filter +input bool SessionLondon = true; // London Session +input bool SessionNewYork = true; // New York Session +input bool SessionAsian = true; // Asian Session +// DATA PERSISTENCE (v5.0) +input group "======== DATA PERSISTENCE v5.0 ========" +input bool EnableDataPersistence = true; // Enable Persistence +input bool AutoSavePerformance = true; // Auto Save Performance // [v6.42] Used in data persistence +input int AutoSaveInterval = 5; // Auto Save Interval (min) +input bool LoadPerformanceOnStart = true; // Load on Start +input bool BackupBeforeSave = true; // Backup Before Save +input string DataFolder = "ICT_Data"; // Data Folder +// BACKTESTING (v5.0) +input group "======== BACKTESTING v5.0 ========" +input ENUM_BACKTEST_MODE BacktestMode = BACKTEST_FULL; // Backtest Mode +input ENUM_OPTIMIZATION_TARGET OptimizationTarget = OPT_EXPECTANCY; // Optimization Target +// * v9.31 FIX#113: Multi-TF Optimizer -- set as optimization param range M5->D1 step 1 +// Chart TF in Tester must be M5. Passes where _Period != Opt_Timeframe are auto-rejected. +// PERIOD_CURRENT (0) = disabled (legacy: EA runs on whatever chart TF is set). +input ENUM_TIMEFRAMES Opt_Timeframe = PERIOD_M5; // [FIX#113/117] TF filter for optimizer. Default=M5. PERIOD_CURRENT=disable filter. +// BacktestInitialBalance REMOVED (FIX#480) — g_effectiveBIB always reads AccountInfoDouble(ACCOUNT_BALANCE) +input bool BacktestShowEquityCurve = false; // Show Equity Curve +input bool ExportBacktestResults_Enabled = true; // Export Results +input int MonteCarloSimulations = 1000; // Monte Carlo Simulations +input bool EnableWalkForward = false; // Enable Walk Forward // [v6.42] Walk-forward optimization +input int WalkForwardWindow = 250; // Walk Forward Window +input int WalkForwardStep = 50; // Walk Forward Step +input bool ShowEquityCurve = false; // Show Equity Curve +// VISUALIZATION +input group "======== VISUALIZATION ========" +//--- Professional Dashboard Settings +input bool Dash_Enabled = true; // Enable Dashboard +input bool Dash_ShowMarketPanel = false; // Show Market Overview Panel +input bool Dash_ShowICTPanel = false; // Show ICT Concepts Panel +input bool Dash_ShowSignalPanel = false; // Show Signals Panel +input bool Dash_ShowPerformancePanel = false; // Show Performance Panel +input bool Dash_ShowEntryScorePanel = false; // Show Entry Score Panel +input bool Dash_ShowMultiTPPanel = false; // Show Multi-TP Panel +input bool Dash_ShowNewFeaturesPanel = false; // Show New Features Panel +input bool Dash_Minimized = false; // Start Minimized +input int Dash_Transparency = 255; // Panel Transparency (0=transparent 255=opaque) +input color Dash_PanelColor = C'25,25,35'; // Panel Background Color +input color Dash_BorderColor = C'60,60,80'; // Panel Border Color +input color Dash_TitleColor = clrGold; // Title Color +input color Dash_TextColor = clrWhite; // Text Color +input color Dash_BullColor = clrLime; // Bullish Color +input color Dash_BearColor = clrRed; // Bearish Color +input color Dash_NeutralColor = clrGray; // Neutral Color +//--- Legacy Dashboard (for compatibility) +input bool ShowDashboard = false; // Show Dashboard (Legacy) +input ENUM_BASE_CORNER DashboardCorner = CORNER_LEFT_UPPER; // Dashboard Corner +input int DASH_XOffset = 20; // Dashboard X Offset +input int DASH_YOffset = 30; // Dashboard Y Offset +input int DashboardFontSize = 9; // Dashboard Font Size +//--- Other Visualization +input bool ShowFibonacci = false; // Show Fibonacci +input int FIB_LookbackBars = 100; // Fib Lookback Bars +input bool ShowTradeChecklist = false; // Show Trade Checklist // [v6.42] Checklist display +input bool ShowSignalDetails = false; // Show Signal Details +input bool DrawLabels = false; // Draw Labels +input bool DrawArrows = false; // Draw Arrows +input bool ShowZoneBoxes = true; // Show Zone Boxes +input bool ShowZoneLines = true; // Show Zone Lines +// ALERTS & NOTIFICATIONS +input group "======== ALERTS ========" +input bool EnableSoundAlerts = true; // Enable Sound Alerts +input bool AlertOnApproachingEntry = true; // Alert Approaching Entry +input double ApproachingEntryPips = 5.0; // Approaching Entry (pips) +input bool AlertOnSignalExpiry = false; // Alert Signal Expiry +input bool AlertOnHighQuality = true; // Alert High Quality +input double HighQualityThreshold = 75.0; // High Quality Threshold +input bool EnablePushNotifications = false; // Enable Push +input bool EnableEmailAlerts = false; // Enable Email +input bool BOS_Alert = true; // BOS Alert +input bool Signal_AlertNew = true; // New Signal Alert +input bool Signal_AlertTP = true; // TP Alert +input bool Signal_AlertSL = true; // SL Alert +// TRADE JOURNAL +input group "======== TRADE JOURNAL ========" +input bool EnableTradeJournal = true; // Enable Journal +input bool JournalLogAllSignals = true; // Log All Signals +input bool LogScreenshots = false; // Log Screenshots // [v6.42] Screenshot logging +input string JournalFilename = "ICT_Trade_Journal.csv"; // Journal Filename +// PERFORMANCE SETTINGS +input group "======== PERFORMANCE ========" +input int MaxBarsToShow = 500; // Max Bars to Show +input int ObjectCleanupInterval = 600; // Object Cleanup (sec) +input int ObjectMaxAge = 7; // Object Max Age (days) +input int ArrayCompactInterval = 3600; // Array Compact (sec) +input int MLUpdateInterval = 120; // ML Update (sec) +input int MaxFVGsToKeep = 20; // Max FVGs to Keep +input int MaxOBsToKeep = 15; // Max OBs to Keep +input int MaxSignalsToKeep = 20; // Max Signals to Keep +input int MaxLiquidityLevels = 12; // Max Liquidity Levels +// DEBUG & TESTING +input group "======== DEBUG ========" +input bool ShowProcessingTime = false; // Show Processing Time +input bool LogMemoryUsage = false; // Log Memory Usage +input int MemoryLogInterval = 3600; // Memory Log (sec) +input bool EnableTestMode = false; // Enable Test Mode +input bool VerboseMLLogging = false; // Verbose ML Logging +// FULL FIBONACCI SETTINGS (EXTENDED) +input group "======== [RULER] FULL FIBONACCI ========" +input bool FIB_Enabled = true; // Enable Full Fibonacci +input ENUM_FIB_MODE FIB_Mode = FIB_MODE_AUTO; // Fibonacci Mode +input ENUM_FIB_STYLE FIB_Style = FIB_STYLE_FULL; // Fibonacci Style +input ENUM_FIB_EXTENSION FIB_Extensions = FIB_EXT_STANDARD; // Extension Levels +input int FIB_SwingStrength = 5; // Swing Detection Strength +input bool FIB_ShowLabels = true; // Show Level Labels +input bool FIB_ShowPrices = true; // Show Prices on Labels +input bool FIB_HighlightOTE = true; // Highlight OTE Zone +input bool FIB_ShowCurrentPrice = true; // Show Current Price Level +input bool FIB_AutoUpdate = true; // Auto Update on New Swing +input int FIB_ExtendRight = 50; // Extend Right (bars) +input group "======== [RULER] FIBONACCI COLORS ========" +input color FIB_Color_0 = clrGray; // 0% Color +input color FIB_Color_236 = clrDarkGray; // 23.6% Color +input color FIB_Color_382 = clrSilver; // 38.2% Color +input color FIB_Color_500 = clrYellow; // 50% (EQ) Color +input color FIB_Color_618 = clrGold; // 61.8% Color (OTE) +input color FIB_Color_705 = clrOrange; // 70.5% Color (Optimal) +input color FIB_Color_786 = clrOrangeRed; // 78.6% Color (OTE) +input color FIB_Color_100 = clrRed; // 100% Color +input color FIB_Color_Extensions = clrDodgerBlue; // Extension Colors +input color FIB_OTE_ZoneColor = clrDarkSlateGray; // OTE Zone Background +// CRT (CANDLE RANGE THEORY) +input group "======== CRT (CANDLE RANGE THEORY) ========" +input bool CRT_Enabled = true; // Enable CRT Detection +input bool CRT_ShowOnChart = false; // Show CRT on Chart +input bool CRT_ShowProjections = true; // Show CRT Projections +input bool CRT_UseForTP = true; // Use CRT for Take Profit +input double CRT_Multiplier = 1.0; // CRT Range Multiplier +input double CRT_MinRange = 0.5; // Min Range Size (xATR) +input double CRT_MinRangeATR = 0.8; // Min Range ATR +input double CRT_MaxRangeATR = 3.0; // Max Range ATR +input int CRT_LookbackBars = 50; // Lookback Bars +input int CRT_ExpiryBars = 30; // Expiry Bars +input bool CRT_RequireBreak = true; // Require Break +input bool CRT_RequireRetrace = true; // Require Retrace +input double CRT_ProjectionMultiplier = 1.0; // Projection Multiplier +input double CRT_ExtensionLevel = 1.618; // Extension Level +input bool CRT_RequireConfluence = false; // Require Confluence +input int CRT_MinConfluence = 2; // Min Confluence +input color CRT_Color = clrDeepPink; // CRT Color +input color CRT_BullColor = clrDodgerBlue; // Bull Color +input color CRT_BearColor = clrCrimson; // Bear Color +input color CRT_ProjectionColor = clrGold; // Projection Color +input int CRT_Transparency = 70; // Transparency +// TBS (TURTLE SOUP / FALSE BREAKOUT) +input group "======== TBS (TURTLE SOUP) ========" +input bool TBS_Enabled = true; // Enable TBS Detection +input bool TBS_ShowOnChart = false; // Show TBS on Chart +input bool TBS_ShowSetups = true; // Show TBS Setups +input int TBS_Lookback = 12; // TBS Lookback Period +input int TBS_LiquidityLookback = 20; // Liquidity Lookback +input double TBS_MinWickRatio = 0.35; // TBS Min Wick Ratio +input double TBS_MinSweepATR = 0.2; // Min Sweep ATR +input double TBS_MaxSweepATR = 1.5; // Max Sweep ATR +input int TBS_ConfirmationBars = 3; // Confirmation Bars +input int TBS_ExpiryBars = 15; // Expiry Bars +input bool TBS_RequireOB = false; // Require OB +input bool TBS_RequireOTE = false; // Require OTE +input bool TBS_RequireKillzone = false; // Require Killzone +input double TBS_DefaultSL_ATR = 1.5; // Default SL ATR +input double TBS_TP1_RR = 1.0; // TP1 Risk:Reward +input double TBS_TP2_RR = 2.0; // TP2 Risk:Reward +input double TBS_TP3_RR = 3.0; // TP3 Risk:Reward +input color TBS_BullColor = clrLime; // TBS Bull Color +input color TBS_BearColor = clrRed; // TBS Bear Color +input color TBS_SweepColor = clrMagenta; // Sweep Color +// AMD (ACCUMULATION-MANIPULATION-DISTRIBUTION) +input group "======== AMD PHASES ========" +input bool AMD_Enabled = true; // Enable AMD Detection +input bool AMD_ShowOnChart = false; // Show AMD on Chart +input bool AMD_ShowPhases = true; // Show AMD Phases +input bool AMD_AutoDetect = true; // Auto Detect +input int AMD_AccumMinBars = 4; // Accumulation Min Bars (short) +input int AMD_AccumMaxBars = 50; // Accumulation Max Bars +input double AMD_AccumMaxRange = 1.5; // Accumulation Max Range +input double AMD_ManipMoveATR = 0.8; // Manipulation Move (xATR) +input double AMD_ManipMinSweep = 0.3; // Manipulation Min Sweep +input int AMD_ManipMaxBars = 10; // Manipulation Max Bars +input double AMD_DistMinMove = 1.0; // Distribution Min Move +input bool AMD_UseSessionTiming = true; // Use Session Timing +input int AMD_AsianStartHour = 0; // Asian Start Hour +input int AMD_AsianEndHour = 8; // Asian End Hour +input int AMD_LondonStartHour = 7; // London Start Hour +input int AMD_NYStartHour = 12; // NY Start Hour +input bool AMD_TradeInDist = true; // Trade in Distribution +input bool AMD_TradeInManip = true; // Trade in Manipulation +input bool AMD_AvoidAccum = true; // Avoid Accumulation +input color AMD_AccumColor = clrYellow; // Accumulation Color +input color AMD_ManipColor = clrOrange; // Manipulation Color +input color AMD_DistColor = clrCyan; // Distribution Color +// JUDAS SWING +input group "======== JUDAS SWING ========" +input bool Judas_Enabled = true; // Enable Judas Swing Detection +input bool Judas_ShowOnChart = false; // Show Judas on Chart +input bool Judas_ShowSwings = true; // Show Judas Swing +input bool Judas_RequireForEntry = false; // Require Judas for Entry +input bool Judas_LondonEnabled = true; // London Enabled +input bool Judas_NYEnabled = true; // NY Enabled +input int Judas_LondonStartHour = 2; // London Start Hour +input int Judas_LondonEndHour = 5; // London End Hour +input int Judas_NYStartHour = 12; // NY Start Hour +input int Judas_NYEndHour = 15; // NY End Hour +input double Judas_MinMove = 0.5; // Min Judas Move (xATR) +input double Judas_MinFakeMove = 0.5; // Min Fake Move +input double Judas_MaxFakeMove = 2.0; // Max Fake Move +input bool Judas_RequireSweep = true; // Require Sweep +input bool Judas_RequireAsianSweep = true; // Require Asian Sweep +input bool Judas_RequireReversal = true; // Require Reversal +input double Judas_SL_ATR = 1.0; // SL ATR +input double Judas_TP1_RR = 1.5; // TP1 Risk:Reward +input double Judas_TP2_RR = 2.5; // TP2 Risk:Reward +input double Judas_TP3_RR = 4.0; // TP3 Risk:Reward +input color Judas_Color = clrOrangeRed; // Judas Swing Color +input color Judas_BullColor = clrLime; // Bull Color +input color Judas_BearColor = clrRed; // Bear Color +input color Judas_ArrowColor = clrGold; // Arrow Color +// MULTIPLE TAKE PROFITS +input group "=============== MULTIPLE TAKE PROFITS ===============" +input bool InpEnableMultiTP = true; // Enable Multiple TP Levels +input double InpTP1_RR = 2.0; // * v9.02: reverted to 2.0 (sync with TP1_ATR) +input double InpTP2_RR = 2.5; // TP2 Risk:Reward Ratio +input double InpTP3_RR = 4.0; // TP3 Risk:Reward Ratio +input color InpTP1Color = clrLime; // TP1 Line Color +input color InpTP2Color = clrSpringGreen; // TP2 Line Color +input color InpTP3Color = clrDarkGreen; // TP3 Line Color +input int InpTPLineWidth = 2; // TP Line Width +input ENUM_LINE_STYLE InpTPLineStyle = STYLE_DASH; // TP Line Style +input bool InpShowTPLabels = true; // Show TP Labels +input bool InpShowTPPercent = true; // Show Position % on Labels +input bool InpEnableTPAlerts = true; // Enable TP Hit Alerts +input bool InpAutoAdjustTPToCRT = false; // Auto-adjust TP to CRT levels +input bool InpAutoAdjustTPToFVG = false; // Auto-adjust TP to FVG levels +input bool InpAutoAdjustTPToLiquidity = false; // Auto-adjust TP to Liquidity +// SMART ENTRY SYSTEM +input group "======== SMART ENTRY SYSTEM ========" +input bool SmartEntry_Enabled = true; // Enable Smart Entry System +input bool SmartEntry_AdaptToRegime = true; // Adapt Strategy to Market Regime +input int SmartEntry_MinConfidence = 36; // * FIX#17: 65->36 (new max=85) +input bool SmartEntry_UseWinProb = true; // Use Win Probability Filter +input double SmartEntry_MinWinProb = 40.0; // * v9.01 FIX: 45->40 (EURUSD M15 ranging: valid signals with WinP=42-44% and EV=1-4R were rejected) +input double SmartEntry_MinExpValue = 0.15; // Minimum Expected Value (R) [v6.38: 0.35->0.15, was useless at 0.35 since WP always 50-68%] +input bool SmartEntry_AdaptiveSize = true; // Enable Adaptive Position Sizing +input bool SmartEntry_ShowInfo = true; // Show Smart Info on Chart +input bool SmartEntry_LogDecisions = true; // Log Smart Decisions to Journal +// * FIX#349: REMOVED FIX#60 SmartEntry TrendBypass inputs. +// REASON: Fully redundant — the "WEAK regime blocks" problem it solved is already handled by: +// FIX#176+225+343: CT-WEAK exemption at SelectBestCandidate (all TFs) +// FIX#232: SmartEntry CT block skipped when FIX#176+225 approved +// pair table min_conf: each TF has calibrated floor (M15=60, H1=50, H4=44) +// HARM: withTrend via MTF_STRONG_BULL check fired even on CT-regime trades +// (WEAK_DOWN + BUY + STRONG_BULL) → TREND BYPASS entered with WP=47%, EV=-0.06R +// On ALL TFs (H4/H1/M15/M5) — not M15-only problem. +// bool SmartEntry_TrendBypass removed — variable kept as compile reference below. +// MARKET REGIME SETTINGS +input group "======== MARKET REGIME SETTINGS ========" +input bool Regime_Enabled = true; // Enable Regime Detection +input bool Regime_ShowOnChart = true; // Show Regime on Chart +input int Regime_Lookback = 18; // Regime Detection Lookback +input int Regime_ADXPeriod = 14; // ADX Period +input double Regime_TrendThreshold = 48.0; // Trend Strength Threshold +input double Regime_TrendADXMin = 25.0; // Trend ADX Min +input double Regime_StrongTrendADX = 40.0; // Strong Trend ADX +input double Regime_VolatileThresh = 2.2; // Volatility Threshold (xATR) +input double Regime_RangeATRRatio = 0.5; // Range ATR Ratio +input int Regime_ConfirmBars = 3; // * v8.08: was 2. Bars to Confirm Regime Change (3 = more stable) +input int Regime_TrendingMinConf = 48; // Min Confidence in Trending +input int Regime_RangingMinConf = 58; // Min Confidence in Ranging +input int Regime_VolatileMinConf = 70; // Min Confidence in Volatile +input int Regime_BreakoutMinConf = 52; // Min Confidence in Breakout +input bool Regime_AdaptStrategy = true; // Adapt Strategy +input bool Regime_AdjustSL = true; // Adjust SL +input bool Regime_AdjustTP = true; // Adjust TP +input bool Regime_AdjustPosition = true; // Adjust Position +input double Regime_TrendRR = 3.0; // Trend R:R +input double Regime_RangeRR = 1.5; // Range R:R +input double Regime_VolatileRR = 2.0; // Volatile R:R +input bool Regime_AllowCounterTrend = false; // Allow Counter Trend +input bool Regime_UseMeanReversion = true; // Use Mean Reversion +// DIVERGENCE DETECTION +input group "======== DIVERGENCE DETECTION ========" +input bool Divergence_Enabled = true; // Enable Divergence Detection +input bool Divergence_ShowOnChart = true; // Show Divergence on Chart +input bool Divergence_UseForConfirm = true; // Use Divergence for Entry Confirm +input int Divergence_RSIPeriod = 14; // RSI Period +input int Divergence_Lookback = 20; // Divergence Lookback Bars +input int Divergence_MinBars = 4; // Min Bars Between Pivots +input double Divergence_ScoreBonus = 20.0; // Score Bonus for Divergence +input color Divergence_BullColor = clrLime; // Bullish Divergence Color +input color Divergence_BearColor = clrMagenta; // Bearish Divergence Color +input color Divergence_HiddenColor = clrOrange; // Hidden Divergence Color +input bool Divergence_ShowLines = true; // Show Divergence Lines on Chart +input bool Divergence_ShowArrows = true; // Show Divergence Arrows +input bool Divergence_DetectHidden = true; // Detect Hidden Divergences +input double Divergence_ExpiryATR = 1.5; // Invalidation Distance (x ATR) +// AUTO TRENDLINES +input group "======== AUTO TRENDLINES ========" +input bool Trendline_Enabled = true; // Enable Auto Trendlines +input bool Trendline_ShowOnChart = true; // Show Trendlines on Chart +input bool Trendline_ShowBreaks = true; // Show Trendline Breaks +input bool Trendline_UseForConfirm = true; // Use Trendline for Confirmation +input int Trendline_MinTouches = 2; // Min Touches for Valid Trendline +input int Trendline_Lookback = 30; // Trendline Lookback Period +input double Trendline_Tolerance = 0.4; // Touch Tolerance (x ATR) +input double Trendline_ScoreBonus = 10.0; // Score Bonus for Trendline Confirm +input color Trendline_BullColor = clrDodgerBlue; // Bullish Trendline Color +input color Trendline_BearColor = clrCrimson; // Bearish Trendline Color +input color Trendline_BreakColor = clrYellow; // Trendline Break Color +input bool Trendline_ShowBreakArrow = true; // Show Trendline Break Arrow +input int Trendline_MaxAge = 100; // Max Trendline Age (bars) +input int Trendline_BreakExpiry = 50; // Break Marker Expiry (bars) +input bool Trendline_ShowZones = true; // Show Trendline Zones +// ============================================================ +// NEWS FILTER - MULTI-ASSET OPTIMIZED v6.41 +// ============================================================ +input group "======== NEWS FILTER ========" +input bool News_FilterEnabled = true; // Enable News Filter +input bool News_ShowOnChart = true; // Show News on Chart +input bool News_AvoidHighImpact = true; // Avoid High Impact News +input bool News_AvoidMediumImpact = false; // Avoid Medium Impact News +input int News_MinsBeforeHigh = 30; // Minutes Before High Impact +input int News_MinsAfterHigh = 30; // Minutes After High Impact +input int News_MinsBeforeMedium = 15; // Minutes Before Medium Impact +input int News_MinsAfterMedium = 15; // Minutes After Medium Impact +input bool News_CheckUSD = true; // Check USD (CRITICAL!) +input bool News_CheckEUR = true; // Check EUR (EURUSD) +input bool News_CheckGBP = true; // Check GBP (GBPUSD) +input bool News_CheckJPY = true; // Not in portfolio +input bool News_CheckCHF = true; // Not in portfolio +input bool News_CheckAUD = true; // Not in portfolio +input bool News_CheckCAD = true; // Not in portfolio +input bool News_CheckNZD = true; // Not in portfolio +input double News_ReducedRisk = 0.7; // Reduced Risk (was 0.5) +input bool News_CloseBeforeNews = false; // Close Before News +input color News_HighColor = clrRed; // High Impact Color +input color News_MediumColor = clrOrange; // Medium Impact Color +input color News_LowColor = clrYellow; // Low Impact Color +// ============================================================ +// CORRELATION FILTER - MULTI-ASSET PORTFOLIO v6.41 +// ============================================================ +input group "======== CORRELATION FILTER ========" +input bool Corr_FilterEnabled = true; // Enable Correlation Filter +input bool Corr_ShowOnChart = true; // Show Correlation on Chart // [v6.42] Correlation display +input int Corr_Period = 100; // Correlation Period (was 50) +input double Corr_HighThreshold = 0.7; // High Threshold +input double Corr_MaxExposure = 2.0; // Max Exposure (was 3.0) +input int Corr_MaxCorrelatedPos = 2; // Max Correlated Positions (was 3) +input bool Corr_CheckBeforeEntry = true; // Check Before Entry +input bool Corr_ReduceIfCorrelated = true; // Reduce If Correlated +input double Corr_ReductionFactor = 0.6; // Reduction Factor (was 0.5) +input bool Corr_PreventOverexposure = true; // Prevent Overexposure +input bool Corr_CheckDXY = true; // Check DXY +input string Corr_Pairs = "EURUSD,GBPUSD,XAUUSD"; // Pairs to Check (* v9.25: removed US500,US100 -- not available on FTMO-Demo, caused 2000+ errors/session) +input int Corr_UpdateMinutes = 60; // Update Minutes // [v6.42] Update frequency +// TIME OF DAY ANALYSIS +// ============================================================ +// TIME OF DAY ANALYSIS - CLEANED v6.41 +// ============================================================ +input group "======== TIME OF DAY ANALYSIS ========" +input bool Time_AnalysisEnabled = true; // Enable Time Analysis +input bool Time_ShowOnChart = true; // Show Time Stats on Chart // [v6.42] Time stats display +input bool Time_FilterByHour = true; // Filter By Hour +input bool Time_FilterByDay = true; // Filter By Day +input double Time_MinWinRate = 0.50; // Min Win Rate (50%) +input int Time_MinSamples = 30; // Min Samples per Hour +input bool Time_AvoidWorstHours = true; // Avoid Worst Hours +input bool Time_PreferBestHours = true; // Prefer Best Hours // [v6.42] Best hours preference +input double Time_BestHourBonus = 1.15; // Best Hour Bonus +input double Time_WorstHourPenalty = 0.7; // Worst Hour Penalty +input bool Time_AvoidFridayPM = true; // Avoid Friday PM +input bool Time_AvoidMondayAM = true; // Avoid Monday AM +input bool Time_AvoidWeekends = true; // Avoid Weekends +input int Time_AvoidHoursStart = 22; // Dead Zone Start (10 PM) +input int Time_AvoidHoursEnd = 1; // Dead Zone End (1 AM) +// ENTRY QUALITY SCORING +input group "======== ENTRY QUALITY SCORING ========" +input bool Scoring_Enabled = true; // Enable Entry Quality Scoring +input bool Scoring_ShowOnChart = true; // Show Score on Chart +// CONFLUENCE FILTER +input group "======== CONFLUENCE FILTER ========" +input bool Confluence_Enabled = true; // Enable Confluence Detection +input bool Confluence_Required = false; // REQUIRE Confluence for Entry +input int Confluence_MinZones = 2; // Min Overlapping Zones (2-5) +input bool Confluence_OB_FVG = true; // Check OB + FVG Confluence +input bool Confluence_OB_OTE = true; // Check OB + OTE Confluence +input bool Confluence_FVG_OTE = true; // Check FVG + OTE Confluence +// ============================================================ +// WIN PROBABILITY - FIXED WEIGHTS v6.41 +// ============================================================ +input group "======== WIN PROBABILITY ========" +input bool WinProb_Enabled = true; // Enable Win Probability +input bool WinProb_ShowOnChart = true; // Show on Chart // [v6.42] WinProb display +input int WinProb_LookbackTrades = 150; // Lookback Trades +input int WinProb_MinSamples = 30; // Min Samples +input double WinProb_TrendWeight = 0.25; // Trend Weight +input double WinProb_StructureWeight = 0.20; // Structure Weight +input double WinProb_ZoneWeight = 0.15; // Zone Weight +input double WinProb_ConfluenceWeight = 0.15; // Confluence Weight (was 0.17) +input double WinProb_TimingWeight = 0.13; // Timing Weight +input double WinProb_PatternWeight = 0.12; // Pattern Weight +input double WinProb_MinThreshold = 0.50; // * v9.44 FIX#182: 0.60->0.50 | H4 GBPUSD: valid signals (WP=56-58%) blocked by 0.60 floor. 0.50 = coin-flip floor (EV still positive with 1.3+ R:R). +input double WinProb_HighThreshold = 0.75; // High Threshold +input bool WinProb_AdjustForNews = true; // Adjust For News +input bool WinProb_AdjustForTime = true; // Adjust For Time +input bool WinProb_AdjustForRegime = true; // Adjust For Regime +// EXPECTED VALUE +input group "======== EXPECTED VALUE ========" +input bool EV_Enabled = true; // Enable Expected Value +input bool EV_ShowOnChart = true; // Show on Chart // [v6.42] EV display +input double EV_MinPositive = 0.35; // Min Positive EV +input double EV_HighEVThreshold = 0.35; // High EV Threshold +input bool EV_UseKellyCriterion = true; // * FIX#389: enabled — EV Kelly cross-checks PosSize Kelly. Uses EV_KellyFraction=0.15 (more conservative than PosSize 0.25). +input double EV_KellyFraction = 0.15; // Kelly Fraction +input double EV_DefaultAvgWin = 2.0; // Default Avg Win +input double EV_DefaultAvgLoss = 1.0; // Default Avg Loss +input bool EV_RequirePositiveEV = true; // Require Positive EV +input bool EV_AdjustPosition = true; // Adjust Position +input double EV_MaxPositionMultiplier = 1.3; // Max Position Multiplier +input double EV_MinPositionMultiplier = 0.6; // Min Position Multiplier +// POSITION SIZING +input group "======== POSITION SIZING ========" +input bool PosSize_Enabled = true; // Enable Position Sizing +// * v9.55 FIX#221: EA_RiskPercent = το MAX που βάζεις εσύ. +// Το EA κατεβάζει αυτόματα ανάλογα με την ποιότητα του trade: +// A+ (score≥90): 100% του max → max lots (καλύτερο setup) +// A (score≥72): 75% του max +// B (score≥55): 55% του max +// C/D (score<55): 40% του max → αλλά ΠΟΤΕ κάτω από 1% +// Παράδειγμα: EA_RiskPercent=2.5% → A+=2.5%, A=1.875%, B=1.375%, C/D=1.0% (floor) +// Δεν χρειάζεσαι άλλο input για risk — αλλάζεις ΜΟΝΟ το EA_RiskPercent. +input bool PosSize_WinStreak_Uncap = true; // Win streak: remove multiplier cap for A/A+ +input double PosSize_MinRisk = 1.0; // Min Risk % — hard floor (ποτέ κάτω από αυτό) +input bool PosSize_UseConfidence = true; // Use Confidence +input double PosSize_ConfidenceScale = 0.7; // Confidence Scale +input bool PosSize_UseRegime = true; // Use Regime +input double PosSize_TrendingBonus = 1.2; // Trending Bonus +input double PosSize_RangingPenalty = 0.7; // Ranging Penalty +input bool PosSize_UseStreak = true; // Use Streak +input int PosSize_StreakLookback = 10; // Streak Lookback +input double PosSize_WinStreakBonus = 0.15; // * v9.16: 0.10->0.15 Win Streak Bonus (1W=+15%, 2W=+30%, 3W=+45%) +input double PosSize_LossStreakCut = 0.15; // * v9.44 FIX#183: 0.25->0.15 | FTMO: 0.25 caused 3L streak to cut lots by 75% (0.27->0.06). Recovery mathematically impossible. 0.15: 3L=-45% max, still recoverable. +input bool PosSize_UseDrawdown = true; // Use Drawdown +input double PosSize_DDReduction = 0.7; // DD Reduction +input double PosSize_DDThreshold = 3.0; // * v9.03 FTMO: 10.0->3.0 (start cutting lots at 3% DD -- FTMO limit is 5%, need early intervention) +input bool PosSize_UseCompounding = true; // * v7.5c: Equity Compounding On/Off +input double PosSize_CompoundFactor = 0.80; // * v9.16: 0.50->0.80 (10% balance growth -> 8% lots increase, more aggressive compounding) +input double PosSize_CompoundMaxBoost = 1.20; // * v9.16: 1.20->1.50 (max +50% compound -- lets winners grow faster) +input double PosSize_CompoundMaxCut = 0.70; // * v7.5c: Min compound multiplier (0.70 = -30% max) +input bool PosSize_UseKelly = true; // * FIX#389: enabled — Kelly uses R-multiples (FIX#370/385), fractional 0.25 = safe. Requires PosSize_StreakLookback trades to activate; uses defaults (avgWin=2R/avgLoss=1R) until enough data. +input double PosSize_KellyFraction = 0.25; // Kelly Fraction +//+==================================================================+ +//| [ML] FULL AUTO-OPTIMIZATION SYSTEM v1.0 | +//| Auto-adapt ALL parameters per pair & timeframe | +//+==================================================================+ +input group "======== [ML] FULL AUTO-OPTIMIZATION ========" +input bool AutoOpt_Enabled = true; // * Enable Full Auto-Optimization +input bool AutoOpt_AutoParameters = true; // Auto-Tune ALL Parameters +input bool AutoOpt_AutoTimeframe = true; // Auto-Adapt to Timeframe +input bool AutoOpt_AutoVolatility = true; // Auto-Adapt to Volatility +input bool AutoOpt_AutoSpread = true; // Auto-Adapt to Spread +input bool AutoOpt_AutoSession = true; // Auto-Detect Best Session +input bool AutoOpt_AutoStrategies = true; // Auto-Select Strategies +input int AutoOpt_RecalcMinutes = 1; // * v9.36 FIX#152: 15->1 (auto mode: effective = 1 candle of current TF. M5=5m M15=15m H1=60m H4=240m D1=1440m. Was x4 formula causing H4=16h overrun) +input int AutoOpt_ATR_Period = 14; // ATR Period for Calibration +input int AutoOpt_VolatilityLookback = 100; // Volatility Lookback Bars +input bool AutoOpt_UseHistoricalPerf = true; // Use Historical Performance +input bool AutoOpt_ShowLog = true; // Show Auto-Opt Log on Chart +input bool AutoOpt_ShowPanel = true; // Show Auto-Opt Panel +input int AutoOpt_PanelX = 10; // Panel X Position +input int AutoOpt_PanelY = 450; // Panel Y Position +input group "======== [ML] AUTO-OPT SAFETY LIMITS ========" +input int AutoOpt_Aggressiveness = 50; // Aggressiveness (0=Conservative, 100=Aggressive) +input double AutoOpt_MaxRiskOverride = 2.0; // * v9.49 FIX#199: 1.0→2.0 — was hard-capping ALL dynamic boosts at 1% (same as base). A+/A setups could never get 2-4% risk. FTMO-safe: 2% max, 2 losses = 4% DD < 5% daily limit. +// * v9.27: Aggregate risk caps per quality tier (replaces hardcoded 2.5%) +input double EA_AggRiskCap_APlus = 5.0; // * FIX#390: A+ hard ceiling = 5.0% (FTMO: $1250 on $25k. DD protection stops at 4.5% daily) +input double EA_AggRiskCap_A = 4.0; // * FIX#390: A hard ceiling = 4.0% +input double EA_AggRiskCap_Default = 2.5; // Aggregate risk cap for B/C/D trades (original hardcoded value) +input double AutoOpt_MinRiskOverride = 0.5; // * v9.03 FTMO: 2.0->0.5 (floor -- with Risk=1% base, 0.5% is 50% of base which is reasonable floor) +input double AutoOpt_MaxSL_Mult = 3.5; // * v9.36 FIX#154: 4.0->3.5 (Excel=3.5 -- tighter SL cap) +input double AutoOpt_MinSL_Mult = 1.2; // Min SL ATR Mult (safety floor) +input double AutoOpt_MaxTP_Mult = 8.5; // * v9.52b: 7.0→8.5 — H4 tp3=8.0 ATR was soft-capped. Allows runner targets on H4/D1. +input double AutoOpt_MinTP_Mult = 2.0; // Min TP ATR Mult (safety floor) +input int AutoOpt_MinScoreFloor = 40; // * v9.14 FIX#33: 36->50 (36/85=42% quality floor was too permissive. 50/85=59% minimum.) | * v9.35 FIX#137: 50->40 (GBPUSD M5 backtest: FVG/OB 46-52/85 rejected. 50 too aggressive, kills valid setups. 40/85=47% floor still meaningful.) +input int AutoOpt_MaxScoreCap = 85; // * v9.22 FIX-A: 70->85 (cap=70 was silently rejecting trades with score 60-84 even when EA_MinEntryScore=60. Dynamic: max(cap, EA_MinEntryScore+10)) +//+==================================================================+ +//| CHART PATTERN INPUT PARAMETERS | +//+==================================================================+ +input group "======== CHART PATTERNS ========" +input bool ChartPatterns_Enabled = true; // Enable Chart Patterns +input int ChartPatterns_Lookback = 200; // Lookback Bars +input int ChartPatterns_MinBars = 20; // Min Pattern Bars +input int ChartPatterns_MaxBars = 120; // Max Pattern Bars +input bool ChartPatterns_ShowOnChart = true; // Show on Chart +input int ChartPatterns_ExpiryBars = 40; // Expiry After Bars +input bool ChartPatterns_Alerts = false; // Enable Alerts +input double ChartPatterns_MinHeight = 0.8; // Min Height (xATR) +input group "======== HEAD & SHOULDERS ========" +input bool HS_Enabled = true; // Enable Head & Shoulders +input double HS_MinShoulderSymmetry = 0.7; // Min Shoulder Symmetry (0-1) +input double HS_MaxShoulderDiff = 0.3; // Max Shoulder Height Diff (xATR) +input double HS_MinHeadHeight = 1.0; // Min Head Height (xATR) +input bool HS_RequireNecklineBreak = true; // Require Neckline Break +input bool HS_AllowSlopedNeckline = true; // Allow Sloped Neckline +input color HS_BullColor = clrLime; // Inverse H&S Color +input color HS_BearColor = clrRed; // H&S Top Color +input group "======== DOUBLE/TRIPLE TOP-BOTTOM ========" +input bool DTB_Enabled = true; // Enable Double/Triple +input double DTB_PeakTolerance = 0.3; // Peak Tolerance (xATR) +input int DTB_MinPeakDistance = 10; // Min Bars Between Peaks +input int DTB_MaxPeakDistance = 100; // Max Bars Between Peaks +input bool DTB_RequireNeckBreak = true; // Require Neckline Break +input color DTB_TopColor = clrOrangeRed; // Top Pattern Color +input color DTB_BottomColor = clrDodgerBlue; // Bottom Pattern Color +input group "======== TRIANGLES ========" +input bool Triangle_Enabled = true; // Enable Triangles +input int Triangle_MinTouches = 3; // Min Touches Per Line +input double Triangle_MaxApexDistance = 50; // Max Apex Distance (bars) +input double Triangle_MinConvergence = 0.1; // Min Convergence Rate +input color Triangle_AscendingColor = clrLimeGreen; // Ascending Color +input color Triangle_DescendingColor = clrCoral; // Descending Color +input color Triangle_SymmetricalColor = clrGold; // Symmetrical Color +input group "======== FLAGS & PENNANTS ========" +input bool FlagPennant_Enabled = true; // Enable Flags & Pennants +input double FlagPennant_MinPoleHeight = 1.5; // Min Pole Height (xATR) +input int FlagPennant_MaxFlagBars = 30; // Max Flag Duration (bars) +input double FlagPennant_MaxRetracement = 0.5; // Max Retracement of Pole +input double FlagPennant_MinRetracement = 0.2; // Min Retracement of Pole +input color FlagPennant_BullColor = clrMediumSeaGreen; // Bull Flag/Pennant +input color FlagPennant_BearColor = clrCrimson; // Bear Flag/Pennant +input group "======== WEDGES ========" +input bool Wedge_Enabled = true; // Enable Wedges +input int Wedge_MinTouches = 3; // Min Touches Per Line +input int Wedge_MinBars = 15; // Min Wedge Duration +input double Wedge_MaxAngleDiff = 15; // Max Angle Between Lines +input color Wedge_RisingColor = clrOrange; // Rising Wedge Color +input color Wedge_FallingColor = clrDeepSkyBlue; // Falling Wedge Color +input group "======== DIAMOND ========" +input bool Diamond_Enabled = true; // Enable Diamond +input int Diamond_MinBars = 30; // Min Diamond Duration +input double Diamond_ExpansionRatio = 1.5; // Min Expansion Ratio +input color Diamond_TopColor = clrMagenta; // Diamond Top Color +input color Diamond_BottomColor = clrCyan; // Diamond Bottom Color +input group "======== V-PATTERN ========" +input bool VPattern_Enabled = true; // Enable V-Pattern +input double VPattern_MinSymmetry = 0.7; // Min Symmetry (0-1) +input double VPattern_MinHeight = 2.0; // Min Height (xATR) +input int VPattern_MaxBars = 25; // Max Duration (bars) +input color VPattern_TopColor = clrOrangeRed; // V-Top Color +input color VPattern_BottomColor = clrLime; // V-Bottom Color +//+==================================================================+ +//| EXTENDED CANDLESTICK INPUT PARAMETERS | +//+==================================================================+ +input group "======== CANDLESTICK PATTERNS ========" +input bool CandlePatterns_Enabled = true; // Enable Candlestick Patterns +input bool CandlePatterns_ShowOnChart = true; // Show on Chart +input bool CandlePatterns_Alerts = false; // Enable Alerts +input int CandlePatterns_MinStrength = 3; // Min Pattern Strength (1-5) +input group "======== SINGLE CANDLE PATTERNS ========" +input bool Doji_Enabled = true; // Enable Doji +input double Doji_MaxBodyRatio = 0.1; // Max Body/Range Ratio +input bool Hammer_Enabled = true; // Enable Hammer/Hanging Man +input double Hammer_MinWickRatio = 2.0; // Min Lower Wick/Body Ratio +input double Hammer_MaxUpperWick = 0.3; // Max Upper Wick/Range +input bool Marubozu_Enabled = true; // Enable Marubozu +input double Marubozu_MaxWickRatio = 0.05; // Max Wick/Range Ratio +input group "======== TWO CANDLE PATTERNS ========" +input bool Harami_Enabled = true; // Enable Harami +input double Harami_MaxBodyRatio = 0.5; // Max 2nd Body/1st Body +input bool PiercingDarkCloud_Enabled = true; // Enable Piercing/Dark Cloud +input double Piercing_MinPenetration = 0.5; // Min Penetration (0-1) +input bool Tweezer_Enabled = true; // Enable Tweezer +input double Tweezer_MaxDiff = 0.1; // Max High/Low Diff (xATR) +input group "======== THREE CANDLE PATTERNS ========" +input bool Star_Enabled = true; // Enable Morning/Evening Star +input double Star_MaxMiddleBody = 0.3; // Max Middle Body/Range +input bool ThreeSoldiersCrows_Enabled = true; // Enable 3 Soldiers/Crows +input double ThreeSC_MinBodySize = 0.6; // Min Body/Range Ratio +input double ThreeSC_MaxWickSize = 0.2; // Max Wick/Range +input bool ThreeInsideOutside_Enabled = true; // Enable 3 Inside/Outside +input bool AbandonedBaby_Enabled = true; // Enable Abandoned Baby +input double AbandonedBaby_MinGap = 0.1; // Min Gap Size (xATR) +input group "======== VSA (VOLUME SPREAD ANALYSIS) ========" +input bool VSA_Enabled = true; // Enable VSA Analysis +input double VSA_MinStrength = 60.0; // Minimum VSA Strength (0-100) +input bool VSA_ShowOnChart = false; // Show VSA Patterns on Chart +input bool VSA_ShowPanel = true; // Show VSA Panel +input double VSA_HighVolumeThreshold = 1.5; // High Volume Threshold (x avg) +input double VSA_LowVolumeThreshold = 0.7; // Low Volume Threshold (x avg) +input double VSA_WideSpreadMultiplier = 1.3; // Wide Spread Multiplier (x ATR) +input double VSA_NarrowSpreadMult = 0.7; // Narrow Spread Multiplier (x ATR) +input color VSA_BullishColor = clrLime; // VSA Bullish Color +input color VSA_BearishColor = clrRed; // VSA Bearish Color +input group "======== MULTI-TIMEFRAME ANALYSIS ========" +input bool MTF_Enabled = true; // Enable MTF Analysis +input bool MTF_AutoTimeframe = true; // Auto Timeframe Selection +input ENUM_TIMEFRAMES MTF_ManualTF = PERIOD_H1; // Manual Primary Timeframe +input bool MTF_ShowAlignment = true; // Show MTF Alignment on Dashboard +input bool MTF_ConfluenceFilter = true; // Use MTF as Confluence Filter +input double MTF_MinConfidence = 70.0; // Minimum MTF Confidence (%) +input bool MTF_UseM15 = true; // Use M15 +input bool MTF_UseM30 = false; // Use M30 +input bool MTF_UseH1 = true; // Use H1 +input bool MTF_UseH4 = true; // Use H4 +input bool MTF_UseD1 = true; // Use D1 +input bool MTF_UseW1 = false; // Use W1 +input group "================== ENTRY SCORING ==================" +input bool InpEnableScoring = true; // Enable Entry Quality Scoring +input bool InpShowScoreOnChart = true; // Show Score on Chart +input bool InpRequireMinScore = true; // Require Min Score for Entry +input group "======== PATTERN COLORS ========" +input color Candle_BullishColor = clrLime; // Bullish Pattern Color +input color Candle_BearishColor = clrRed; // Bearish Pattern Color +input color Candle_NeutralColor = clrGray; // Neutral Pattern Color +input int Candle_ArrowSize = 2; // Arrow Size +//+------------------------------------------------------------------+ +//| ΕΝΟΤΗΤΑ 1: ΝΕΕΣ ΠΑΡΑΜΕΤΡΟΙ INPUT | +//| ΘΕΣΗ: Μετά τις υπάρχουσες EA parameters | +//+------------------------------------------------------------------+ +// * v7.4: DD Protection inputs REMOVED - now in unified "RISK & ACCOUNT PROTECTION" group above +//=== SPREAD FILTER === +input group "======== [CHART] SPREAD FILTER ========" +input bool EA_EnableSpreadFilter = true; // Enable Spread Filter +input double EA_MaxSpreadPips = 30.0; // * v9.36 FIX#154: 50->30 (Excel M15=30, was accepting 2x spread) +input double EA_MaxSpreadATR = 0.5; // Max Spread (ATR multiplier) +input bool EA_CheckSpreadOnEntry = true; // Check Spread on Entry +input bool EA_CheckSpreadOnModify = true; // Check Spread on Modify +//=== TIME FILTER === +input group "======== [TIMER] TIME FILTER ========" +input bool EA_EnableTimeFilter = true; // Enable Time Filter +input int EA_StartHour = 7; // * v9.36 FIX#154: 0->7 (Excel: no Asian session trading) +input int EA_StartMinute = 0; // Trading Start Minute +input int EA_EndHour = 21; // * v9.36 FIX#154: 23->21 (Excel: stop at 21:00) +input int EA_EndMinute = 59; // Trading End Minute +input bool EA_TradeMondayEnabled = true; // Trade on Monday +input bool EA_TradeTuesdayEnabled = true; // Trade on Tuesday +input bool EA_TradeWednesdayEnabled = true; // Trade on Wednesday +input bool EA_TradeThursdayEnabled = true; // Trade on Thursday +input bool EA_TradeFridayEnabled = true; // Trade on Friday +input bool EA_AvoidFridayClose = true; // Close All on Friday End +input int EA_FridayCloseHour = 20; // * v9.36 FIX#154: 22->20 (Excel: Friday close at 20:00) +// SESSION-BASED SL ADJUSTMENT +input group "======== [HOT] SESSION SL ADJUSTMENT ========" +input bool SessionSL_Enabled = true; // Enable Session-Based SL +input double SessionSL_AsianMult = 0.8; // Asian Session SL Multiplier +input double SessionSL_LondonMult = 1.0; // London Session SL Multiplier [v6.2: 1.2->1.0] +input double SessionSL_NYMult = 1.1; // NY Session SL Multiplier [v6.2: 1.5->1.1] +input double SessionSL_OverlapMult = 1.1; // Session Overlap SL Multiplier [v6.2: 1.3->1.1] +input bool SessionSL_ShowInfo = true; // Show Session Info on Chart +input color SessionSL_LabelColor = clrYellow; // Session Label Color +//+------------------------------------------------------------------+ +//| END OF SECTION 4 | +//+------------------------------------------------------------------+ +#define MultiTP_Enabled InpEnableMultiTP +#define MultiTP_ShowLevels InpShowTPLabels +//+------------------------------------------------------------------+ +//| END OF INPUT PARAMETERS | +//+------------------------------------------------------------------+ +// =================================================================== +// INDICATOR BUFFERS +// =================================================================== +double MABuffer[]; +// =================================================================== +// CORE ICT DATA ARRAYS +// =================================================================== +FVG_Struct FVG_Array[]; +OB_Struct OB_Array[]; +LIQ_Struct LIQ_Array[]; +STRUCT_Struct STRUCT_Array[]; +OTE_Struct OTE_Array[]; +BREAKER_Struct BREAKER_Array[]; +MITIGATION_Struct MITIGATION_Array[]; +OFI_Struct OFI_Array[]; +VP_Level VP_Levels[]; +MM_Phase MM_Phases[]; +ML_Prediction ML_Predictions[]; +SIGNAL_Struct SIGNAL_Array[]; +RISK_Struct RISK_Data; +// =================================================================== +// EA COMPATIBILITY ALIASES +// =================================================================== +#define g_fvgs FVG_Array +#define g_obs OB_Array +// =================================================================== +// DEBUG & UTILITY GLOBAL VARIABLES +// =================================================================== +bool g_debugMode = false; +// =================================================================== +// NEURAL NETWORK GLOBAL VARIABLES +// =================================================================== +NeuralNetwork g_neuralNet; +FeatureNormalizer g_featureNormalizer; +bool g_nnInitialized = false; +bool g_nnTrained = false; +int g_nnTrainingEpoch = 0; +double g_nnTrainingLoss = 0; +double g_nnValidationLoss = 0; +double g_nnBestLoss = DBL_MAX; +int g_nnEpochsNoImprovement = 0; +datetime g_lastNNTraining = 0; +datetime g_lastNNPrediction = 0; +// Training data buffers +double g_trainingInputs[]; +double g_trainingTargets[]; +double g_validationInputs[]; +double g_validationTargets[]; +int g_trainingSize = 0; +int g_validationSize = 0; +// Feature extraction cache +double g_featureCache[]; +int g_featureCacheBar = -1; +// =================================================================== +// ICT KILLZONES GLOBAL VARIABLES +// =================================================================== +KillzoneDefinition g_killzoneDefinitions[9]; +ActiveKillzone g_activeKillzones[9]; +int g_numKillzones = 0; +DSTInfo g_dstInfo; +bool g_killzonesInitialized = false; +datetime g_lastKillzoneUpdate = 0; +int g_currentKillzoneIndex = -1; +string g_currentKillzoneName = "NONE"; +bool g_isInKillzone = false; +// Killzone statistics +int g_kzTradesCount[9]; +int g_kzWins[9]; +int g_kzLosses[9]; +double g_kzAvgPips[9]; +// =================================================================== +// PERFORMANCE PERSISTENCE GLOBAL VARIABLES +// =================================================================== +PerformancePersistence g_perfData; +TradeRecord g_tradeHistory[]; +int g_tradeHistoryCount = 0; +bool g_perfDataLoaded = false; +datetime g_lastPerfSave = 0; +string g_dataFolderPath = ""; +int g_perfFileHandle = INVALID_HANDLE; +// Strategy performance tracking +StrategyPerformance g_strategyPerf[15]; +int g_numStrategies = 0; +string g_strategyNames[15]; +// Quality correlation tracking +int g_totalWins = 0; +int g_totalLosses = 0; +int g_highQualityWins = 0; +int g_highQualityLosses = 0; +int g_mediumQualityWins = 0; +int g_mediumQualityLosses = 0; +int g_lowQualityWins = 0; +int g_lowQualityLosses = 0; +double g_totalProfitPips = 0; +double g_totalLossPips = 0; +double g_totalRRWon = 0; +double g_totalRRLost = 0; +double g_avgRRWon = 0; +double g_avgRRLost = 0; +// =================================================================== +// BACKTESTING GLOBAL VARIABLES +// =================================================================== +BacktestResults g_backtestResults; +BacktestTrade g_backtestTrades[]; +int g_backtestTradeCount = 0; +double g_backtestEquity = 0; +double g_backtestPeakEquity = 0; +double g_backtestDrawdown = 0; +bool g_isBacktesting = false; +bool g_liveMode = false; // * FIX#469: true when running on live/demo account (not tester) +bool g_verboseLog = false; // * FIX#469: true only in tester non-optimization — gates all debug prints +datetime g_backtestStartTime = 0; +datetime g_backtestEndTime = 0; +int g_backtestCurrentBar = 0; +MonteCarloResults g_monteCarloResults; +// Walk-forward optimization +int g_wfCurrentWindow = 0; +int g_wfTotalWindows = 0; +double g_wfOptimalParams[]; +// =================================================================== +// COST ANALYSIS +// =================================================================== +BrokerConfig g_brokerConfig; +TradingCosts g_lastTradeCosts; +bool g_costAnalysisEnabled = false; +// =================================================================== +// PAIR OPTIMIZATION +// =================================================================== +PairProfile g_knownPairs[15]; +PairProfile g_pairProfiles[]; +PairProfile g_currentPairProfile; +int g_numKnownPairs = 0; +int g_totalPairProfiles = 0; +SessionPerformance g_sessionPerformance[]; +string g_currentSymbolCategory = ""; +// Performance tracking per pair +double g_pairWinRate = 0; +double g_pairAvgRR = 0; +int g_pairTotalTrades = 0; +// =================================================================== +// ML PREDICTION VISUALIZATION +// =================================================================== +double g_predictionBuffer[]; +double g_upperBandBuffer[]; +double g_lowerBandBuffer[]; +datetime g_predictionTime[]; +int g_predictionBars = 20; // [v6.42] Will be set from PRED_Bars in OnInit +datetime g_lastPredictionUpdate = 0; +bool g_predictionInitialized = false; +// =================================================================== +// PROBABILITY HEATMAP +// =================================================================== +PriceLevel g_heatmapLevels[]; +int g_heatmapRows = 20; +int g_heatmapCols = 15; +// =================================================================== +// GLOBAL STATE +// =================================================================== +int g_totalBars = 0; +int g_prevCalculated = 0; +int g_ticksThisBar = 0; +long g_signalIdCounter = 0; +long g_fvgIdCounter = 0; +long g_obIdCounter = 0; +long g_tradeIdCounter = 0; +int g_fvgCount = 0; // Added for EA compatibility +int g_obCount = 0; // Added for EA compatibility +bool g_initSuccess = false; +// =================================================================== +// CRT (CANDLE RANGE THEORY) GLOBAL VARIABLES +// =================================================================== +CRTSetup g_crtSetups[]; +int g_crtCount = 0; +int g_maxCRT = 10; +CRTSetup g_currentCRT; +bool g_hasCRT = false; +// =================================================================== +// TBS (TURTLE SOUP) GLOBAL VARIABLES +// =================================================================== +TBSSetup g_tbsSetups[]; +int g_tbsCount = 0; +int g_maxTBS = 10; +TBSSetup g_currentTBS; +bool g_hasTBS = false; +// =================================================================== +// AMD (ACCUMULATION-MANIPULATION-DISTRIBUTION) GLOBAL VARIABLES +// =================================================================== +AMDPhaseData g_amdData; +AMDPhaseData g_amdPhase; +bool g_hasAMD = false; +bool g_amdDetected = false; +datetime g_amdLastUpdate = 0; +// =================================================================== +// JUDAS SWING GLOBAL VARIABLES +// =================================================================== +JudasSwingData g_judasSetups[]; +JudasSwingData g_judasSwings[]; +int g_judasCount = 0; +int g_maxJudas = 10; +JudasSwingData g_currentJudas; +bool g_hasJudas = false; +bool g_judasActive = false; +// =================================================================== +// SILVER BULLET GLOBAL VARIABLES +// =================================================================== +SilverBulletSetup g_sbSetups[]; +int g_sbCount = 0; +bool g_sbActive = false; +ENUM_SB_TYPE g_currentSBType = SB_LONDON; +// =================================================================== +// [NEW] ENHANCED FEATURES GLOBALS +// =================================================================== +// Enhanced Divergence +int g_divObjCount = 0; +// =================================================================== +// MARKET REGIME GLOBAL VARIABLES +// =================================================================== +MarketRegimeData g_regimeData; +datetime g_regimeLastUpdate = 0; +bool g_regimeInitialized = false; +bool g_regimeValid = false; +// =================================================================== +// WIN PROBABILITY & EXPECTED VALUE +// =================================================================== +WinProbabilityData g_winProbData; +bool g_winProbValid = false; +ExpectedValueData g_evData; +bool g_evValid = false; +// =================================================================== +// POSITION SIZING +// =================================================================== +PositionSizeData g_posSizeData; +bool g_posSizeValid = false; +// =================================================================== +// NEWS FILTER GLOBAL VARIABLES +// =================================================================== +NewsFilterData g_newsData; +NewsFilterData g_newsFilter; +NewsEvent g_newsEvents[]; +NewsEvent g_newsEventsStruct[]; +int g_newsCount = 0; +int g_newsEventCount = 0; +int g_maxNewsEvents = 50; +datetime g_newsLastUpdate = 0; +bool g_newsValid = false; +bool g_newsTradingBlocked = false; +string g_newsBlockReason = ""; +// =================================================================== +// CORRELATION GLOBAL VARIABLES +// =================================================================== +CorrelationData g_corrData; +bool g_corrValid = false; +// =================================================================== +// TIME ANALYSIS GLOBAL VARIABLES +// =================================================================== +TimeAnalysisData g_timeData; +bool g_timeValid = false; +HourlyPerformance g_hourlyPerf[24]; +bool g_timeAnalysisReady = false; +int g_totalTradesTracked = 0; +// =================================================================== +// SMART ENTRY GLOBAL VARIABLES +// =================================================================== +SmartEntryDecision g_smartEntry; +SmartEntryDecision g_lastSmartDecision; +bool g_smartEntryValid = false; +int g_smartWins = 0; +int g_smartLosses = 0; +double g_smartTotalRR = 0; +// =================================================================== +// HISTORICAL PERFORMANCE +// =================================================================== +double g_historicalWins[]; +double g_historicalLosses[]; +int g_totalHistoricalTrades = 0; +double g_historicalWinRate = 0.5; +// * v9.49 FIX#204: Peak RR arrays — track highest RR reached per trade (independent of outcome). +// Used by AutoOpt to detect that TP3 was reachable on a trade that ended as SL. +// Example: trade reached +2.5R then reversed → g_historicalPeakWins[n]=2.5, g_historicalPeakLosses[m]=2.5. +// AutoOpt can then say: "avg peak of losses = 1.8R → TP3 target 1.9R is realistic". +double g_historicalPeakWins[]; // Peak RR for each win (parallel to g_historicalWins) +double g_historicalPeakLosses[]; // Peak RR for each loss (parallel to g_historicalLosses) +double g_historicalAvgPeakWin = 0.0; // Rolling average of peak RR on wins +double g_historicalAvgPeakLoss = 0.0; // Rolling average of peak RR on losses +// * v9.10 NN outcome-based globals +double g_nnTP1WinProb = 0.5; // P(TP1 hits before SL) -- updated by GenerateNNPrediction +// * v10.29 FIX#316: additional NN outputs for richer ML decisions +double g_nnTP2WinProb = 0.3; // P(TP2 hits) — used to boost/reduce runner sizing +double g_nnOptimalExitR = 1.2; // Predicted optimal exit R — used as dynamic SE floor +bool g_nnReadyForUse = false; // True after min 30 trades trained +int g_nnLastTrainCount = 0; // For auto-retrain every 50 new trades +double g_historicalAvgWin = 2.0; +double g_historicalAvgLoss = 1.0; +// Trading Streak +int g_currentStreak = 0; +int g_maxWinStreak = 0; +int g_maxLossStreak = 0; +// * v9.07 FIX#6 -> v9.13 FIX#23a: Separate MTF-relaxation streak counter. +// v9.13: Changed from 2->1 consecutive win to reset + capped at 8. +// Multi-TP splits made 2-win requirement impossible. +int g_lossStreakForMTF = 0; // counts only losses; needs 1 win to reset (capped at 8) +int g_winsToResetMTF = 0; // consecutive wins since last loss +// =================================================================== +// CACHED VALUES (UNIFIED - NO DUPLICATES) +// =================================================================== +// [v6.42] EnableCache gates caching - if false, recalculate every tick +double g_cachedATR = 0; +double g_cachedADX = 0; +double g_cachedRSI = 50; +double g_cachedMA = 0; +// ── RANGE CONFIRMATION SYSTEM (FIX#501) ────────────────────────── +// Multi-level ranging detection: ADX(HTF) + MA slope + rangeWidthATR + vol contraction +// g_rangeConfScore 0-5: count of levels confirming RANGING market +// Used in ComputeUnifiedScore to gate entries by premium/discount position +// +// Two HTF ADX values following the 3-TF framework: +// Entry TF → Middle TF → Higher TF +// M5 → M15(mid) → H1(high) +// M15 → H1(mid) → H4(high) +// H1 → H4(mid) → D1(high) ← EA ANGEL target +// H4 → D1(mid) → W1(high) +double g_cachedADX_HTF_mid = 0; // Middle TF ADX (1 level up: H4 for H1 EA) +double g_cachedADX_HTF_high = 0; // Higher TF ADX (2 levels up: D1 for H1 EA) +double g_cachedMA_Slope = 0; // |MA[0]-MA[5]| normalised by ATR (0=flat, 1=steep) +int g_rangeConfScore = 0; // 0=trending, 1-2=probable range, 3-5=confirmed range +// g_exhaustionScore 0-5: count of signals confirming TREND EXHAUSTION +// Mirror of g_rangeConfScore but for overextended/dying trends (Scenario 11 transition). +// Score 0-1: healthy trend. Score 2: caution. Score 3: weakening. Score 4-5: overextended. +// Owner: RunSharedAnalysis (computed alongside g_rangeConfScore, just before ComputeMarketContext). +int g_exhaustionScore = 0; +// Rolling peak + trend score (FIX#503b): read by DeriveScenarioProfile +#define EXHAUST_LOOKBACK 4 +int g_exhaustionHistory[EXHAUST_LOOKBACK]; // [0]=current bar, [1]=1 bar ago, ... +int g_exhaustionPeak = 0; // max of last EXHAUST_LOOKBACK bars +double g_exhaustionTrend = 0.0; // weighted mean 0.0-1.0 (recent=more weight) +// Cached ADX handle — avoids iADX()+IndicatorRelease() every bar (FIX#503b perf) +int g_adxHandleExhaust = INVALID_HANDLE; +// Breakout + Reversal confirmation scores (computed alongside exhaustion) +// g_breakoutConfScore 0-4: genuine breakout quality (body strength, ADX rise, vol, regime) +// g_reversalConfScore 0-4: reversal setup quality (CHoCH, divergence, exhaustion, MTF) +int g_breakoutConfScore = 0; +int g_reversalConfScore = 0; +// [v6.42] ShowMA, ShowOrderFlowImb, ShowMMModels are visual display toggles +// They gate their respective indicator drawing functions +datetime g_lastCalculation = 0; +datetime g_lastCacheUpdate = 0; +int g_totalRates = 0; +// =================================================================== +// TIMESTAMP MANAGEMENT +// =================================================================== +datetime g_lastCleanupTime = 0; +datetime g_lastObjectCleanup = 0; +datetime g_lastArrayCompact = 0; +datetime g_lastMLUpdate = 0; +datetime g_lastRiskCheck = 0; +datetime g_lastExpiryCheck = 0; +// =================================================================== +// SIGNAL MANAGEMENT +// =================================================================== +datetime g_signalApproachAlerts[]; +bool g_signalMarkedAsTaken[]; +bool g_signalExpiryAlerted[]; +int g_activeSignalCount = 0; +datetime g_lastSignalCreated = 0; +// =================================================================== +// RISK MANAGEMENT +// =================================================================== +int g_tradesThisDay = 0; +// * v9.34 FIX#132: Per-regime trade counter -- prevents CHOPPY/WEAK throttle from +// counting trades taken in a DIFFERENT regime earlier the same day. +// Bug: 2 trades in TREND → regime shifts CHOPPY → g_ea_stats.trades=2 >= maxChoppy=2 → 0 CHOPPY trades allowed. +// Fix: track trades per regime period separately; reset when regime changes or day resets. +int g_tradesThisRegimePeriod = 0; // trades taken while current regime was active +int g_lastRegimeForThrottle = -1; // last regime used for per-regime counter (-1 = unset) +double g_dailyLoss = 0.0; +double g_dailyProfit = 0.0; +int g_dailyFullSLCount = 0; // (unused, kept for compatibility) +datetime g_lastPatternDetectionBar = 0; // * v9.16 FIX#35: Prevent H&S re-detection every tick +// =================================================================== +// * v6.40 NEW: DAILY DRAWDOWN PROTECTION +// =================================================================== +double g_dailyStartBalance = 0.0; // Balance at start of day (00:00 GMT) +double g_dailyStartEquity = 0.0; // Equity at start of day +datetime g_lastDDResetDate = 0; // Last DD reset timestamp (midnight) +bool g_dailyDDLimitReached = false; // Flag: DD limit hit today +double g_currentDailyDD = 0.0; // Current DD percentage +datetime g_dailyDDBlockedTime = 0; // Timestamp when DD limit was hit +int g_dailyDDBlockCount = 0; // How many times blocked today +// [v6.41] Weekly + Total DD tracking +double g_weeklyStartBalance = 0.0; // Balance at start of week (Monday 00:00) +double g_weeklyStartEquity = 0.0; // * v8.04: Equity at start of week (for correct DD ref) +datetime g_lastWeeklyResetDate = 0; // Last weekly reset timestamp +bool g_weeklyDDLimitReached = false; // Flag: Weekly DD limit hit +double g_currentWeeklyDD = 0.0; // Current weekly DD percentage +double g_peakBalance = 0.0; // Peak balance for total DD tracking +bool g_totalDDLimitReached = false; // Flag: Total DD limit hit +double g_currentTotalDD = 0.0; // Current total DD percentage +double g_effectiveBIB = 0.0; // * FIX#415: Effective BacktestInitialBalance — mirrors input but can be auto-corrected at OnInit when stale saved value detected (input vars are const in MQL5). +// =================================================================== +// MARKET STRUCTURE +// =================================================================== +bool g_isBullishStructure = true; +datetime g_lastCHoCHTime = 0; // * v7.4 FIX: Track last CHoCH to prevent UpdateStructureBias override +string g_currentPhase = PHASE_NONE; +string g_currentPDZone = "EQUILIBRIUM"; +// --- FIX#443: Draw on Liquidity (DOL) globals --- +// Computed every bar in ComputeDrawOnLiquidity(). +// DOL = nearest unswept liquidity pool direction — where price is DRAWN to naturally. +// BUY trade with DOL=DOWN = fighting the magnet = low edge. +// SELL trade with DOL=UP = fighting the magnet = low edge. +int g_dolDirection = 0; // +1=UP (nearest target is BSL above) -1=DOWN (nearest SSL below) 0=NEUTRAL +double g_dolTargetPrice = 0.0; // Price of the nearest unswept DOL target +double g_dolDistance = 0.0; // Distance in pips to DOL target +double g_dolOppositeDistance = 0.0; // Distance to OPPOSING liquidity (for ratio) +bool g_dolValid = false; // false = insufficient data, gate inactive +// --- FIX#196 (v9.48): D1 CHoCH Hard Gate globals --- +// Tracks Daily timeframe Change of Character independently from current TF structure. +// g_d1CHoCH_Bull / _Bear: last confirmed D1 CHoCH direction +// g_d1CHoCH_Valid: true when a clear D1 bias exists (at least one CHoCH confirmed) +// g_d1LastBarTime: prevents recalculation on every tick (only on new D1 bar) +bool g_d1CHoCH_Bull = false; +bool g_d1CHoCH_Bear = false; +bool g_d1CHoCH_Valid = false; +datetime g_d1LastBarTime = 0; +// Stale CHoCH expiry counter (FIX#506): +// Counts consecutive H1 bars where raw MTF contradicts D1 CHoCH direction. +// When >= 12 (12 hours): CHoCH is stale → invalidated → D1=neutral → MTF leads. +int g_d1CHoCH_MtfConflictBars = 0; +// --- FIX#232 (v10.00): FIX#176/225 CT-WEAK → SmartEntry bypass flag --- +// Set TRUE by SelectBestCandidate when FIX#176+FIX#225 explicitly approves a +// counter-trend WEAK trade (MTF BULL/BEAR supports the direction on H4+). +// Read by EvaluateSmartEntry to skip its independent CT block for this candidate. +// Reset FALSE at the top of every EA_CheckSignals() call — never stale. +bool g_fix176CTExempt = false; +// --- +// =================================================================== +// SYMBOL INFORMATION +// =================================================================== +double g_pipValue; +int g_digits; +double g_point; +double g_tickValue; +double g_tickSize; +double g_minLot; +double g_maxLot; +double g_lotStep; +// =================================================================== +// INDICATOR HANDLES +// =================================================================== +int g_maHandle = INVALID_HANDLE; +int g_atrHandle = INVALID_HANDLE; +int g_rsiHandle = INVALID_HANDLE; +int g_tcEmaFastHandle = INVALID_HANDLE; // * v7.8 TC: Fast EMA handle +int g_tcEmaSlowHandle = INVALID_HANDLE; // * v7.8 TC: Slow EMA handle +int g_tcRSIHandle = INVALID_HANDLE; // * v9.16 FIX#48: TC-specific RSI handle (uses TC_RSI_Period) +bool g_tcEnabled = true; // * v7.8 TC: working copy of EnableTrendCont +// * v7.8 SL COOLDOWN: block re-entry same direction after SL +datetime g_lastSLTime = 0; // time of last SL hit +int g_lastSLDirection = 0; // direction of last SL: 1=buy -1=sell +int g_consecutiveSLs = 0; // consecutive SL count (same direction) +bool g_slCooldownActive = false; // true = currently in cooldown +int g_slCooldownDirection = 0; // direction blocked by cooldown +bool g_blockSellFromCooldown= false;// v7.8: SELL entries blocked by SL cooldown +bool g_blockBuyFromCooldown = false;// v7.8: BUY entries blocked by SL cooldown +int g_rsiDivergenceHandle = INVALID_HANDLE; +// =================================================================== +// WORKING VARIABLES (Modifiable copies of inputs) +// =================================================================== +double g_workingFVG_MinSize; +int g_workingFVG_MaxAge; +int g_workingFVG_ExtendBars; +double g_workingMinConfluence; +// [v6.42] Correlation reduction factor +double g_corrReductionFactor = 1.0; +// [v6.42] Hourly statistics for Time_FilterByHour +struct HourlyStats { int totalTrades; int wins; int losses; double profitPips; }; +HourlyStats g_hourlyStats[24]; +double g_workingMinRiskReward; +double g_workingMinEntryQuality; +double g_workingSL_ATRMultiplier; +double g_workingTP_ATRMultiplier; +// * v9.03: Working globals for TF-aware position management +// Set from AutoOpt (when ON) or from inputs (when OFF) +double g_workingBE_RR; // Effective breakeven R:R +double g_workingBE_Ranging_RR; // Effective BE for ranging +double g_workingBE_Volatile_RR; // Effective BE for volatile +double g_workingBE_TP2_RR; // * v10.08 FIX#278: Effective TP2 BE threshold (AutoOpt-aware) +double g_workingTrailStart_RR; // Effective trail start R:R +double g_workingTrailStop_ATR; // Effective trail distance +double g_workingSmartExit_MinRR; // Effective smart exit min R:R +double g_workingSE_RSI_OB = 70.0; // * FIX#421: SmartExit RSI OB threshold (default 70) +double g_workingSE_RSI_OS = 30.0; // * FIX#421: SmartExit RSI OS threshold (default 30) +double g_workingSE_PeakTh1 = 0.65; // * FIX#421: Peak trail ratio peak<1.5R (default 0.65) +double g_workingSE_PeakTh2 = 0.72; // * FIX#421: Peak trail ratio peak<2.5R (default 0.72) +double g_workingSE_PeakTh3 = 0.78; // * FIX#421: Peak trail ratio peak>=2.5R (default 0.78) +double g_workingSE_OverrideRR = 0.0; // * FIX#421: 2-cat override min RR (0=disabled) +double g_workingSE_MomRatio = 0.70; // * FIX#423: bar shrink ratio (default 0.70) +double g_workingSE_MomScoreMult = 1.40; // * FIX#423: score mult on momentum fade (default 1.40) +int g_lastSE_catCount = 0; // * FIX#423: last catCount from SmartExit_Check (shared) +// * FIX#424: SmartExit Re-Entry — when SE closes at profit, store zone info +// so EA_CheckSignals can allow one re-touch entry within 3 bars. +// g_seReEntry.active is set by SmartExit close, cleared by re-entry or timeout. +struct SEReEntry { + bool active; // Re-entry window open + datetime closedAt; // When SmartExit fired + double zoneTop; // Zone boundaries from closed trade + double zoneBottom; + string zoneType; + int direction; // 1=BUY 2=SELL that was profitably closed + double closeRR; // RR at close (only store if >= se_minrr) + int maxBars; // Max bars to wait for re-entry +}; +SEReEntry g_seReEntry; // * FIX#424: single re-entry slot (one at a time) +bool g_seReEntryReady = false; // * FIX#424: set when re-entry signal is synthesized +// g_workingFIX41_TrailStart: removed (dead — ProfitGuard now uses EA_Trail_Activation_RR directly) +double g_workingFIX41_SmartExitRR; // * v9.24 FIX#80: Synced EA_SmartExit_MinProfit_RR <- AutoOpt +int g_workingFIX41_ScoreThresh; // * v9.24 FIX#80: Synced 15 <- AutoOpt +int g_workingSmartExit_Signals; // Effective smart exit signals needed +double g_workingTrail_Volatile_Start;// Effective volatile trail start +double g_workingTrail_Volatile_Dist; // Effective volatile trail distance +double g_workingTrail_Trending_Start;// Effective trending trail start +// * FIX#303: per-TF Killzone/Trend require flags — shadow of input bools (inputs cannot be assigned at runtime) +bool g_workingRequireKillzone; // Effective EA_RequireKillzone (may be overridden per TF) +bool g_workingRequireTrend; // Effective EA_RequireTrend (may be overridden per TF) +// * v10.24 FIX#310a: Per-TF TP percent working vars — inputs are defaults, SCALP overrides to 60/25/15 +int g_workingTP1_Pct; // Effective EA_TP1_Percent (M5: 60, others: input value) +int g_workingTP2_Pct; // Effective EA_TP2_Percent (M5: 25, others: input value) +int g_workingTP3_Pct; // Effective EA_TP3_Percent (M5: 15, others: input value) +// * v10.24 FIX#310b: Per-TF BlockNeutral — M5 blocks neutral D1 phases, H4/H1/M15 follow global input +bool g_workingBlockNeutral; // Effective EA_D1CHoCH_BlockNeutral (M5: true, others: input value) +// * v10.27 FIX#314: per-TF position sizing working vars +double g_workingLossStreakCut; // Effective PosSize_LossStreakCut (H4: 0.08, others: global 0.15) +double g_workingMaxLot; // Effective max lot cap (H4: 2.00, others: SYMBOL_VOLUME_MAX) +// * v10.29 FIX#317: H4 hardcoded risk ceiling — ignores EA_RiskPercent input +// H4: always 1.80%. Other TFs: mirrors EA_RiskPercent (no change). +double g_workingRiskCeiling; // Replaces EA_RiskPercent in CalculatePositionSize for H4 +// * v10.31 FIX#320: choppy/MTF protection working vars +double g_workingChoppyMinConf; // Min score in CHOPPY (0=disabled) +double g_workingChoppyLotMult; // Lot mult in CHOPPY (0=no change) +int g_workingBlockTCChoppy; // 1=block TC in CHOPPY/RANGING +int g_workingMTFHardBlock; // 1=hard block when MTF strongly opposes +int g_workingD1CHoCHGate; // 1=D1 CHoCH gate on for this TF (from pair table) +// * FIX#363: per-TF OB override +int g_workingAllowOB; // -1=force-off, 0=global, 1=force-on +// * FIX#372: per-TF BOS_RETEST override +int g_workingAllowBOSRetest; // -1=disable, 0=global (enabled), 1=force-on +// * FIX#454: per-TF overrides for remaining techniques (-1=off, 0=global, 1=on) +int g_workingAllowFVG = 0; // FVG override (0=global EnableFVG) +int g_workingAllowOTE = 0; // OTE override (0=global EnableOTE) +int g_workingAllowLIQ = 0; // LIQ override (0=global EnableLiquidity) +int g_workingAllowBreaker = 0; // BREAKER override (0=global EnableBreakerBlocks) +int g_workingAllowTC = 0; // TC override (0=global EnableTrendCont) +double g_workingMaxCostPct = 25.0; // Effective max cost % of SL per TF +// * FIX#373: Mean Reversion working vars (set by ApplyPairTFProfile, read by EA_CheckSignals) +int g_workingMR_Enabled = 0; // -1=off, 0=use global Regime_UseMeanReversion, 1=on +int g_workingMR_RSIBuy = 35; // RSI level to trigger BUY at rangeLow +int g_workingMR_RSISell = 65; // RSI level to trigger SELL at rangeHigh +double g_workingMR_SLMult = 0.30; // SL buffer beyond range extreme (× ATR) +double g_workingMR_MinRangeATR = 1.5; // Min rangeWidthATR to activate MR +double g_workingTrail_Trending_Dist; // Effective trending trail distance +bool g_workingUseSessionFilter; +bool g_workingSessionLondon; +bool g_workingSessionNewYork; +bool g_workingSessionAsian; +int g_workingRefreshRate; +double g_workingRSI_Overbought; +double g_workingRSI_Oversold; +double g_workingATRMinValue; +double g_workingATRMaxValue; +bool g_workingUseTrendFilter; +bool g_workingUseRSIFilter; +bool g_workingUseATRFilter; +// * v7.4 FIX: Re-added g_workingMaxSpreadPips -- needed so AutoOpt/PairDetection can override +// input EA_MaxSpreadPips is const at runtime, so we need a mutable working copy +double g_workingMaxSpreadPips = 0; // Initialized from EA_MaxSpreadPips in OnInit, overridden by AutoOpt +double g_workingOBVolumeMult = 0; // * v7.5b: Initialized from OB_VolumeMultiplier, overridden by AutoOpt +// * v9.31 FIX#102: Pair+TF profile -- SL minimum and commission +double g_workingSL_MinPips = 5.0; // Minimum SL in pips (pair-specific, from settings table) +double g_workingCommissionPerLot = 7.0; // Commission per lot (pair-specific, from settings table) +bool g_workingKZ_ShowBoxes; +bool g_workingShowDashboard; +// * v9.03 FIX#11: Working globals for TF-aware detection parameters +int g_workingOTE_MaxAge; // Effective OTE max age +int g_workingBB_MaxAge; // Effective Breaker Block max age +int g_workingMB_MaxAge; // Effective Mitigation Block max age +int g_workingTrendline_MaxAge; // Effective Trendline max age +int g_workingCRT_Lookback; // Effective CRT lookback +int g_workingCRT_Expiry; // Effective CRT expiry +int g_workingTBS_Expiry; // Effective TBS expiry +int g_workingAMD_AccumMaxBars; // Effective AMD accumulation max bars +int g_workingSB_MaxAge; // Effective Silver Bullet max age +int g_workingSignalExpiryBars; // Effective signal expiry bars +double g_workingFVG_MinStrength; // Effective FVG min strength +int g_workingRegime_Lookback; // Effective regime lookback +int g_workingRegime_ConfirmBars; // Effective regime confirm bars +int g_workingDivergence_Lookback; // Effective divergence lookback +int g_workingTrendline_Lookback; // Effective trendline discovery lookback +int g_workingFIB_Lookback; // Effective Fibonacci lookback +// * v9.16 FIX: Working globals for TF-scaled indicator inputs (were used but never declared) +int g_workingTC_EMA_Fast = 0; // TC EMA fast period (AutoOpt override) +int g_workingTC_EMA_Slow = 0; // TC EMA slow period (AutoOpt override) +int g_workingTC_PullbackBars = 0; // TC pullback bars (AutoOpt override) +int g_workingVP_Period = 0; // Volume Profile period (AutoOpt override) +int g_workingMM_Lookback = 0; // Market Maker lookback (AutoOpt override) +int g_workingPD_LookbackBars = 0; // Premium/Discount lookback (AutoOpt override) +int g_workingWinProb_LookbackTrades = 0; // Win probability lookback (AutoOpt override) +double g_workingJudas_SL_ATR = 0; // Judas Swing SL ATR mult (AutoOpt override) +double g_workingTBS_MinSweepATR = 0; // TBS min sweep ATR (AutoOpt override) +double g_workingTBS_MaxSweepATR = 0; // TBS max sweep ATR (AutoOpt override) +double g_workingAMD_ManipMoveATR = 0; // AMD manipulation move ATR (AutoOpt override) +double g_workingAMD_DistMinMove = 0; // AMD distribution min move (AutoOpt override) +int g_workingNews_MinsBeforeHigh = 0; // News filter mins before (AutoOpt override) +int g_workingNews_MinsAfterHigh = 0; // News filter mins after (AutoOpt override) +int g_workingRegime_ADRPeriod = 20; // ADR lookback period (AutoOpt override, was hardcoded 20) +// * v9.16 FIX#47: Working globals for WinProb weights (AutoOpt override) +// BUG: AutoOpt calculated wp_trend_weight etc per pair but calculation used raw input -> DEAD values +double g_workingWP_TrendWeight = 0.25; // WinProb trend weight +double g_workingWP_StructureWeight = 0.20; // WinProb structure weight +double g_workingWP_ZoneWeight = 0.15; // WinProb zone weight +double g_workingWP_ConfluenceWeight = 0.15; // WinProb confluence weight +double g_workingWP_TimingWeight = 0.13; // WinProb timing weight +double g_workingWP_PatternWeight = 0.12; // WinProb pattern weight +double g_workingWP_MinThreshold = 0.60; // WinProb minimum threshold +// * v9.16 FIX#47: Working globals for PosSize regime multipliers (AutoOpt override) +// BUG: AutoOpt calculated pos_trending_bonus per pair but PosSize used raw input -> DEAD values +double g_workingPosSize_TrendingBonus = 1.2; // Position sizing trending bonus +double g_workingPosSize_RangingPenalty = 0.7; // Position sizing ranging penalty +int g_workingTBS_ConfirmBars = 3; // * v9.16 FIX#48: TBS confirmation bars (AutoOpt TF-scaled) +int g_workingCorr_UpdateMins = 60; // * v9.16 FIX#48: Correlation update frequency (AutoOpt TF-scaled) +// * v9.24 FIX#82: New working vars -- complete AutoOpt coverage +double g_workingTC_RSI_Min = 45.0; // TC bullish RSI floor (TF-aware) +double g_workingTC_RSI_Max = 55.0; // TC bearish RSI cap (TF-aware) +double g_workingTC_MinSlopeATR = 0.05; // TC EMA slope threshold (TF-aware) +double g_workingTC_BaseScore = 25.0; // TC signal base score (TF-aware) +double g_workingTP1_RR = 2.0; // MultiTP TP1 R:R (TF+pair-aware) +double g_workingTP2_RR = 2.5; // MultiTP TP2 R:R (TF+pair-aware) +double g_workingTP3_RR = 4.0; // MultiTP TP3 R:R (TF+pair-aware) +double g_workingMTF_MinConfidence = 70.0; // MTF min confidence % (pair-aware) +double g_workingRegime_TrendThresh = 48.0;// Trend detection threshold (TF-aware) +double g_workingRegime_ADXMin = 25.0; // ADX min for trend (TF-aware) +double g_workingCRT_MinRangeATR = 0.8; // CRT min range ATR (TF-aware) +double g_workingCRT_MaxRangeATR = 3.0; // CRT max range ATR (TF-aware) +double g_workingJudas_TP1_RR = 1.5; // Judas TP1 R:R (TF-aware) +double g_workingJudas_TP2_RR = 2.5; // Judas TP2 R:R (TF-aware) +double g_workingJudas_TP3_RR = 4.0; // Judas TP3 R:R (TF-aware) +double g_workingTBS_TP1_RR = 1.0; // TBS TP1 R:R (TF-aware) +double g_workingTBS_TP2_RR = 2.0; // TBS TP2 R:R (TF-aware) +double g_workingTBS_TP3_RR = 3.0; // TBS TP3 R:R (TF-aware) +double g_workingVSA_MinStrength = 60.0; // VSA min strength (pair+vol-aware) +//+------------------------------------------------------------------+ +//| [ML] FULL AUTO-OPTIMIZATION - STRUCTURES & GLOBALS | +//+------------------------------------------------------------------+ +// Timeframe category for parameter scaling +enum ENUM_TF_CATEGORY { + TF_CAT_SCALP = 0, // M1-M5 + TF_CAT_INTRADAY = 1, // M15-M30 + TF_CAT_INTRASWING = 2,// H1 (* v9.03 FIX#11b: was lumped with H4) + TF_CAT_SWING = 3, // H4 + TF_CAT_POSITION = 4 // D1+ +}; +// Volatility regime +enum ENUM_VOL_REGIME { + VOL_VERY_LOW = 0, + VOL_LOW = 1, + VOL_NORMAL = 2, + VOL_HIGH = 3, + VOL_EXTREME = 4 +}; +// Auto-optimized parameter set +struct AutoOptParams { + // Core trading parameters + double sl_atr_mult; + double tp_atr_mult; + double min_rr; + double risk_pct; + int max_daily_trades; + // FVG parameters + double fvg_min_size; + int fvg_max_age; + int fvg_extend_bars; + // OB parameters + double ob_volume_mult; + int ob_max_age; + // Structure parameters + int struct_swing_strength; + // Liquidity parameters + int liq_swing_strength; + int liq_max_age; + // Confluence / Quality + double min_confluence; + double min_entry_quality; + int min_entry_score; + // Session filter + bool use_session_filter; + bool session_london; + bool session_ny; + bool session_asian; + // Strategy selection + bool allow_fvg_entry; + bool allow_ob_entry; + bool allow_breaker_entry; + bool allow_liq_grab; + bool allow_bos_retest; + bool allow_ote; + bool allow_scalping; + bool allow_swing; + // Filters + double rsi_overbought; + double rsi_oversold; + double max_spread_pips; + double atr_min_value; + double atr_max_value; + // Smart Entry + int smart_min_confidence; + double smart_min_win_prob; + double smart_min_ev; + // Win Probability weights + double wp_trend_weight; + double wp_structure_weight; + double wp_zone_weight; + double wp_confluence_weight; + double wp_timing_weight; + double wp_pattern_weight; + double wp_min_threshold; + // Position sizing + double pos_trending_bonus; + double pos_ranging_penalty; + // * v9.03: TF-Aware Position Management (override inputs when AutoOpt ON) + double breakeven_rr; // EA_BreakEven_RR override + double trail_start_rr; // EA_TrailStart_RR override + double trail_stop_atr; // EA_TrailStop_ATR override + double smart_exit_min_rr; // EA_SmartExit_MinProfit_RR override + int smart_exit_signals; // EA_SmartExit_Signals override + double trail_volatile_start; // EA_Trail_Volatile_Start override + double trail_volatile_dist; // EA_Trail_Volatile_Dist override + double trail_trending_start; // EA_Trail_Trending_Start override + int max_positions_per_signal; // * FIX#304: 0=use EA global, >0=per-TF cap + int score_cap; // * FIX#305: 0=no cap, >0=reject composite≥this + // * v10.26 FIX#312: per-TF counter-trend thresholds (from pair table). + // 0 = use hardcoded defaults in EvaluateSmartEntry. + double ct_ev_min; // Min EV for CT (0=0.20R hardcoded Forex) + int ct_score_min; // Min score for CT (0=58 hardcoded Forex) + double ct_wp_min; // Min WP for CT at low EV (0=62% hardcoded) + double ct_wp_mid; // Min WP for CT at mid EV ≥0.25R (0=58%) + double ct_wp_high; // Min WP for CT at high EV ≥0.50R (0=55%) + // * FIX#309: per-TF overrides that survive UpdateAutoOptimization resets. + // Set by ApplyPairTFProfile. 0 = no override (use normal AutoOpt/global flow). + double min_conf_override; // FIX#309: bypasses EA_MinEntryScore ceiling in ComputeActiveGates + double se_minrr_override; // FIX#309b: protects se_minrr from AutoOpt per-bar reset + double trail_trending_dist; // EA_Trail_Trending_Dist override + // v9.15 FIX#36: TF-scaled indicator inputs + int tc_ema_fast; int tc_ema_slow; int tc_pullback_bars; + int vp_period; int mm_lookback; int pd_lookback; + int winkprob_lookback; double judas_sl_atr; double tbs_min_sweep_atr; + double tbs_max_sweep_atr; double amd_manip_move_atr; double amd_dist_min_move; + int news_mins_before_high; int news_mins_after_high; int regime_adr_period; + // * v9.03 FIX#11: TF-Aware DETECTION Parameters (indicator-level) + // Ages (bars) -- scale DOWN for higher TFs (fewer bars = same real time) + int ote_max_age; // g_workingOTE_MaxAge override + int bb_max_age; // g_workingBB_MaxAge (Breaker Blocks) override + int mb_max_age; // g_workingMB_MaxAge (Mitigation Blocks) override + int trendline_max_age; // g_workingTrendline_MaxAge override + int crt_lookback; // g_workingCRT_Lookback override + int crt_expiry; // g_workingCRT_Expiry override + int tbs_expiry; // g_workingTBS_Expiry override + int tbs_confirmation_bars; // * v9.16 FIX#48: TF-scaled confirmation bars + int corr_update_mins; // * v9.16 FIX#48: TF-scaled correlation update frequency + int amd_accum_max_bars; // g_workingAMD_AccumMaxBars override + int sb_max_age; // g_workingSB_MaxAge override + int signal_expiry_bars; // g_workingSignalExpiryBars override + // Sensitivity -- adjust detection quality per TF + double fvg_min_strength; // g_workingFVG_MinStrength override + int regime_lookback; // g_workingRegime_Lookback override + int regime_confirm_bars; // g_workingRegime_ConfirmBars override + int divergence_lookback; // g_workingDivergence_Lookback override + int trendline_lookback; // g_workingTrendline_Lookback (detection range) override + int fib_lookback; // g_workingFIB_Lookback override + // * v9.24 FIX#82: TC parameters (TF-aware) + double tc_rsi_min; // TC RSI floor for bullish (TF-scaled from TC_RSI_Min) + double tc_rsi_max; // TC RSI cap for bearish (TF-scaled from TC_RSI_Max) + double tc_min_slope_atr; // TC EMA slope threshold (TF-scaled from TC_MinSlopeATR) + double tc_base_score; // TC signal base score (TF-scaled from TC_BaseScore) + // * v9.24 FIX#82: TP R:R ratios (TF+pair-aware) + double tp1_rr; // MultiTP TP1 R:R (TF-scaled from InpTP1_RR) + double tp2_rr; // MultiTP TP2 R:R (TF-scaled from InpTP2_RR) + double tp3_rr; // MultiTP TP3 R:R (TF-scaled from InpTP3_RR) + // * v9.24 FIX#82: MTF + Regime thresholds (TF+pair-aware) + double mtf_min_confidence; // MTF min confidence % (pair-scaled from MTF_MinConfidence) + double regime_trend_threshold; // Trend detection threshold (TF-scaled from Regime_TrendThreshold) + double regime_trend_adx_min; // ADX min for trend (TF-scaled from Regime_TrendADXMin) + // * v9.24 FIX#82: CRT range limits (TF-aware) + double crt_min_range_atr; // CRT min range ATR (TF-scaled from CRT_MinRangeATR) + double crt_max_range_atr; // CRT max range ATR (TF-scaled from CRT_MaxRangeATR) + // * v9.24 FIX#82: Judas Swing TP R:Rs (TF-aware) + double judas_tp1_rr; // Judas TP1 R:R (TF-scaled from Judas_TP1_RR) + double judas_tp2_rr; // Judas TP2 R:R (TF-scaled from Judas_TP2_RR) + double judas_tp3_rr; // Judas TP3 R:R (TF-scaled from Judas_TP3_RR) + // * v9.24 FIX#82: TBS TP R:Rs (TF-aware) + double tbs_tp1_rr; // TBS TP1 R:R (TF-scaled from TBS_TP1_RR) + double tbs_tp2_rr; // TBS TP2 R:R (TF-scaled from TBS_TP2_RR) + double tbs_tp3_rr; // TBS TP3 R:R (TF-scaled from TBS_TP3_RR) + // * v9.24 FIX#82: VSA strength threshold (pair+vol-aware) + double vsa_min_strength; // VSA min strength (pair-scaled from VSA_MinStrength) + // * v9.31 FIX#102: Pair+TF profile fields (from settings table) + double sl_min_pips; // Minimum SL in pips for this pair/TF + double commission_per_lot; // Commission per lot (used in cost calc) + // Metadata + ENUM_TF_CATEGORY tf_category; + ENUM_VOL_REGIME vol_regime; + string pair_category; + string applied_reason; + datetime last_update; + int recalc_count; +}; +// Market condition snapshot +struct MarketSnapshot { + double current_atr; + double avg_atr_20; + double avg_atr_50; + double atr_percentile; // 0-100 where ATR sits vs history + double current_spread; + double avg_spread; + double spread_ratio; // current/avg + double daily_range; + double avg_daily_range; + double hourly_range; + double volatility_ratio; // current vs avg + double trend_strength; // 0-100 + int trend_direction; // 1=bull, -1=bear, 0=range + double range_score; // 0-100 how ranging the market is + bool is_trending; + bool is_ranging; + bool is_high_vol; + bool is_low_vol; + ENUM_VOL_REGIME vol_regime; + ENUM_TF_CATEGORY tf_category; + int current_hour; + int current_dow; + bool is_killzone; + string active_session; + datetime snapshot_time; +}; +// Globals +datetime g_lastTrailCheck = 0, g_lastSmartExitCheck = 0, g_lastDOLCheck = 0; +// FIX#378-382: Market Context + Smart Exit 2.0 globals +MarketContext g_mktCtx; // Current market context (updated each bar) +datetime g_mktCtxLastBar = 0; // Last bar context was computed +double g_structTrailSL = 0; // FIX#382: current structure-based trail SL +datetime g_structTrailLastBar = 0; // Last bar structure trail was updated + +// * v10.14 FIX#294b: Communication bridge between SmartExit_Check and trail system. +// When SmartExit has signalsNeeded-1 categories (1 short of closing), trail pre-tightens +// by using ATR×0.8 instead of ATR×1.5. This catches the reversal BEFORE full SE fires. +// g_smartExitWarning removed — ProfitGuard_Trail now calls SmartExit_Check() directly +AutoOptParams g_autoOptParams; +// * v9.31 FIX#114: Optimized profile per pair+TF loaded from CSV +struct OptimizedProfile +{ + string symbol; + int tf; + double score; + int trades; + double risk_pct; + double sl_atr; + double tp1_atr; + double tp2_atr; + double tp3_atr; + double min_rr; + int min_score; + int w_fvg; + int w_ob; + int w_breaker; + int w_tc; + int w_judas; + bool sess_london; + bool sess_ny; + bool sess_asian; + double trail_act_rr; + double be_rr; + double smartexit_rr; + bool loaded; +}; +OptimizedProfile g_optProfiles[50]; +int g_optProfileCount = 0; +bool g_optProfilesLoaded = false; +MarketSnapshot g_marketSnap; +datetime g_lastAutoOptCalc = 0; +bool g_autoOptInitialized = false; +int g_autoOptRecalcCount = 0; +string g_autoOptLog = ""; + +// ---- PairTFConfig struct (unified pair profile - v10.01 REFACTOR) ---- +struct PairTFConfig +{ + double sl[5]; // SL ATR multiplier per TF [M5, M15, H1, H4, D1] + double tp1[5]; // TP1 ATR multiplier + double tp2[5]; // TP2 ATR multiplier + double tp3[5]; // TP3 ATR multiplier (runner) + double mrr[5]; // Min R:R required — MUST satisfy tp1[i]/sl[i] >= mrr[i] + double risk[5]; // Risk % per TF + double sl_min[5]; // Min SL in points + double spread[5]; // Max spread in points per TF + // * v10.13 FIX#289: min_conf and min_ev are now arrays [5] per TF. + // Previously single values → EURUSD H4 min_conf=34 also changed EURUSD M15. + // Now each TF has its own calibrated threshold from backtest results. + // Default (0) = use category default from UpdateAutoOptimization. + double min_conf[5]; // Min score per TF (0=use category default ~36 Major) + double min_ev[5]; // Min EV per TF (0=use category default 0.08 Major) + // * v10.19 FIX#303: SmartExit and filter thresholds per TF — same pattern as min_conf/min_ev. + // Default (0) = use global input value. + // se_minrr: SmartExit min profit R before firing (0=global EA_SmartExit_MinProfit_RR) + // req_kz: Require Killzone: 0=global, 1=force ON, -1=force OFF + // req_trend:Require HTF Trend: 0=global, 1=force ON, -1=force OFF + double se_minrr[5]; + int req_kz[5]; + int req_trend[5]; + // * v10.20 FIX#304: per-TF position sizing and score cap. + // max_positions: max simultaneous positions per signal (0=use EA global). + // score_cap: composite score above this = reject (overfitting zone). 0=no cap. + int max_positions[5]; + int score_cap[5]; + // * v10.25 FIX#311: per-pair+TF overrides — ALL calibrations stored here. + // Convention: 0 = use global input / category default (no override). + // tp1_pct/tp2_pct/tp3_pct: TP close % per TF (0=EA_TP1_Percent etc.) + // M5 EURUSD: 60/25/15 — 0 TP2/TP3 hits in 86 trades; 25%→60% TP1 fixes EV. + // H4/H1/M15: 0 → keep global 25/25/50 (TP3 reachable on longer TFs). + // block_neutral: D1 CHoCH neutral-phase block: 0=global, 1=force ON, -1=force OFF. + // M5=1: prevents directional lock in D1 ranging; H4/H1=0: OBs valid in range. + // trendline_lb: trendline lookback override. 0=AutoOpt default. -1=disable. + // M5=-1: 30-bar=2.5h lookback → false trendlines → disable cleanly. + // rng_pen_tf: ranging lot penalty per TF. 0=category default. + // M5=0.80: removes extra SCALP ×0.75 penalty (was 0.80×0.75=0.60 total). + // Win streak +15% was cancelled by -40% ranging penalty on M5. + int tp1_pct[5]; // TP1 close % (0=global EA_TP1_Percent) + int tp2_pct[5]; // TP2 close % (0=global EA_TP2_Percent) + int tp3_pct[5]; // TP3 close % (0=global EA_TP3_Percent) + int block_neutral[5]; // D1 neutral block: 0=global, 1=ON, -1=OFF + int trendline_lb[5]; // Trendline lookback: 0=default, -1=disable, N=override + double rng_pen_tf[5]; // Ranging penalty: 0=category default, >0=override + // * v10.26 FIX#312: per-TF counter-trend thresholds. + // When isCounterTrend=true, the CT quality gate uses these per-TF thresholds instead of hardcoded. + // 0 = use hardcoded defaults (0.20R EV, score 58, WP 62%). + // H4 EURUSD: BOS_RETEST 100% WR but 14 CT blocks in 99 days. Relax to allow quality CT setups. + double ct_ev_min[5]; // Min EV for counter-trend (0=hardcoded 0.20R Forex) + int ct_score_min[5]; // Min score for counter-trend (0=hardcoded 58 Forex) + double ct_wp_min[5]; // Min WinProb for counter-trend at low EV (0=hardcoded 62%) + double ct_wp_mid[5]; // Min WinProb for CT at medium EV ≥0.25R (0=hardcoded 58%) + double ct_wp_high[5]; // Min WinProb for CT at high EV ≥0.50R (0=hardcoded 55%) + // * v10.27 FIX#314: per-TF position sizing calibration fields. + // tr_bonus_tf: trending regime lot bonus per TF (0=c.tr_bonus category default) + // loss_streak_cut_tf: loss streak lot cut per TF (0=PosSize_LossStreakCut global input) + // max_lot_tf: user-defined max lot cap per TF (0=broker SYMBOL_VOLUME_MAX) + // H4: tr_bonus=1.35 (vs default 1.20), loss_streak_cut=0.08 (vs global 0.15) + // Evidence: score=88 got ×0.769 while score=81 got ×0.980 — regime penalty dominated. + // LossStreakCut=0.15 cut score=88 trade by 23% after one prior SL — too aggressive. + // H4 has 5 trades/month: each SL must not disable the next 2 trades' lot potential. + double tr_bonus_tf[5]; // Trending bonus per TF (0=c.tr_bonus) + double loss_streak_cut_tf[5];// Loss streak cut per TF (0=PosSize_LossStreakCut global) + double max_lot_tf[5]; // Max lot cap per TF (0=SYMBOL_VOLUME_MAX) + double comm; // Commission per lot (round trip) + double tr_bonus; // Trending bonus multiplier + double rng_pen; // Ranging penalty multiplier + double min_wp; // Min win probability + string category; // "Major","Metal","Index","Crypto","Energy","VolatileCross","Exotic" + // * v10.31 FIX#320: CHOPPY/RANGING protection fields — stored in pair table per TF. + // All defaults = 0 (disabled). Non-zero = H4/TF-specific override. + // choppy_min_conf: minimum score required when regime=CHOPPY (0=no extra filter) + // choppy_lot_mult: lot multiplier in CHOPPY regime (0=no reduction, e.g. 0.60=60%) + // block_tc_choppy: 1=block TC strategy in CHOPPY/RANGING (TC=trend continuation needs trend) + // mtf_hard_block: 1=hard block trade when MTF strongly opposes direction (vs Structure) + // min_conf_trend: min score when regime=TRENDING (may be lower than choppy) + double choppy_min_conf[5]; // Min score in CHOPPY regime (0=disabled) + double choppy_lot_mult[5]; // Lot multiplier in CHOPPY (0=no change, 0.60=60%) + int block_tc_choppy[5]; // Block TC in CHOPPY/RANGING (0=allow, 1=block) + int mtf_hard_block[5]; // Hard block when MTF=STRONG_BEAR/BULL opposes trade + int d1choch_gate[5]; // D1 CHoCH gate per TF: 0=off, 1=on (overrides EA_D1CHoCHGate input) + // * FIX#363: per-TF OB signal override. + // allow_ob[tf]: 0=default (use global EnableOB + allow_ob_entry), 1=force-on, -1=force-off. + // H4 EURUSD: -1 (OB WR=30%, Net=-$905 over 185 trades — structural trap zones). + // All other pairs/TFs: 0 (inherit global). + int allow_ob[5]; // OB override per TF: 0=global, 1=force-on, -1=force-off + // * FIX#372: per-TF BOS_RETEST signal override (same pattern as allow_ob). + // -1=disable, 0=default (global enabled), 1=force-on. + // EURUSD H4: -1 (BOS_RETEST 1W/8L WR=11%, -$283 in post-filter backtest). + int allow_bos_retest[5]; // BOS_RETEST override: 0=global, 1=force-on, -1=force-off + // * FIX#454: per-TF overrides for remaining techniques — same -1/0/1 convention. + // 0 = inherit global input (Enable*). 1 = force-on. -1 = force-off. + // Needed: FIX#223 was "dead" — XAUUSD M5 FVG/OB block lived only in comments. + // These arrays are the correct architectural place for per-pair/per-TF technique control. + int allow_fvg[5]; // FVG override: 0=global EnableFVG, 1=force-on, -1=force-off + int allow_ote[5]; // OTE override: 0=global EnableOTE, 1=force-on, -1=force-off + int allow_liq[5]; // LIQ override: 0=global EnableLiquidity, 1=force-on, -1=force-off + int allow_breaker[5]; // BREAKER override: 0=global EnableBreakerBlocks, -1=force-off + int allow_tc[5]; // TC override: 0=global EnableTrendCont, -1=force-off + // * FIX#373: Mean Reversion per-TF config (CHOPPY/RANGING regime entries) + // mr_enabled: -1=force-off, 0=use Regime_UseMeanReversion global, 1=force-on + // mr_rsi_buy: RSI threshold for BUY (price at rangeLow). 0=use global default (35) + // mr_rsi_sell: RSI threshold for SELL (price at rangeHigh). 0=use global default (65) + // mr_sl_mult: SL = beyond extreme + (mr_sl_mult × ATR). 0=use 0.3 default + // mr_min_range_atr: min rangeWidthATR before MR activates. 0=use 1.5 default + int mr_enabled[5]; // MR override per TF: 0=global, 1=force-on, -1=force-off + int mr_rsi_buy[5]; // RSI BUY threshold (0=default 35) + int mr_rsi_sell[5]; // RSI SELL threshold (0=default 65) + double mr_sl_mult[5]; // SL buffer beyond extreme (0=default 0.3) + double mr_min_range_atr[5]; // Min range width in ATR units (0=default 1.5) + // * FIX#421: SmartExit per-TF calibration — all SE thresholds in pair table. + // Convention: 0 = use built-in defaults in SmartExit_Check: + // se_rsi_ob=70, se_rsi_os=30, se_peak_th1=0.65, se_peak_th2=0.72, + // se_peak_th3=0.78, se_override_rr=0 (2-cat override disabled). + // Non-zero = pair+TF calibrated override, read/written by AutoOpt each bar. + double se_rsi_ob[5]; // SmartExit RSI OB threshold (0=default 70) + double se_rsi_os[5]; // SmartExit RSI OS threshold (0=default 30) + double se_peak_th1[5]; // Peak trail ratio peak 0.60-1.50R (0=default 0.65) + double se_peak_th2[5]; // Peak trail ratio peak 1.50-2.50R (0=default 0.72) + double se_peak_th3[5]; // Peak trail ratio peak >=2.50R (0=default 0.78) + double se_override_rr[5]; // 2-cat exit override min RR (0=disabled) + // Cooperative SmartExit: FIX#294b uses g_smartExitWarning (global flag set by + // SmartExit_Check when 1 signal short) → trail pre-tightens to ATR×0.8 automatically. + // No per-ticket catCount needed — g_smartExitWarning is sufficient for H1 timeframe. + double se_mom_ratio[5]; // bar shrink ratio (0=default 0.70) + double se_mom_score_mult[5]; // score mult on momentum fade (0=default 1.40) + double max_cost_pct[5]; // Max cost % of SL per TF (0=use global COST_MaxCostPercent) + // min_confluence_cap: max allowed min_confluence for this TF. + // 0 = use FIX#165 TF defaults (H4=0.65, H1=0.77, M15=0.79). + // >0 = override FIX#165 cap — pair table wins. + // EURUSD H1: 0.60 (FVG/OB typically achieves 0.60-0.75, 0.77 cap blocks most signals) + double min_confluence_cap[5]; // FIX#165 cap override per TF (0=use TF default) +}; + +int g_autoOpt_ATR_Handle = INVALID_HANDLE; +// Professional Dashboard globals +bool g_dashMinimized = false; +int g_dashWidth = 280; +// [v6.42] Will be set from DASH_XOffset/DASH_YOffset in OnInit +// [v6.42] TradingStylePreset checked in OnInit for style overrides +ENUM_TRADING_STYLE g_activeStyle = STYLE_CUSTOM; // Set from TradingStylePreset +int g_dashStartX = 10; +int g_dashStartY = 25; +int g_dashLineHeight = 16; +int g_dashPanelSpacing = 8; +// =================================================================== +// DISPLAY TOGGLES +// =================================================================== +bool g_fvgDisplayEnabled = true; +bool g_obDisplayEnabled = true; +bool g_liqDisplayEnabled = true; +bool g_structDisplayEnabled = true; +bool g_oteDisplayEnabled = true; +bool g_breakerDisplayEnabled = true; +bool g_mitigationDisplayEnabled = true; +// =================================================================== +// TEST MODE +// =================================================================== +bool g_testModeActive = false; +// =================================================================== +// UI STATE +// =================================================================== +bool g_checklistVisible = false; +int g_currentChecklistSignal = -1; +bool g_buttonsCreated = false; +// =================================================================== +// JOURNAL +// =================================================================== +int g_journalFileHandle = INVALID_HANDLE; +bool g_journalInitialized = false; +datetime g_lastJournalWrite = 0; +// =================================================================== +// PERFORMANCE MONITORING +// =================================================================== +datetime g_lastMemoryLog = 0; +uint g_tickStart = 0; // [v6.42] Processing time measurement +ulong g_totalCalculationTime = 0; +int g_totalCalculations = 0; +datetime g_lastPerformanceReport = 0; +// =================================================================== +// FIBONACCI GLOBALS +// =================================================================== +FIB_Struct g_fibData; +bool g_fibInitialized = false; +datetime g_fibLastUpdate = 0; +int g_fibLevelCount = 0; +double g_fibCurrentLevel = 0; +// Standard Fibonacci Levels +double g_fibStandardLevels[] = {0.0, 0.236, 0.382, 0.5, 0.618, 0.705, 0.786, 0.886, 1.0}; +double g_fibExtensionLevels[] = {1.272, 1.414, 1.618, 2.0, 2.618}; +// ICT Important Levels +double g_fibICTLevels[] = {0.0, 0.5, 0.618, 0.705, 0.786, 1.0}; +// =================================================================== +// SPREAD ANALYSIS GLOBALS +// =================================================================== +SpreadAnalysis g_spreadAnalysis; +double g_spreadHistory[]; +int g_spreadHistoryIndex = 0; +datetime g_lastSpreadUpdate = 0; +// =================================================================== +// DIVERGENCE GLOBAL VARIABLES +// =================================================================== +// Enhanced Divergence globals +DivergenceStruct g_divergences[]; +int g_divergenceCount = 0; +int g_maxDivergences = 30; +int g_divIdCounter = 0; +// Session SL Adjustment globals +ENUM_SESSION_SL_TYPE g_currentSessionSL = SESSION_SL_DEAD_ZONE; +double g_sessionSLMultiplier = 1.0; +datetime g_lastSessionCheck = 0; +// Pivot arrays for divergence detection +double g_swingHighs[]; +double g_swingLows[]; +int g_swingHighBars[]; +int g_swingLowBars[]; +double g_rsiBuffer[]; +bool g_rsiReady = false; +bool g_bullishDivergence = false; +bool g_bearishDivergence = false; +// Enhanced Trendline globals +TrendlineStruct g_trendlines[]; +int g_trendlineCount = 0; +int g_maxTrendlines = 20; +int g_tlIdCounter = 0; +// Risk Management Globals +RiskManagement g_risk; +SpreadInfo g_spread; +PartialCloseInfo g_partialPositions[]; +// News Filter Globals (αν δεν υπάρχουν ήδη) +datetime g_nextHighImpactNews = 0; +datetime g_lastNewsCheck = 0; +bool g_newsFilterActive = false; +// Trendline status flags +bool g_tlSupportActive = false; +bool g_tlResistanceActive = false; +bool g_tlBreakBullish = false; +bool g_tlBreakBearish = false; +bool g_tlRetestBullish = false; +bool g_tlRetestBearish = false; +// Old compatibility flags (keep these!) +bool g_bullTrendlineActive = false; +bool g_bearTrendlineActive = false; +bool g_trendlineBreakBull = false; // <- ΑΥΤΟ ΛΕΙΠΕΙ! +bool g_trendlineBreakBear = false; // <- ΚΑΙ ΑΥΤΟ! +// =================================================================== +// ENTRY SCORING GLOBAL VARIABLES +// =================================================================== +EntryScoreStruct g_lastEntryScore; +ConfluenceScoreStruct g_lastConfluence; // [NEW] ΛΕΙΠΕΙ +CandlePatternStruct g_lastCandlePattern; +int g_scoreObjCount = 0; // [NEW] ΛΕΙΠΕΙ +// Multi-TP Arrays +MultiTPEntry g_multiTPEntries[]; +MultiTPStats g_multiTPStats; +int g_multiTPCounter = 0; +// Symbol Profile Structure +struct SymbolProfile +{ + string name; // Symbol name + string category; // "GOLD", "FOREX_MAJOR", "INDEX", etc + double pointsMultiplier; // Multiplier για FVG sizes, etc + double volatilityFactor; // Volatility adjustment factor + double optimalMinConfluence; // Optimal confluence threshold + double optimalRiskReward; // Optimal R:R ratio + double optimalRisk; // Optimal risk % per trade + int optimalSwingStrength; // Optimal swing detection strength + bool tradeAsian; // Trade Asian session? + bool tradeLondon; // Trade London session? + bool tradeNY; // Trade NY session? + string preferredTimeframes; // Best timeframes για αυτό το symbol +}; +// Global symbol profile +SymbolProfile g_symbolProfile; +bool g_autoOptimizationApplied = false; +// Working variables (χρησιμοποιούνται σε όλο τον κώδικα) +double g_workingAccountRiskPercent; +int g_workingSTRUCT_SwingStrength; +int g_workingLIQ_SwingStrength; +int g_workingOB_MaxAge; // * v9.03 FIX#13: was missing -- detection used raw input +int g_workingLIQ_MaxAge; // * v9.03 FIX#13: was missing -- detection used raw input +//+------------------------------------------------------------------+ +//| | +//| SECTION 6: FORWARD DECLARATIONS | +//| | +//+------------------------------------------------------------------+ +// =================================================================== +// INITIALIZATION FUNCTIONS +// =================================================================== +void RunSharedAnalysis(const datetime &time[], const double &open[], const double &high[], + const double &low[], const double &close[], const long &tick_volume[], + int rates_total, int detectionLimit); +bool InitializeAllArrays(); +bool ValidateInputs(); +void FreeAllArrays(); +void AdaptParametersToTimeframe(); +void PrintInitializationSummary(); +void InitializeWorkingVariables(); +void InitializeStrategyNames(); +void InitializeTradeJournal(); +void InitializePredictionBuffers(); +void InitializeHeatmap(); +// =================================================================== +// NEURAL NETWORK FUNCTIONS +// =================================================================== +bool InitializeNeuralNetwork(); +void DestroyNeuralNetwork(); +bool TrainNeuralNetwork(const datetime &time[], const double &open[], + const double &high[], const double &low[], + const double &close[], const long &volume[]); +double PredictWithNeuralNetwork(double &features[]); +void ForwardPass(double &inputs[]); +void BackwardPass(double &targets[]); +void UpdateWeights(); +void ApplySoftmax(int layerIndex); +double CalculateLoss(double &targets[]); +double ValidateNetwork(); +double ApplyActivation(double x, ENUM_NN_ACTIVATION activation); +double ApplyActivationDerivative(double x, ENUM_NN_ACTIVATION activation); +void ExtractFeatures(int barIndex, const datetime &time[], const double &open[], + const double &high[], const double &low[], const double &close[], + const long &volume[], double &features[]); +void NormalizeFeatures(double &features[]); +bool SaveNeuralNetwork(); +bool LoadNeuralNetwork(); +// =================================================================== +// KILLZONE FUNCTIONS +// =================================================================== +void InitializeKillzones(); +void InitializeDSTInfo(); +void UpdateKillzones(datetime currentTime); +bool IsInKillzone(datetime time, ENUM_KILLZONE_TYPE &kzType); +bool IsInAnyKillzone(datetime time); +void DrawKillzones(const datetime &time[], const double &high[], const double &low[]); +void DeleteKillzoneObjects(); +// int GetUTCOffset(); // * REMOVED: Dead forward declaration (never implemented, never called) +bool IsUSDST(datetime time); +bool IsEUDST(datetime time); +bool IsUKDST(datetime time); +datetime ConvertToUTC(datetime brokerTime); +datetime ConvertFromUTC(datetime utcTime); +void GetAdjustedKillzoneTimes(int kzIndex, int &startHour, int &startMin, int &endHour, int &endMin); +double GetKillzoneQualityBonus(ENUM_KILLZONE_TYPE kzType); +bool PassesKillzoneFilter(); +string GetCurrentKillzoneName(); +double GetKillzoneWinRate(int kzIndex); +void UpdateKillzoneStats(int kzIndex, bool isWin, double pips); +int GetNthSunday(int year, int month, int n); +int GetLastSunday(int year, int month); +datetime GetNextDSTChange(datetime currentTime); +int GetTimezoneOffset(ENUM_TIMEZONE tz); +int GetLocalUTCOffset(); +string GetTimezoneString(ENUM_TIMEZONE tz); +int DetectBrokerUTCOffset(); +// =================================================================== +// PERSISTENCE FUNCTIONS +// =================================================================== +bool InitializePersistence(); +bool SavePerformanceData(); +bool LoadPerformanceData(); +void ForceFlushAllEntryGroups(); // * v9.31 FIX#116 +void LoadOptimizedProfiles(); +void SaveOptimizedProfile(string symbol, int tf, double score); +void ApplyOptimizedProfileToAutoOpt(string symbol, int tf); +int FindOptimizedProfile(string symbol, int tf); +bool SaveTradeToHistory(TradeRecord &trade); +bool ExportTradeHistory(); +string GetDataFilePath(string filename); +bool CreateDataFolder(); +void BackupPerformanceFile(); +void UpdatePerformanceFromTrade(TradeRecord &trade); +void RecalculatePerformanceStats(); +void CalculateRiskAdjustedReturns(); +void UpdateDailyStats(TradeRecord &trade); +void UpdateStrategyStats(TradeRecord &trade); +bool LoadTradeHistory(); +ENUM_KILLZONE_TYPE GetKillzoneTypeFromName(string name); +int GetDailyStatsCount(); +string GetPerformanceSummary(); +void PrintStrategyRanking(); +bool ExportMLHistory(); +void WriteToJournal(string type, SIGNAL_Struct &signal, string notes = ""); +bool MigratePerformanceData(string filepath, int oldVersion); +// =================================================================== +// BACKTESTING FUNCTIONS +// =================================================================== +void InitializeBacktest(); +void ProcessBacktestBar(int barIndex, const datetime &time[], const double &open[], + const double &high[], const double &low[], const double &close[]); +void FinalizeBacktest(); +void CalculateBacktestMetrics(); +void DrawEquityCurve(); +void RunMonteCarloSimulation(); +void ExportBacktestResults(); +void AddBacktestTrade(SIGNAL_Struct &signal, datetime entryTime, double entryPrice); +double CalculateBacktestLotSize(double entryPrice, double stopLoss); +void CheckBacktestExits(int barIndex, const datetime &time[], + const double &high[], const double &low[], const double &close[]); +void CloseBacktestTrade(int tradeIndex, datetime exitTime, double exitPrice, string exitReason); +void RecordEquityPoint(datetime time); +void CalculateBacktestRiskMetrics(); +void CalculateStrategyBreakdownMetrics(); +void RunWalkForwardOptimization(); +void DeleteEquityCurveObjects(); +void PrintBacktestResults(); +double GetOptimizationScore(); +// =================================================================== +// SIGNAL & RISK FUNCTIONS +// =================================================================== +double CalculateEntryQuality(double price, bool isBullish, string strategy, double confluence); +void UpdateActiveSignals(const datetime &time[], const double &high[], + const double &low[], const double &close[]); +void UpdateSignalVisual(int index, string status); +void UpdatePerformanceStats(SIGNAL_Struct &signal, bool isWin); +void InitializeRiskManagement(); +void UpdateRiskMetrics(); +double CalculatePositionSize(double entryPrice, double stopLoss); +// FIX#378-382 forward decls — defined before EA_CheckSignals +// (bodies appear ~42780 onward; no separate forward decl needed in MQL5 +// because definitions appear before all callers) +void CreateSignal(datetime signalTime, double entryPrice, double sl, double tp, + bool isBullish, string strategy, double confluence, + double quality, double mlConfidence); +void DrawSignal(SIGNAL_Struct &signal); +// FIX#502: Scenario profile forward declarations +bool IsPullbackActive(); +bool IsFlagPattern(); +bool IsRetestInProgress(); +bool IsFakeoutCondition(); +bool IsCompressionSetup(); +bool HasRecentCHoCH(); +ScenarioProfile DeriveScenarioProfile(const MarketContext &ctx); +double GetScenarioWeightMult(ENUM_SCENARIO scenario, ENUM_ENTRY_TECHNIQUE tech); +void AdjustSLTPForScenario(); +void GenerateSignals_ForScenario(const datetime &time[], const double &open[], + const double &high[], const double &low[], + const double &close[], const long &volume[]); +// =================================================================== +// ICT DETECTION FUNCTIONS +// =================================================================== +void DetectFVG(const datetime &time[], const double &open[], const double &high[], + const double &low[], const double &close[], int limit); +void UpdateFVGStatus(const datetime &time[], const double &high[], + const double &low[], const double &close[]); +void DrawFVGs(const datetime &time[]); +void AddFVG(datetime time, double top, double bottom, bool isBullish, bool isInverse, int barIndex); +ENUM_FVG_QUALITY CalculateFVGQuality(int index); +double CalculateFVGStrength(int index, int barIndex); +void DeleteFVGObjects(int index); +void DetectOrderBlocks(const datetime &time[], const double &open[], const double &high[], + const double &low[], const double &close[], const long &volume[], int limit); +void UpdateOrderBlockStatus(const datetime &time[], const double &high[], + const double &low[], const double &close[]); +void DrawOrderBlocks(const datetime &time[]); +void AddOrderBlock(datetime time, double high, double low, double open, double close, + bool isBullish, long vol, long avgVol); +void DetectLiquidity(const datetime &time[], const double &high[], + const double &low[], const double &close[], int limit); +void CheckLiquiditySweeps(const datetime &time[], const double &high[], + const double &low[], const double &close[]); +void DrawLiquidity(const datetime &time[]); +void AddLiquidityLevel(datetime time, double price, bool isBSL, int touches); +void DrawLiquiditySweep(int index); +void DetectMarketStructure(const datetime &time[], const double &high[], + const double &low[], const double &close[], int limit); +void AddSwingPoint(datetime time, double price, bool isHigh, int barIndex); +double CalculateSwingStrength(int barIndex, bool isHigh); +void AnalyzeStructureBreaks(const datetime &time[], const double &high[], + const double &low[], const double &close[]); +void MarkSwingAsBroken(datetime swingTime); +void DrawBOSLine(datetime time, double price, bool isBullish, string type); +void UpdateStructureBias(); +void ComputeDrawOnLiquidity(); +void DetectOTEZones(const datetime &time[], const double &high[], + const double &low[], const double &close[], int limit); +void DrawOTEZones(const datetime &time[]); +void DetectBreakerBlocks(const datetime &time[], const double &open[], const double &high[], + const double &low[], const double &close[], int limit); +void DrawBreakerBlocks(const datetime &time[]); +void DetectMitigationBlocks(const datetime &time[], const double &open[], const double &high[], + const double &low[], const double &close[], int limit); +void DrawMitigationBlocks(const datetime &time[]); +void DetectOrderFlowImbalance(const datetime &time[], const double &open[], const double &high[], + const double &low[], const double &close[], const long &volume[], int limit); +void CalculateVolumeProfile(const datetime &time[], const double &high[], const double &low[], + const double &close[], const long &volume[], int limit); +void DrawVolumeProfile(const datetime &time[]); +void DetectMarketMakerPhase(const datetime &time[], const double &open[], const double &high[], + const double &low[], const double &close[], const long &volume[], int limit); +void DrawMarketMakerPhases(const datetime &time[]); +void UpdatePremiumDiscountZones(const double &high[], const double &low[], const double &close[]); +void DrawPDZones(double rangeHigh, double rangeLow, double eq, double premStart, double discEnd); +// =================================================================== +// SIGNAL GENERATION FUNCTIONS +// =================================================================== +void GenerateSignals(const datetime &time[], const double &open[], + const double &high[], const double &low[], + const double &close[], const long &volume[]); +void CheckFVGEntrySignal(const datetime &time[], const double &high[], + const double &low[], const double &close[], + const double &open[], double currentPrice); +void CheckOBEntrySignal(const datetime &time[], const double &high[], + const double &low[], const double &close[], + const double &open[], double currentPrice); +void CheckBOSRetestSignal(const datetime &time[], const double &high[], + const double &low[], const double &close[], + const double &open[], double currentPrice); +void CheckLiquidityGrabSignal(const datetime &time[], const double &high[], + const double &low[], const double &close[], + const double &open[], double currentPrice); +void CheckOTEEntrySignal(const datetime &time[], const double &high[], + const double &low[], const double &close[], + const double &open[], double currentPrice); +void CheckBreakerBlockSignal(const datetime &time[], const double &high[], + const double &low[], const double &close[], double currentPrice); +void CheckMitigationBlockSignal(const datetime &time[], const double &high[], + const double &low[], const double &close[], double currentPrice); +void CheckMarketMakerSignal(const datetime &time[], const double &high[], const double &low[], + const double &close[], const long &volume[], double currentPrice); +void CheckMLSignal(const datetime &time[], const double &open[], + const double &high[], const double &low[], + const double &close[], const long &volume[], + double currentPrice); +void CheckKillzoneSignal(const datetime &time[], const double &high[], + const double &low[], const double &close[], + double currentPrice); +void CheckMultiConfluenceSignals(const datetime &time[], const double &open[], const double &high[], + const double &low[], const double &close[], const long &volume[], + double currentPrice); +void CheckTCEntrySignal(const datetime &time[], const double &high[], + const double &low[], const double &close[], + const double &open[], double currentPrice); +void CheckTBSEntrySignal(const datetime &time[], const double &high[], + const double &low[], const double &close[], + const double &open[], double currentPrice); +void CheckMeanReversionSignal(const datetime &time[], const double &high[], + const double &low[], const double &close[], + const double &open[], double currentPrice); +// =================================================================== +// ML PREDICTION FUNCTIONS +// =================================================================== +void ProcessMLPredictions(const datetime &time[], const double &open[], + const double &high[], const double &low[], + const double &close[], const long &volume[]); +void GenerateNNPrediction(const datetime &time[], const double &open[], + const double &high[], const double &low[], + const double &close[], const long &volume[]); +void ValidatePreviousPredictions(const double &close[]); +void UpdateMLAccuracy(); +void UpdatePredictionVisualization(const datetime &time[], const double &close[]); +void UpdateProbabilityHeatmap(const datetime &time[], const double &high[], + const double &low[], const double &close[]); +void DrawHeatmapLevel(int index, datetime time, double price, double height, double strength); +// =================================================================== +// UTILITY FUNCTIONS +// =================================================================== +bool ValidatePrice(double price); +double CalculateMemoryUsage(); +int GetStrategyIndex(string strategyName); +bool IsInTradingSession(); +ENUM_TIMEFRAMES GetHigherTimeframe(ENUM_TIMEFRAMES currentTF); +bool GetTimeframeBias(ENUM_TIMEFRAMES tf); +bool IsPriceInFVG(double price, int &fvgIndex, bool bullishOnly = false); +bool IsPriceNearOB(double price, int &obIndex, bool bullishOnly = false); +bool IsPriceInOTE(double price, int &oteIndex); +// =================================================================== +// MAINTENANCE FUNCTIONS +// =================================================================== +void UpdateCachedIndicators(); +void PerformMaintenanceTasks(datetime currentTime); +void CleanupOldObjects(); +void CompactArrays(); +void UpdateDashboard(); +void CreateLabel(string name, int x, int y, string text, color clr, int fontSize); +// Professional Dashboard forward declarations +void UpdateProfessionalDashboard(); +void DrawDashboardTitleBar(int x, int y); +int DrawMarketOverviewPanel(int x, int y); +int DrawICTConceptsPanel(int x, int y); +int DrawEntryScorePanel(int x, int y); +int DrawSignalsPanel(int x, int y); +int DrawMultiTPPanel(int x, int y); +int DrawPerformancePanel(int x, int y); +int DrawNewFeaturesPanel(int x, int y); +int DrawRiskDDPanel(int x, int y); // * v8.06 NEW: Risk % + Drawdown panel +int DrawBuildPanel(int x, int y); // * v9.43: eaEURUSD version + fixes + last backtest +void CreateDashRect(string name, int x, int y, int width, int height, color bgColor, color borderColor); +void CreateDashLabel(string name, int x, int y, string text, color clr, int fontSize, string fontName); +void DrawProgressBar(int x, int y, int width, int height, double value, double maxValue, color fillColor, string id = ""); // * v8.07: added id for unique names +void DrawScoreBar(int x, int y, int width, int height, int score, int maxScore); +void DrawPDZoneBar(int x, int y, int width, int height); +void CleanupDashboard(); +void WarmupCache(); +void PrintLoadedPerformanceStats(); +void PrintNNArchitecture(); +string GetDeinitReasonText(int reason); +// =================================================================== +// PAIR & BROKER FUNCTIONS +// =================================================================== +void InitializeBrokerConfig(); +// void LoadPairProfile(); // * REMOVED: Dead forward declaration (never implemented, never called) +// =================================================================== +// NEW FEATURE INITIALIZATION FUNCTIONS +// =================================================================== +void InitializeFullFibonacci(); +void InitializeCostAnalysis(); +void InitializeAllPairProfiles(); +bool LoadCurrentPairProfile(); +void CreateControlButtons(); +// Unified pair profile system (v10.01 REFACTOR) +PairTFConfig GetPairTFConfig(string sym); +double GetPairTFMinRR(string sym, ENUM_TIMEFRAMES tf); +void ApplyPairTFProfile(); +void ValidatePairTFConfigs(); +void ComputeActiveGates(); // * v10.06 FIX#275: fills g_gates once per bar +// * FIX#369: unified score — single source of truth for ALL gates +UnifiedScoreResult ComputeUnifiedScore(bool isBullish, double entryPrice, + double slPrice, double tp1Price, + double tp2Price, double tp3Price, + const ConfluenceScoreStruct &confluence, + const CandlePatternStruct &candle, + int obQuality, + const ConfirmationCascade &cascade, + bool cascadeAvailable); +// =================================================================== +// UI EVENT HANDLERS +// =================================================================== +void DisplaySignalDetails(int signalIndex); +void HandleSignalClick(string objName); +void HandleButtonClick(string objName); +void HandleKeyPress(int key); +// =================================================================== +// CHART PATTERN GLOBALS +// =================================================================== +// Head & Shoulders +HeadShouldersPattern g_hsPatterns[]; +int g_hsCount = 0; +bool g_hasHeadShoulders = false; +HeadShouldersPattern g_currentHS; +// Double/Triple Top-Bottom +MultipleTopBottomPattern g_mtbPatterns[]; +int g_mtbCount = 0; +bool g_hasMultipleTopBottom = false; +MultipleTopBottomPattern g_currentMTB; +// Triangles +TrianglePattern g_trianglePatterns[]; +int g_triangleCount = 0; +bool g_hasTriangle = false; +TrianglePattern g_currentTriangle; +// Flags & Pennants +FlagPennantPattern g_fpPatterns[]; +int g_fpCount = 0; +bool g_hasFlagPennant = false; +FlagPennantPattern g_currentFP; +// Wedges +WedgePattern g_wedgePatterns[]; +int g_wedgeCount = 0; +bool g_hasWedge = false; +WedgePattern g_currentWedge; +// Diamonds +DiamondPattern g_diamondPatterns[]; +int g_diamondCount = 0; +bool g_hasDiamond = false; +DiamondPattern g_currentDiamond; +// V-Patterns +VPattern g_vPatterns[]; +int g_vCount = 0; +bool g_hasVPattern = false; +VPattern g_currentVPattern; +// Extended Candlestick Patterns +CandlePatternStructExtended g_extendedCandlePatterns[]; +int g_extendedCandleCount = 0; +CandlePatternStructExtended g_lastExtendedCandlePattern; +// Master Pattern Container +AllPatternsData g_allPatterns; +bool g_patternsInitialized = false; +datetime g_patternsLastUpdate = 0; +// Pattern Detection Helpers +datetime g_swingHighTimes[]; +datetime g_swingLowTimes[]; +int g_swingHighCount = 0; +int g_swingLowCount = 0; +// VSA Globals +VSA_Pattern g_vsaPatterns[]; +int g_vsaCount = 0; +int g_maxVSAPatterns = 50; +VSA_Pattern g_currentVSA; +bool g_vsaInitialized = false; +bool g_ea_has_open_buy = false; // * vFIX: updated each bar for direction-conflict guard +bool g_ea_has_open_sell = false; // * vFIX: updated each bar for direction-conflict guard +bool g_antiHedge_blockBuy = false; // * v9.03: Block BUY when SELL is open (anti-hedging) +bool g_antiHedge_blockSell = false; // * v9.03: Block SELL when BUY is open (anti-hedging) +// * v9.38 FIX#166: Intra-bar zone detection flag for H4+ +// On H4, price can enter a FVG/OB zone mid-bar and exit before the next bar open. +// Without this, OnNewBar (bar-open-only) never sees the zone touch = missed entries. +bool g_intraBarChecked = false; // reset each new bar, set true after intra-bar check fires +// MTF Globals +MTF_Analysis g_mtfAnalysis; +TF_Bias g_tfBiases[]; +int g_activeTFCount = 0; +ENUM_TIMEFRAMES g_activeTimeframes[]; +ENUM_TIMEFRAMES g_primaryTF = PERIOD_H1; +bool g_mtfInitialized = false; +datetime g_lastMTFUpdate = 0; +// MA Handles for MTF +int g_mtfMAHandles[]; +int g_mtfMA20Handles[]; // * v9.09 FIX#16e: EMA(20) for slope detection +//+------------------------------------------------------------------+ +//| MULTI-TP MODULE - ALL FUNCTIONS IN CORRECT ORDER | +//| Add this BEFORE OnInit() in your oploindi.mq5 file | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| CleanObject - Κεντρική Ασφαλής Διαγραφή Object | +//+------------------------------------------------------------------+ +bool CleanObject(string objName) +{ + if(objName == "" || objName == NULL) + return false; + if(ObjectFind(0, objName) < 0) + return false; + bool result = ObjectDelete(0, objName); + if(!result && EnableDebugMode) + { + Print("[WARN] Failed to delete object: ", objName); + } + return result; +} +//+------------------------------------------------------------------+ +//| 1. Calculate Multi-TP Levels | +//+------------------------------------------------------------------+ +void CalculateMultiTPLevels(double entryPrice, double stopLoss, int direction, + double &tp1, double &tp2, double &tp3) +{ + double riskPoints = MathAbs(entryPrice - stopLoss); + // * v9.11 FIX#18b: When AutoOpt is active, use ATR-based TPs (same as EA orders) + // Old: Always used InpTP_RR (R:R based) -> indicator showed WRONG levels vs actual orders + // New: When pair thresholds loaded, calculate from ATR multipliers for consistency + // * v9.24 FIX#82: Use TF+pair-aware working vars (AutoOpt-scaled) + double tp1_rr = (AutoOpt_Enabled && g_workingTP1_RR > 0) ? g_workingTP1_RR : InpTP1_RR; + double tp2_rr = (AutoOpt_Enabled && g_workingTP2_RR > 0) ? g_workingTP2_RR : InpTP2_RR; + double tp3_rr = (AutoOpt_Enabled && g_workingTP3_RR > 0) ? g_workingTP3_RR : InpTP3_RR; + // FIX#164: Use profile TP ratios only in AutoOpt mode + if(AutoOpt_Enabled && g_gates.computed && g_autoOptParams.sl_atr_mult > 0) + { + double _tp2R = 1.35, _tp3R = 1.75; + GetTP2TP3Ratios(g_autoOptParams.tf_category, g_autoOptParams.pair_category, _tp2R, _tp3R); + tp1_rr = g_autoOptParams.tp_atr_mult / g_autoOptParams.sl_atr_mult; + tp2_rr = (g_autoOptParams.tp_atr_mult * _tp2R) / g_autoOptParams.sl_atr_mult; + tp3_rr = (g_autoOptParams.tp_atr_mult * _tp3R) / g_autoOptParams.sl_atr_mult; + } + if(direction == 1) // Long + { + tp1 = entryPrice + (riskPoints * tp1_rr); + tp2 = entryPrice + (riskPoints * tp2_rr); + tp3 = entryPrice + (riskPoints * tp3_rr); + // Auto-adjust to nearby levels if enabled + if(InpAutoAdjustTPToCRT) AdjustTPToCRT(tp1, tp2, tp3, direction); + if(InpAutoAdjustTPToFVG) AdjustTPToFVG(tp1, tp2, tp3, direction); + if(InpAutoAdjustTPToLiquidity) AdjustTPToLiquidity(tp1, tp2, tp3, direction); + } + else // Short + { + tp1 = entryPrice - (riskPoints * tp1_rr); + tp2 = entryPrice - (riskPoints * tp2_rr); + tp3 = entryPrice - (riskPoints * tp3_rr); + // Auto-adjust to nearby levels if enabled + if(InpAutoAdjustTPToCRT) AdjustTPToCRT(tp1, tp2, tp3, direction); + if(InpAutoAdjustTPToFVG) AdjustTPToFVG(tp1, tp2, tp3, direction); + if(InpAutoAdjustTPToLiquidity) AdjustTPToLiquidity(tp1, tp2, tp3, direction); + } +} +//+------------------------------------------------------------------+ +//| 2. Auto-Adjust TP to CRT Levels | +//+------------------------------------------------------------------+ +void AdjustTPToCRT(double &tp1, double &tp2, double &tp3, int direction) +{ + double tolerance = g_cachedATR * 0.5; + for(int i = 0; i < ArraySize(g_crtSetups); i++) + { + if(!g_crtSetups[i].active) continue; + // Use CRT projections as target levels + double crtTargetUp = g_crtSetups[i].projectionUp; + double crtTargetDown = g_crtSetups[i].projectionDown; + double crtTP1 = g_crtSetups[i].takeProfit1; + double crtTP2 = g_crtSetups[i].takeProfit2; + double crtTP3 = g_crtSetups[i].takeProfit3; + if(direction == 1) // Long - adjust TPs up to CRT targets + { + // Check projection level + if(crtTargetUp > 0 && MathAbs(tp1 - crtTargetUp) < tolerance && crtTargetUp > tp1) + tp1 = crtTargetUp; + if(crtTargetUp > 0 && MathAbs(tp2 - crtTargetUp) < tolerance && crtTargetUp > tp2) + tp2 = crtTargetUp; + if(crtTargetUp > 0 && MathAbs(tp3 - crtTargetUp) < tolerance && crtTargetUp > tp3) + tp3 = crtTargetUp; + // Check CRT TP levels + if(crtTP1 > 0 && MathAbs(tp1 - crtTP1) < tolerance && crtTP1 > tp1) + tp1 = crtTP1; + if(crtTP2 > 0 && MathAbs(tp2 - crtTP2) < tolerance && crtTP2 > tp2) + tp2 = crtTP2; + if(crtTP3 > 0 && MathAbs(tp3 - crtTP3) < tolerance && crtTP3 > tp3) + tp3 = crtTP3; + } + else // Short - adjust TPs down to CRT targets + { + // Check projection level + if(crtTargetDown > 0 && MathAbs(tp1 - crtTargetDown) < tolerance && crtTargetDown < tp1) + tp1 = crtTargetDown; + if(crtTargetDown > 0 && MathAbs(tp2 - crtTargetDown) < tolerance && crtTargetDown < tp2) + tp2 = crtTargetDown; + if(crtTargetDown > 0 && MathAbs(tp3 - crtTargetDown) < tolerance && crtTargetDown < tp3) + tp3 = crtTargetDown; + // Check CRT TP levels + if(crtTP1 > 0 && MathAbs(tp1 - crtTP1) < tolerance && crtTP1 < tp1) + tp1 = crtTP1; + if(crtTP2 > 0 && MathAbs(tp2 - crtTP2) < tolerance && crtTP2 < tp2) + tp2 = crtTP2; + if(crtTP3 > 0 && MathAbs(tp3 - crtTP3) < tolerance && crtTP3 < tp3) + tp3 = crtTP3; + } + } +} +//+------------------------------------------------------------------+ +//| 3. Auto-Adjust TP to FVG Levels | +//+------------------------------------------------------------------+ +void AdjustTPToFVG(double &tp1, double &tp2, double &tp3, int direction) +{ + double tolerance = g_cachedATR * 0.5; + for(int i = 0; i < ArraySize(FVG_Array); i++) + { + if(FVG_Array[i].status != FVG_STATUS_ACTIVE) continue; + // CE is already calculated in the structure + double fvgCE = FVG_Array[i].ce; + // If CE is not set, calculate it + if(fvgCE == 0) + fvgCE = (FVG_Array[i].top + FVG_Array[i].bottom) / 2.0; + if(direction == 1 && !FVG_Array[i].isBullish) // Long - target bearish FVG above + { + if(MathAbs(tp1 - fvgCE) < tolerance && fvgCE > tp1) + tp1 = fvgCE; + if(MathAbs(tp2 - fvgCE) < tolerance && fvgCE > tp2) + tp2 = fvgCE; + if(MathAbs(tp3 - fvgCE) < tolerance && fvgCE > tp3) + tp3 = fvgCE; + } + else if(direction == -1 && FVG_Array[i].isBullish) // Short - target bullish FVG below + { + if(MathAbs(tp1 - fvgCE) < tolerance && fvgCE < tp1) + tp1 = fvgCE; + if(MathAbs(tp2 - fvgCE) < tolerance && fvgCE < tp2) + tp2 = fvgCE; + if(MathAbs(tp3 - fvgCE) < tolerance && fvgCE < tp3) + tp3 = fvgCE; + } + } +} +//+------------------------------------------------------------------+ +//| 4. Auto-Adjust TP to Liquidity Levels | +//+------------------------------------------------------------------+ +void AdjustTPToLiquidity(double &tp1, double &tp2, double &tp3, int direction) +{ + double tolerance = g_cachedATR * 0.5; + for(int i = 0; i < ArraySize(LIQ_Array); i++) + { + if(!LIQ_Array[i].isValid || LIQ_Array[i].swept) continue; + double liqLevel = LIQ_Array[i].price; + if(direction == 1 && LIQ_Array[i].isBSL) // Long - target BSL above + { + if(MathAbs(tp1 - liqLevel) < tolerance && liqLevel > tp1) + tp1 = liqLevel; + if(MathAbs(tp2 - liqLevel) < tolerance && liqLevel > tp2) + tp2 = liqLevel; + if(MathAbs(tp3 - liqLevel) < tolerance && liqLevel > tp3) + tp3 = liqLevel; + } + else if(direction == -1 && !LIQ_Array[i].isBSL) // Short - target SSL below + { + if(MathAbs(tp1 - liqLevel) < tolerance && liqLevel < tp1) + tp1 = liqLevel; + if(MathAbs(tp2 - liqLevel) < tolerance && liqLevel < tp2) + tp2 = liqLevel; + if(MathAbs(tp3 - liqLevel) < tolerance && liqLevel < tp3) + tp3 = liqLevel; + } + } +} +//+------------------------------------------------------------------+ +//| 5. Add New Multi-TP Entry | +//+------------------------------------------------------------------+ +// * v9.11 FIX#18: Added actual TP prices as parameters +// When called from EA_ExecuteTrade, pass the REAL order TPs (from BuildCandidateSLTP/AutoOpt) +// When called from indicator signal, use defaults (0=recalculate from InpTP_RR) +bool AddMultiTPEntry(double entryPrice, double stopLoss, int direction, + double lotSize = 0.01, long posTicket = 0, + double actualTP1 = 0, double actualTP2 = 0, double actualTP3 = 0) +{ + if(!InpEnableMultiTP) return false; + // * FIX#17b: Aggressive cleanup -- sync with real positions first + SyncMultiTPWithPositions(); + CleanupCompletedMultiTP(); + int activeCount = GetActiveMultiTPCount(); + if(activeCount >= MAX_MULTITP_ENTRIES) + { + Print("[WARN] Maximum Multi-TP entries reached (", MAX_MULTITP_ENTRIES, ")"); + return false; + } + int size = ArraySize(g_multiTPEntries); + ArrayResize(g_multiTPEntries, size + 1); + g_multiTPEntries[size].id = g_multiTPCounter++; + g_multiTPEntries[size].entryTime = TimeCurrent(); + g_multiTPEntries[size].entryPrice = entryPrice; + g_multiTPEntries[size].stopLoss = stopLoss; + g_multiTPEntries[size].currentSL = stopLoss; + g_multiTPEntries[size].direction = direction; + g_multiTPEntries[size].active = true; + g_multiTPEntries[size].riskPoints = MathAbs(entryPrice - stopLoss); + // * v9.36 FIX#138: Cache ATR and SL distance at entry time. + // These NEVER change mid-trade. All trail/BE/SmartExit calculations + // must use these cached values -- NOT live ATR which changes every bar. + g_multiTPEntries[size].atr_at_entry = (g_cachedATR > 0) ? g_cachedATR : MathAbs(entryPrice - stopLoss); + g_multiTPEntries[size].sl_dist_cached = MathAbs(entryPrice - stopLoss); + g_multiTPEntries[size].peakRR = 0.0; // * v9.37 FIX#150: will be updated by ManagePositions each tick + // * FIX#423: Initialize cooperative SE state fields + // * v9.41 FIX#177: Zone reference — copy from winning candidate signal + // g_ea_signal carries zone data set by EA_CheckSignals() for each technique + g_multiTPEntries[size].zoneTop = g_ea_signal.zoneTop; + g_multiTPEntries[size].zoneBottom = g_ea_signal.zoneBottom; + g_multiTPEntries[size].zoneType = g_ea_signal.zoneType; + g_multiTPEntries[size].zoneInvalidated = false; + // * FIX#373: Snapshot market state at entry for AdverseClose pre-existing signal filter. + g_multiTPEntries[size].entryRegime = g_regimeData.regime; + g_multiTPEntries[size].entryStructureBull = g_isBullishStructure; + g_multiTPEntries[size].entryMTFBull = (g_mtfAnalysis.overallDirection == MTF_BULLISH || + g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH || + g_mtfAnalysis.overallDirection == MTF_NEUTRAL); + // * v9.11 FIX#18: Use actual order TPs when provided (from EA execution path) + // Only recalculate from InpTP_RR when called from indicator signal path (TPs=0) + if(actualTP1 > 0 && actualTP2 > 0 && actualTP3 > 0) + { + g_multiTPEntries[size].tp1Price = actualTP1; + g_multiTPEntries[size].tp2Price = actualTP2; + g_multiTPEntries[size].tp3Price = actualTP3; + } + else + { + CalculateMultiTPLevels(entryPrice, stopLoss, direction, + g_multiTPEntries[size].tp1Price, + g_multiTPEntries[size].tp2Price, + g_multiTPEntries[size].tp3Price); + } + g_multiTPEntries[size].initialLotSize = lotSize; + // * FIX#377: Guard against g_workingTP1/2/3_Pct=0 (uninitialized or bad input). + // If any pct=0, lot split gives 0 lots → minLot floor fires → wrong split ratios. + // Fallback: EA_TP1/2/3_Percent inputs (always set by user, default 25/25/50). + int _tp1Pct = (g_workingTP1_Pct > 0) ? g_workingTP1_Pct : (int)EA_TP1_Percent; + int _tp2Pct = (g_workingTP2_Pct > 0) ? g_workingTP2_Pct : (int)EA_TP2_Percent; + int _tp3Pct = (g_workingTP3_Pct > 0) ? g_workingTP3_Pct : (int)EA_TP3_Percent; + g_multiTPEntries[size].tp1Lots = NormalizeDouble(lotSize * _tp1Pct / 100.0, 2); // * FIX#377: via guarded _tp1Pct + g_multiTPEntries[size].tp2Lots = NormalizeDouble(lotSize * _tp2Pct / 100.0, 2); // * FIX#377 + g_multiTPEntries[size].tp3Lots = NormalizeDouble(lotSize * _tp3Pct / 100.0, 2); // * FIX#377 + g_multiTPEntries[size].remainingLots = lotSize; + // * v9.30 FIX#100: Per-trade dynamic threshold calculation + // Instead of hardcoded global thresholds, each trade gets thresholds + // scaled to ITS OWN TP1/SL ratio. Works correctly for ALL pairs & TFs. + // + // Logic: + // tp1_R = TP1_distance / SL_distance (e.g. H4: 33p/41p = 0.80R) + // BE = tp1_R x 0.50 -> fire halfway to TP1 + // Trail = tp1_R x 0.55 -> start trailing just above BE + // SE = tp1_R x 0.40 -> SmartExit checks from 40% of TP1 distance + // + // Floor: minimum 0.25R (avoids noise-triggered BE on very small TPs) + // Ceiling: maximum 1.5R (avoids delayed BE on very large TPs) + { + double sl_dist = MathAbs(entryPrice - stopLoss); + double tp1_dist = MathAbs(g_multiTPEntries[size].tp1Price - entryPrice); + double tp1_R = (sl_dist > 0) ? (tp1_dist / sl_dist) : 1.0; + g_multiTPEntries[size].tp1_R_ratio = tp1_R; + // * FIX#365: H4+ BE threshold lowered from 0.50× to 0.40× of TP1_R. + // ROOT CAUSE: With new FIX#359 TP1_R=2.0 (sl=1.60, tp1=3.20), perTrade_BE_RR=1.0R. + // 3 near-miss SELL trades peaked at 0.83-0.99R and reversed fully to SL (-$279 total). + // BE at 1.0R never fired. Reducing to 0.40× → BE = 2.0×0.40 = 0.80R → catches 0.83R+. + // Floor MathMax(0.50) ensures BE never fires below 0.5R (noise protection unchanged). + // H4+ only: lower TFs use faster TP1_R ratios where 0.50× is correct. + // DATA: 3 near-miss = -$279. Catching them adds ~$93 avg per trade = +$279 projected. + // FIX#508: BE trigger lowered for

= PERIOD_H4) ? 0.40 : 0.40; // FIX#508: 0.50→0.40 for

= PERIOD_D1) ? 1.50 : + (_Period >= PERIOD_H4) ? 1.20 : 0.90; + g_multiTPEntries[size].perTrade_BE_RR = MathMax(0.50, MathMin(beCeiling, tp1_R * beMultiplier)); + // * FIX#399: H4+ trail multiplier 0.55→0.35, SE multiplier 0.60→0.40 + // PROBLEM: perTrade_Trail = MathMax(0.50, 1.60×0.55) = 0.88R + // effectiveTrailStart = MathMax(global=1.20R, perTrade=0.88R) = 1.20R ← global kills it + // Trade 7 peaked at 0.71R < 1.20R → trail NEVER fired → full SL hit + // FIX A: Lower H4 multipliers so perTrade is realistic for choppy H4 moves + // H4 trail: 0.35 × 1.60R = 0.56R (catches 0.62R+ peaks) + // H4 SE: 0.40 × 1.60R = 0.64R (evaluates from 0.64R) + // FIX B: effectiveTrailStart uses MathMin (per-trade wins on H4, global on lower TFs) + // → effectiveTrailStart = MathMin(0.56R, 1.20R) = 0.56R ← per-trade wins + double _trailMult399 = (_Period >= PERIOD_H4) ? 0.35 : 0.55; + double _seMult399 = (_Period >= PERIOD_H4) ? 0.40 : 0.60; + g_multiTPEntries[size].perTrade_Trail_RR = MathMax(0.40, MathMin(1.8, tp1_R * _trailMult399)); + // SE floor raised 0.40→0.60R: EvaluateExit should not close below 0.60R profit. + // On H1 EURUSD (TP1=1.50R): SE = 1.50×0.60 = 0.90R → evaluates from 0.90R + // The 0.40R floor was allowing exit at noise-level profit (observed PeakRR=0.03-0.22R) + g_multiTPEntries[size].perTrade_SmartExit_RR = MathMax(0.60, MathMin(1.5, tp1_R * _seMult399)); + g_multiTPEntries[size].isCounterTrend = false; // * v9.31 FIX#104c: set by caller + g_multiTPEntries[size].entryConfirmTime = TimeCurrent(); + // * v9.38 FIX#168: Always log FIX#100 thresholds (removed EnableDebugMode gate). + // Bug: User sets EA_BreakEven_RR=0.3 but FIX#100 overrides to 0.90R (tp1_R x 0.50). + // This critical override was SILENT unless EnableDebugMode=true, making it impossible + // to diagnose why BE/Trail/SE never fired in production runs. + PrintFormat("* FIX#100 Per-Trade Thresholds: TP1=%.2fR | BE=%.2fR | Trail=%.2fR | SE=%.2fR | SL=%.1fp TP1=%.1fp (input BE=%.2f)", + tp1_R, + g_multiTPEntries[size].perTrade_BE_RR, + g_multiTPEntries[size].perTrade_Trail_RR, + g_multiTPEntries[size].perTrade_SmartExit_RR, + sl_dist / g_pipValue, + tp1_dist / g_pipValue, + EA_BreakEven_RR); + } + double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); + if(g_multiTPEntries[size].tp1Lots < minLot) g_multiTPEntries[size].tp1Lots = minLot; + if(g_multiTPEntries[size].tp2Lots < minLot) g_multiTPEntries[size].tp2Lots = minLot; + if(g_multiTPEntries[size].tp3Lots < minLot) g_multiTPEntries[size].tp3Lots = minLot; + g_multiTPEntries[size].tp1Hit = false; + g_multiTPEntries[size].tp2Hit = false; + g_multiTPEntries[size].tp3Hit = false; + g_multiTPEntries[size].slMovedToBE = false; + g_multiTPEntries[size].isAutoAdjusted = false; + g_multiTPEntries[size].adjustmentReason = ""; + g_multiTPEntries[size].ticket = posTicket; // * FIX#17b: Link to real MT5 position + g_multiTPEntries[size].objName = "MTP_" + IntegerToString(g_multiTPEntries[size].id); + DrawMultiTPEntry(g_multiTPEntries[size]); + g_multiTPStats.totalEntries++; + Print("[OK] Multi-TP Entry Created: ID=", g_multiTPEntries[size].id, + " | Dir=", (direction == 1 ? "LONG" : "SHORT"), + " | Entry=", DoubleToString(entryPrice, _Digits), + " | SL=", DoubleToString(stopLoss, _Digits), + " | TP1=", DoubleToString(g_multiTPEntries[size].tp1Price, _Digits), + " | TP2=", DoubleToString(g_multiTPEntries[size].tp2Price, _Digits), + " | TP3=", DoubleToString(g_multiTPEntries[size].tp3Price, _Digits)); + + // * FIX#479: Create entryGroup at trade OPEN — not at deal close. + // ROOT CAUSE of PeakRR=0.00 across ALL timeframes (M5 to D1): + // 1. entryGroup is created by EA_UpdateTradeResults when it sees DEAL_ENTRY_OUT. + // 2. During the trade, entryGroup doesn't exist → ManagePositions live peakRR + // propagation (FIX#456/469b) always fails: no active group to write to. + // 3. FIX#470 tried to seed peakRR at group creation (close time) by reading + // g_multiTPEntries[].peakRR — but CleanupCompletedMultiTP() removes inactive + // entries after 5 minutes (cutoffTime = TimeCurrent() - 300). + // 4. For M15/H1/H4/D1 trades lasting hours, the multiTPEntry is ALREADY DELETED + // by the time EA_UpdateTradeResults runs → peakRR=0.00 every time. + // M5 worked accidentally: trades close in 5-15min before cleanup deletes the entry. + // + // ARCHITECTURAL FIX: create the entryGroup HERE at trade open, linked to posTicket. + // ManagePositions runs every tick with g_ea_position.Ticket() = posTicket. + // Primary match (positionIDs[]) now works from tick 1 of the trade. + // At close, EA_UpdateTradeResults finds the group already exists (time/price match) + // and just adds the deal to it — peakRR was written live all along. + // No dependency on multiTPEntries at close time. Works M5 through D1. + { + // One-time init guard (mirrors EA_UpdateTradeResults init) + if(!g_entryGroupsInitialized) + { + for(int _gi = 0; _gi < 4; _gi++) // 4 = MAX_ENTRY_GROUPS (defined later in file) + { + g_entryGroups[_gi].totalPnL = 0; + g_entryGroups[_gi].totalRMultiple = 0; + g_entryGroups[_gi].dealCount = 0; + g_entryGroups[_gi].firstDealTime = 0; + g_entryGroups[_gi].posIDCount = 0; + g_entryGroups[_gi].active = false; + g_entryGroups[_gi].peakRR = 0.0; + g_entryGroups[_gi].entryPrice = 0.0; + } + g_entryGroupsInitialized = true; + } + + // Find an existing group for this trade (should not exist yet, but guard anyway) + int _egIdx479 = -1; + for(int _eg = 0; _eg < 4; _eg++) // 4 = MAX_ENTRY_GROUPS + { + if(!g_entryGroups[_eg].active) continue; + // Already have a group with this entry price? (re-entry or multi-leg) + if(g_entryGroups[_eg].entryPrice > 0 && + MathAbs(g_entryGroups[_eg].entryPrice - entryPrice) < g_pipValue * 3) + { _egIdx479 = _eg; break; } + } + + // Allocate new group if none found + if(_egIdx479 < 0) + { + for(int _eg = 0; _eg < 4; _eg++) // 4 = MAX_ENTRY_GROUPS + { + if(!g_entryGroups[_eg].active) + { + _egIdx479 = _eg; + g_entryGroups[_eg].active = true; + g_entryGroups[_eg].totalPnL = 0; + g_entryGroups[_eg].totalRMultiple = 0; + g_entryGroups[_eg].dealCount = 0; + g_entryGroups[_eg].firstDealTime = TimeCurrent(); + g_entryGroups[_eg].posIDCount = 0; + g_entryGroups[_eg].peakRR = 0.0; + g_entryGroups[_eg].entryPrice = entryPrice; + break; + } + } + } + + // Register posTicket as a positionID so ManagePositions primary match works from tick 1 + if(_egIdx479 >= 0 && posTicket > 0) + { + bool _alreadyIn = false; + for(int _p = 0; _p < g_entryGroups[_egIdx479].posIDCount; _p++) + if(g_entryGroups[_egIdx479].positionIDs[_p] == (ulong)posTicket) + { _alreadyIn = true; break; } + if(!_alreadyIn && g_entryGroups[_egIdx479].posIDCount < 6) + { + g_entryGroups[_egIdx479].positionIDs[g_entryGroups[_egIdx479].posIDCount] = (ulong)posTicket; + g_entryGroups[_egIdx479].posIDCount++; + } + if(g_verboseLog) + PrintFormat("[FIX#479] entryGroup %d pre-created at open | Entry=%.5f | PosID=%llu | TF=%s", + _egIdx479, entryPrice, (ulong)posTicket, EnumToString(_Period)); + } + } + + return true; +} +//+------------------------------------------------------------------+ +//| 6. Check if TP is hit | +//+------------------------------------------------------------------+ +bool IsTPHit(double currentPrice, double tpPrice, int direction) +{ + // * v9.16 FIX#45a: ZERO tolerance -- match MT5 server behavior exactly. + // Old: 5-point tolerance -> tracker marked TP "hit" BEFORE MT5 closed -> false positives + // -> visual green but position open, stats wrong, SL Ladder moved on phantom hit. + // Fix: Same logic as MT5 server -- price must REACH or CROSS TP. + if(direction == 1) // Long: bid >= TP + { + return (currentPrice >= tpPrice); + } + else // Short: ask <= TP + { + return (currentPrice <= tpPrice); + } +} +//+------------------------------------------------------------------+ +//| 7. Check if SL is hit | +//+------------------------------------------------------------------+ +bool IsSLHit(double currentPrice, double slPrice, int direction) +{ + // * v9.16 FIX#45a: ZERO tolerance -- match MT5 server behavior exactly + if(direction == 1) // Long: bid <= SL + { + return (currentPrice <= slPrice); + } + else // Short: ask >= SL + { + return (currentPrice >= slPrice); + } +} +//+------------------------------------------------------------------+ +//| * v9.16 FIX#45b: Close specific tranche position by comment | +//| Finds MT5 position matching entry price + direction + tranche | +//| and closes it. Returns true if closed or already gone. | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| * v9.16 FIX#49: Force-close ALL matching EA positions | +//| Called when Signal Tracker detects TP hit via candle HIGH/LOW | +//| This bridges the Signal visual system to actual position mgmt | +//| BUG: In "Open prices only" backtesting, MT5 checks TP only at | +//| bar open, missing intra-bar TP touches. Signal Tracker uses | +//| candle HIGH/LOW -> detects TP -> turns green, but position | +//| stays open because MT5 never saw the TP touch. | +//+------------------------------------------------------------------+ +void ForceCloseMatchingPositions(bool isBullish, double signalEntry) +{ + int closed = 0; + for(int p = PositionsTotal() - 1; p >= 0; p--) + { + if(!g_ea_position.SelectByIndex(p)) continue; + if(g_ea_position.Symbol() != _Symbol) continue; + if(g_ea_position.Magic() != EA_MagicNumber) continue; + // Match direction + bool dirMatch = (isBullish && g_ea_position.PositionType() == POSITION_TYPE_BUY) || + (!isBullish && g_ea_position.PositionType() == POSITION_TYPE_SELL); + if(!dirMatch) continue; + // Match entry price (within 2 pip tolerance -- same signal) + if(MathAbs(g_ea_position.PriceOpen() - signalEntry) > g_pipValue * 3) continue; + // Force close + ulong ticket = g_ea_position.Ticket(); + if(g_ea_trade.PositionClose(ticket)) + { + closed++; + Print("* FIX#49: Signal WIN -> force-closed position #", ticket, + " | Entry=", DoubleToString(g_ea_position.PriceOpen(), _Digits), + " | Profit=", DoubleToString(g_ea_position.Profit(), 2)); + } + } + if(closed > 0) + Print("* FIX#49: Force-closed ", closed, " position(s) on Signal TP hit"); +} +bool CloseTranchePosition(double entryPrice, int direction, string trancheSuffix) +{ + for(int p = PositionsTotal() - 1; p >= 0; p--) + { + if(!g_ea_position.SelectByIndex(p)) continue; + if(g_ea_position.Symbol() != _Symbol) continue; + if(g_ea_position.Magic() != EA_MagicNumber) continue; + // Match direction + bool dirMatch = (direction == 1 && g_ea_position.PositionType() == POSITION_TYPE_BUY) || + (direction == -1 && g_ea_position.PositionType() == POSITION_TYPE_SELL); + if(!dirMatch) continue; + // Match entry price (within 1 pip tolerance) + if(MathAbs(g_ea_position.PriceOpen() - entryPrice) > g_pipValue * 2) continue; + // Match tranche comment + string posComment = g_ea_position.Comment(); + if(StringFind(posComment, trancheSuffix) < 0) continue; + // Found it -- close + ulong ticket = g_ea_position.Ticket(); + if(g_ea_trade.PositionClose(ticket)) + { + Print("* FIX#45b: Closed ", trancheSuffix, " tranche | Ticket #", ticket, + " | Entry=", DoubleToString(entryPrice, _Digits)); + return true; + } + else + { + Print("* FIX#45b: Failed to close ", trancheSuffix, " | Ticket #", ticket, + " | Error=", GetLastError()); + return false; + } + } + return true; // Position not found = already closed by MT5 server TP +} +//+------------------------------------------------------------------+ +//| 8. TP1 Hit Handler | +//+------------------------------------------------------------------+ +void OnTP1Hit(int index) +{ + g_multiTPEntries[index].tp1Hit = true; + g_multiTPEntries[index].tp1HitTime = TimeCurrent(); + g_multiTPEntries[index].remainingLots -= g_multiTPEntries[index].tp1Lots; + double profitR = ((AutoOpt_Enabled && g_workingTP1_RR > 0) ? g_workingTP1_RR : InpTP1_RR) * (double)g_workingTP1_Pct / 100.0; // * FIX#310a: g_workingTP1_Pct + g_multiTPStats.totalProfitTP1 += profitR; + g_multiTPStats.tp1HitCount++; + // * v9.16 FIX#45b: ACTUALLY CLOSE the _TP1 position! + // Old: Only marked tp1Hit=true (visual), relied on MT5 server TP. + // Bug: In "Open prices only" mode, MT5 only checks TP at bar opens -> misses intra-bar TP hits. + // Fix: Explicitly close the _TP1 tranche position. + CloseTranchePosition(g_multiTPEntries[index].entryPrice, + g_multiTPEntries[index].direction, "_TP1"); + if(InpMoveSLToBreakeven && !g_multiTPEntries[index].slMovedToBE) + { + // * v9.24 FIX#79: MultiTP BE sync with FIX#41 trail + // When FIX#41 trail is active, it is the SOLE master of SL movement. + // MultiTP must NOT also compute and apply a ladderSL -- this causes two systems + double atrBuffer = g_multiTPEntries[index].atr_at_entry * 0.4; + double ladderSL; + if(g_multiTPEntries[index].direction == 1) // Long: SL below TP1, at least above entry + { + ladderSL = g_multiTPEntries[index].tp1Price - atrBuffer; + double entryFloor = g_multiTPEntries[index].entryPrice + g_pipValue * 0.5; + if(ladderSL < entryFloor) ladderSL = entryFloor; + } + else // Short: SL above TP1, at least below entry + { + ladderSL = g_multiTPEntries[index].tp1Price + atrBuffer; + double entryFloor = g_multiTPEntries[index].entryPrice - g_pipValue * 0.5; + if(ladderSL > entryFloor) ladderSL = entryFloor; + } + g_multiTPEntries[index].currentSL = ladderSL; + g_multiTPEntries[index].slMovedToBE = true; + // Apply ladder SL to real MT5 positions for all runners (TP2, TP3) + // Without this, runners keep the original SL until ProtectTrade trail catches up — + // exposing them to full loss even after TP1 was secured. + { + double _entryP = g_multiTPEntries[index].entryPrice; + double _entryTol = (g_cachedATR > 0) ? g_cachedATR * 0.15 : g_pipValue * 8; + for(int _p = PositionsTotal()-1; _p >= 0; _p--) + { + ulong _pTkt = PositionGetTicket(_p); + if(_pTkt == 0 || !PositionSelectByTicket(_pTkt)) continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; + if((long)PositionGetInteger(POSITION_MAGIC) != EA_MagicNumber) continue; + bool _sameDir = (g_multiTPEntries[index].direction == 1 && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) || + (g_multiTPEntries[index].direction == -1 && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL); + if(!_sameDir) continue; + if(MathAbs(PositionGetDouble(POSITION_PRICE_OPEN) - _entryP) > _entryTol) continue; + string _cmt = PositionGetString(POSITION_COMMENT); + bool _isRunner = (StringFind(_cmt, "_TP2") >= 0 || StringFind(_cmt, "_TP3") >= 0); + if(!_isRunner) continue; + double _curSL = PositionGetDouble(POSITION_SL); + // Only improve SL — never widen it + bool _improves = (g_multiTPEntries[index].direction == 1) ? (ladderSL > _curSL) + : (ladderSL < _curSL); + if(_improves) + SafePositionModify(_pTkt, ladderSL, PositionGetDouble(POSITION_TP), "TP1Ladder"); + } + } + Print("[OK] TP1 HIT | ID=", g_multiTPEntries[index].id, + " | SL Ladder -> TP1-0.4ATR: ", DoubleToString(ladderSL, _Digits), + " (buf=", DoubleToString(atrBuffer * 10000, 1), "p)"); + } + if(InpEnableTPAlerts) + { + Alert("[TARGET] TP1 HIT! ", _Symbol, + " | +", DoubleToString(profitR, 2), "R", + " | Closed ", InpTP1_Percent, "%"); + } + UpdateMultiTPVisual(g_multiTPEntries[index]); +} +//+------------------------------------------------------------------+ +//| 9. TP2 Hit Handler | +//+------------------------------------------------------------------+ +void OnTP2Hit(int index) +{ + g_multiTPEntries[index].tp2Hit = true; + g_multiTPEntries[index].tp2HitTime = TimeCurrent(); + g_multiTPEntries[index].remainingLots -= g_multiTPEntries[index].tp2Lots; + double profitR = ((AutoOpt_Enabled && g_workingTP2_RR > 0) ? g_workingTP2_RR : InpTP2_RR) * (double)g_workingTP2_Pct / 100.0; // * FIX#310a: g_workingTP2_Pct + g_multiTPStats.totalProfitTP2 += profitR; + g_multiTPStats.tp2HitCount++; + // * v9.16 FIX#45b: ACTUALLY CLOSE the _TP2 position! + CloseTranchePosition(g_multiTPEntries[index].entryPrice, + g_multiTPEntries[index].direction, "_TP2"); + // * v9.03 FIX#4: SL LADDER -- move SL to TP2 level for TP3 runner + // * FIX#MERGE_C4b: ADD 0.4×ATR BUFFER to TP2 SL ladder (mirrors OnTP1Hit FIX#MERGE_C4). + // BUG: SL at exact TP2 price = TP3 runner starts with 0 margin. Any spread or + // micro-retracement after TP2 immediately closes the runner at near-TP2 (≈0R extra). + // FIX: SL = TP2 ∓ (0.4 × ATR_at_entry). Uses atr_at_entry (stable, cached on open). + // 0.4× ATR = half a typical bar range — gives TP3 runner real breathing room. + // TP2 tranche is already secured; runner can afford this buffer. + { + double atrBuffer2 = g_multiTPEntries[index].atr_at_entry * 0.4; + double ladderSL2; + if(g_multiTPEntries[index].direction == 1) // Long: SL below TP2 + ladderSL2 = g_multiTPEntries[index].tp2Price - atrBuffer2; + else // Short: SL above TP2 + ladderSL2 = g_multiTPEntries[index].tp2Price + atrBuffer2; + g_multiTPEntries[index].currentSL = ladderSL2; + // Apply ladder SL to TP3 runner MT5 position + { + double _entryP = g_multiTPEntries[index].entryPrice; + double _entryTol = (g_cachedATR > 0) ? g_cachedATR * 0.15 : g_pipValue * 8; + for(int _p = PositionsTotal()-1; _p >= 0; _p--) + { + ulong _pTkt = PositionGetTicket(_p); + if(_pTkt == 0 || !PositionSelectByTicket(_pTkt)) continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; + if((long)PositionGetInteger(POSITION_MAGIC) != EA_MagicNumber) continue; + bool _sameDir = (g_multiTPEntries[index].direction == 1 && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) || + (g_multiTPEntries[index].direction == -1 && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL); + if(!_sameDir) continue; + if(MathAbs(PositionGetDouble(POSITION_PRICE_OPEN) - _entryP) > _entryTol) continue; + if(StringFind(PositionGetString(POSITION_COMMENT), "_TP3") < 0) continue; + double _curSL = PositionGetDouble(POSITION_SL); + bool _improves = (g_multiTPEntries[index].direction == 1) ? (ladderSL2 > _curSL) + : (ladderSL2 < _curSL); + if(_improves) + SafePositionModify(_pTkt, ladderSL2, PositionGetDouble(POSITION_TP), "TP2Ladder"); + } + } + } + if(InpEnableTPAlerts) + { + Alert("[TARGET][TARGET] TP2 HIT! ", _Symbol, + " | +", DoubleToString(profitR, 2), "R", + " | Closed ", InpTP2_Percent, "%"); + } + Print("[OK] TP2 HIT | ID=", g_multiTPEntries[index].id, + " | Price=", DoubleToString(g_multiTPEntries[index].tp2Price, _Digits), + " | SL Ladder -> TP2-0.4ATR: ", DoubleToString(g_multiTPEntries[index].currentSL, _Digits), + " (buf=", DoubleToString(g_multiTPEntries[index].atr_at_entry * 0.4 * 10000, 1), "p)"); + UpdateMultiTPVisual(g_multiTPEntries[index]); +} +//+------------------------------------------------------------------+ +//| 10. TP3 Hit Handler | +//+------------------------------------------------------------------+ +void OnTP3Hit(int index) +{ + g_multiTPEntries[index].tp3Hit = true; + g_multiTPEntries[index].tp3HitTime = TimeCurrent(); + g_multiTPEntries[index].remainingLots = 0; + g_multiTPEntries[index].active = false; + double profitR = ((AutoOpt_Enabled && g_workingTP3_RR > 0) ? g_workingTP3_RR : InpTP3_RR) * (double)g_workingTP3_Pct / 100.0; // * FIX#310a: g_workingTP3_Pct + g_multiTPStats.totalProfitTP3 += profitR; + g_multiTPStats.tp3HitCount++; + // * v9.16 FIX#45b: ACTUALLY CLOSE the _TP3 position! + CloseTranchePosition(g_multiTPEntries[index].entryPrice, + g_multiTPEntries[index].direction, "_TP3"); + double _rTP1 = (AutoOpt_Enabled && g_workingTP1_RR > 0) ? g_workingTP1_RR : InpTP1_RR; + double _rTP2 = (AutoOpt_Enabled && g_workingTP2_RR > 0) ? g_workingTP2_RR : InpTP2_RR; + double _rTP3 = (AutoOpt_Enabled && g_workingTP3_RR > 0) ? g_workingTP3_RR : InpTP3_RR; + double totalProfitR = (_rTP1 * g_workingTP1_Pct / 100.0) + + (_rTP2 * g_workingTP2_Pct / 100.0) + + (_rTP3 * g_workingTP3_Pct / 100.0); // * FIX#310a: working vars (M5=60/25/15) + if(InpEnableTPAlerts) + { + Alert("[TARGET][TARGET][TARGET] TP3 HIT! FULL TARGET! ", _Symbol, + " | Total: +", DoubleToString(totalProfitR, 2), "R"); + } + Print("[OK] TP3 HIT | ID=", g_multiTPEntries[index].id, + " | TRADE COMPLETED! | Total Profit: +", DoubleToString(totalProfitR, 2), "R"); + UpdateMultiTPVisual(g_multiTPEntries[index]); +} +//+------------------------------------------------------------------+ +//| 11. SL Hit Handler | +//+------------------------------------------------------------------+ +void OnSLHit(int index) +{ + g_multiTPEntries[index].active = false; + double lossR = 0; + if(g_multiTPEntries[index].slMovedToBE) + { + g_multiTPStats.beHitCount++; + double netProfitR = ((AutoOpt_Enabled && g_workingTP1_RR > 0) ? g_workingTP1_RR : InpTP1_RR) * (double)g_workingTP1_Pct / 100.0; // * FIX#310a: g_workingTP1_Pct + Print("[WARN] BREAKEVEN HIT | ID=", g_multiTPEntries[index].id, + " | Protected capital | Net: +", DoubleToString(netProfitR, 2), "R"); + } + else + { + lossR = 1.0; + g_multiTPStats.totalLoss += lossR; + g_multiTPStats.slHitCount++; + Print("[X] STOP LOSS HIT | ID=", g_multiTPEntries[index].id, + " | Loss: -", DoubleToString(lossR, 2), "R"); + if(InpEnableTPAlerts) + { + Alert("[X] STOP LOSS HIT! ", _Symbol, " | -1R"); + } + } + UpdateMultiTPVisual(g_multiTPEntries[index]); +} +//+------------------------------------------------------------------+ +//| 12. Draw Multi-TP Entry on Chart | +//+------------------------------------------------------------------+ +void DrawMultiTPEntry(MultiTPEntry &entry) +{ + datetime currentTime = TimeCurrent(); + datetime futureTime = currentTime + PeriodSeconds(_Period) * 50; + // Entry Line + string entryName = entry.objName + "_Entry"; + ObjectDelete(0, entryName); + ObjectCreate(0, entryName, OBJ_TREND, 0, entry.entryTime, entry.entryPrice, + futureTime, entry.entryPrice); + ObjectSetInteger(0, entryName, OBJPROP_COLOR, clrYellow); + ObjectSetInteger(0, entryName, OBJPROP_WIDTH, 2); + ObjectSetInteger(0, entryName, OBJPROP_STYLE, STYLE_SOLID); + ObjectSetInteger(0, entryName, OBJPROP_RAY_RIGHT, true); + ObjectSetInteger(0, entryName, OBJPROP_BACK, true); + // SL Line + string slName = entry.objName + "_SL"; + ObjectDelete(0, slName); + ObjectCreate(0, slName, OBJ_TREND, 0, entry.entryTime, entry.stopLoss, + futureTime, entry.stopLoss); + ObjectSetInteger(0, slName, OBJPROP_COLOR, clrRed); + ObjectSetInteger(0, slName, OBJPROP_WIDTH, 2); + ObjectSetInteger(0, slName, OBJPROP_STYLE, STYLE_DOT); + ObjectSetInteger(0, slName, OBJPROP_RAY_RIGHT, true); + ObjectSetInteger(0, slName, OBJPROP_BACK, true); + // TP1 Line + string tp1Name = entry.objName + "_TP1"; + ObjectDelete(0, tp1Name); + ObjectCreate(0, tp1Name, OBJ_TREND, 0, entry.entryTime, entry.tp1Price, + futureTime, entry.tp1Price); + ObjectSetInteger(0, tp1Name, OBJPROP_COLOR, InpTP1Color); + ObjectSetInteger(0, tp1Name, OBJPROP_WIDTH, InpTPLineWidth); + ObjectSetInteger(0, tp1Name, OBJPROP_STYLE, InpTPLineStyle); + ObjectSetInteger(0, tp1Name, OBJPROP_RAY_RIGHT, true); + ObjectSetInteger(0, tp1Name, OBJPROP_BACK, true); + // TP2 Line + string tp2Name = entry.objName + "_TP2"; + ObjectDelete(0, tp2Name); + ObjectCreate(0, tp2Name, OBJ_TREND, 0, entry.entryTime, entry.tp2Price, + futureTime, entry.tp2Price); + ObjectSetInteger(0, tp2Name, OBJPROP_COLOR, InpTP2Color); + ObjectSetInteger(0, tp2Name, OBJPROP_WIDTH, InpTPLineWidth); + ObjectSetInteger(0, tp2Name, OBJPROP_STYLE, InpTPLineStyle); + ObjectSetInteger(0, tp2Name, OBJPROP_RAY_RIGHT, true); + ObjectSetInteger(0, tp2Name, OBJPROP_BACK, true); + // TP3 Line + string tp3Name = entry.objName + "_TP3"; + ObjectDelete(0, tp3Name); + ObjectCreate(0, tp3Name, OBJ_TREND, 0, entry.entryTime, entry.tp3Price, + futureTime, entry.tp3Price); + ObjectSetInteger(0, tp3Name, OBJPROP_COLOR, InpTP3Color); + ObjectSetInteger(0, tp3Name, OBJPROP_WIDTH, InpTPLineWidth); + ObjectSetInteger(0, tp3Name, OBJPROP_STYLE, InpTPLineStyle); + ObjectSetInteger(0, tp3Name, OBJPROP_RAY_RIGHT, true); + ObjectSetInteger(0, tp3Name, OBJPROP_BACK, true); + if(InpShowTPLabels) + { + DrawTPLabels(entry); + } +} +//+------------------------------------------------------------------+ +//| 13. Draw TP Labels | +//+------------------------------------------------------------------+ +void DrawTPLabels(MultiTPEntry &entry) +{ + datetime labelTime = entry.entryTime + PeriodSeconds(_Period) * 5; + // Entry Label + string entryLabelName = entry.objName + "_Entry_Label"; + ObjectDelete(0, entryLabelName); + ObjectCreate(0, entryLabelName, OBJ_TEXT, 0, labelTime, entry.entryPrice); + string entryText = (entry.direction == 1) ? "[^] LONG" : "[v] SHORT"; + ObjectSetString(0, entryLabelName, OBJPROP_TEXT, entryText); + ObjectSetInteger(0, entryLabelName, OBJPROP_COLOR, clrYellow); + ObjectSetInteger(0, entryLabelName, OBJPROP_FONTSIZE, 9); + ObjectSetString(0, entryLabelName, OBJPROP_FONT, "Arial Bold"); + // SL Label + string slLabelName = entry.objName + "_SL_Label"; + ObjectDelete(0, slLabelName); + ObjectCreate(0, slLabelName, OBJ_TEXT, 0, labelTime, entry.stopLoss); + ObjectSetString(0, slLabelName, OBJPROP_TEXT, "SL -1R"); + ObjectSetInteger(0, slLabelName, OBJPROP_COLOR, clrRed); + ObjectSetInteger(0, slLabelName, OBJPROP_FONTSIZE, 8); + ObjectSetString(0, slLabelName, OBJPROP_FONT, "Arial"); + // TP1 Label + string tp1LabelName = entry.objName + "_TP1_Label"; + ObjectDelete(0, tp1LabelName); + ObjectCreate(0, tp1LabelName, OBJ_TEXT, 0, labelTime, entry.tp1Price); + string tp1Text = "TP1 +" + DoubleToString(InpTP1_RR, 1) + "R"; + if(InpShowTPPercent) tp1Text += " (" + IntegerToString(InpTP1_Percent) + "%)"; + ObjectSetString(0, tp1LabelName, OBJPROP_TEXT, tp1Text); + ObjectSetInteger(0, tp1LabelName, OBJPROP_COLOR, InpTP1Color); + ObjectSetInteger(0, tp1LabelName, OBJPROP_FONTSIZE, 9); + ObjectSetString(0, tp1LabelName, OBJPROP_FONT, "Arial Bold"); + // TP2 Label + string tp2LabelName = entry.objName + "_TP2_Label"; + ObjectDelete(0, tp2LabelName); + ObjectCreate(0, tp2LabelName, OBJ_TEXT, 0, labelTime, entry.tp2Price); + string tp2Text = "TP2 +" + DoubleToString(InpTP2_RR, 1) + "R"; + if(InpShowTPPercent) tp2Text += " (" + IntegerToString(InpTP2_Percent) + "%)"; + ObjectSetString(0, tp2LabelName, OBJPROP_TEXT, tp2Text); + ObjectSetInteger(0, tp2LabelName, OBJPROP_COLOR, InpTP2Color); + ObjectSetInteger(0, tp2LabelName, OBJPROP_FONTSIZE, 9); + ObjectSetString(0, tp2LabelName, OBJPROP_FONT, "Arial Bold"); + // TP3 Label + string tp3LabelName = entry.objName + "_TP3_Label"; + ObjectDelete(0, tp3LabelName); + ObjectCreate(0, tp3LabelName, OBJ_TEXT, 0, labelTime, entry.tp3Price); + string tp3Text = "TP3 +" + DoubleToString(InpTP3_RR, 1) + "R"; + if(InpShowTPPercent) tp3Text += " (" + IntegerToString(InpTP3_Percent) + "%)"; + ObjectSetString(0, tp3LabelName, OBJPROP_TEXT, tp3Text); + ObjectSetInteger(0, tp3LabelName, OBJPROP_COLOR, InpTP3Color); + ObjectSetInteger(0, tp3LabelName, OBJPROP_FONTSIZE, 9); + ObjectSetString(0, tp3LabelName, OBJPROP_FONT, "Arial Bold"); +} +//+------------------------------------------------------------------+ +//| 14. Update Multi-TP Visual | +//+------------------------------------------------------------------+ +void UpdateMultiTPVisual(MultiTPEntry &entry) +{ + // Update TP1 color if hit + if(entry.tp1Hit) + { + string tp1Name = entry.objName + "_TP1"; + string tp1LabelName = entry.objName + "_TP1_Label"; + ObjectSetInteger(0, tp1Name, OBJPROP_COLOR, clrGray); + ObjectSetInteger(0, tp1Name, OBJPROP_STYLE, STYLE_DOT); + ObjectSetInteger(0, tp1LabelName, OBJPROP_COLOR, clrGray); + string newText = "[OK] TP1 +" + DoubleToString(InpTP1_RR, 1) + "R"; + ObjectSetString(0, tp1LabelName, OBJPROP_TEXT, newText); + } + // Update TP2 color if hit + if(entry.tp2Hit) + { + string tp2Name = entry.objName + "_TP2"; + string tp2LabelName = entry.objName + "_TP2_Label"; + ObjectSetInteger(0, tp2Name, OBJPROP_COLOR, clrGray); + ObjectSetInteger(0, tp2Name, OBJPROP_STYLE, STYLE_DOT); + ObjectSetInteger(0, tp2LabelName, OBJPROP_COLOR, clrGray); + string newText = "[OK] TP2 +" + DoubleToString(InpTP2_RR, 1) + "R"; + ObjectSetString(0, tp2LabelName, OBJPROP_TEXT, newText); + } + // Update TP3 color if hit + if(entry.tp3Hit) + { + string tp3Name = entry.objName + "_TP3"; + string tp3LabelName = entry.objName + "_TP3_Label"; + ObjectSetInteger(0, tp3Name, OBJPROP_COLOR, clrGray); + ObjectSetInteger(0, tp3Name, OBJPROP_STYLE, STYLE_DOT); + ObjectSetInteger(0, tp3LabelName, OBJPROP_COLOR, clrGray); + string newText = "[OK] TP3 +" + DoubleToString(InpTP3_RR, 1) + "R"; + ObjectSetString(0, tp3LabelName, OBJPROP_TEXT, newText); + } + // Update SL if moved to BE + if(entry.slMovedToBE) + { + string slName = entry.objName + "_SL"; + string slLabelName = entry.objName + "_SL_Label"; + datetime currentTime = TimeCurrent(); + datetime futureTime = currentTime + PeriodSeconds(_Period) * 50; + ObjectMove(0, slName, 0, entry.entryTime, entry.currentSL); + ObjectMove(0, slName, 1, futureTime, entry.currentSL); + ObjectSetInteger(0, slName, OBJPROP_COLOR, clrDodgerBlue); + ObjectSetInteger(0, slName, OBJPROP_WIDTH, 2); + ObjectSetInteger(0, slName, OBJPROP_STYLE, STYLE_SOLID); + ObjectMove(0, slLabelName, 0, entry.entryTime + PeriodSeconds(_Period) * 5, entry.currentSL); + ObjectSetString(0, slLabelName, OBJPROP_TEXT, "BE (Protected)"); + ObjectSetInteger(0, slLabelName, OBJPROP_COLOR, clrDodgerBlue); + } + // If trade is closed (inactive), dim all lines + if(!entry.active) + { + string entryName = entry.objName + "_Entry"; + ObjectSetInteger(0, entryName, OBJPROP_COLOR, clrDarkGray); + ObjectSetInteger(0, entryName, OBJPROP_STYLE, STYLE_DOT); + } +} +//+------------------------------------------------------------------+ +//| 15. Get Active Multi-TP Count | +//+------------------------------------------------------------------+ +int GetActiveMultiTPCount() +{ + int count = 0; + for(int i = 0; i < ArraySize(g_multiTPEntries); i++) + { + if(g_multiTPEntries[i].active) count++; + } + return count; +} +//+------------------------------------------------------------------+ +//| 16. Delete Multi-TP Objects | +//+------------------------------------------------------------------+ +void DeleteMultiTPObjects(MultiTPEntry &entry) +{ + CleanObject(entry.objName + "_Entry"); + CleanObject(entry.objName + "_Entry_Label"); + CleanObject(entry.objName + "_SL"); + CleanObject(entry.objName + "_SL_Label"); + CleanObject(entry.objName + "_TP1"); + CleanObject(entry.objName + "_TP1_Label"); + CleanObject(entry.objName + "_TP2"); + CleanObject(entry.objName + "_TP2_Label"); + CleanObject(entry.objName + "_TP3"); + CleanObject(entry.objName + "_TP3_Label"); +} +//+------------------------------------------------------------------+ +//| 17. Update Multi-TP Levels (called from OnCalculate) | +//+------------------------------------------------------------------+ +void UpdateMultiTPLevels(const double &high[], const double &low[], + const double &close[], const datetime &time[]) +{ + if(!InpEnableMultiTP) return; + double currentBid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + double currentAsk = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + for(int i = ArraySize(g_multiTPEntries) - 1; i >= 0; i--) + { + if(!g_multiTPEntries[i].active) continue; + double price = (g_multiTPEntries[i].direction == 1) ? currentBid : currentAsk; + // Check TP1 + if(!g_multiTPEntries[i].tp1Hit && + IsTPHit(price, g_multiTPEntries[i].tp1Price, g_multiTPEntries[i].direction)) + { + OnTP1Hit(i); + } + // * FIX#445: HistoryDeals TP1 fallback — D1 fix. + // PROBLEM: On D1 (and in fast backtest mode), MT5 server closes the _TP1 position + // at the TP price before UpdateMultiTPLevels runs on the next bar. When EA checks + // positions, _TP1 is already gone → CloseTranchePosition returns "not found" → tp1Hit + // never set → runner (TP2/TP3) never activates. + // Evidence: D1 Jan 19 SELL tp1_price=1.07350, TP hit during bar, next bar tp1Hit=false. + // FIX: If tp1Hit=false but _TP1 position no longer open AND HistoryDeals confirms + // a DEAL_ENTRY_OUT at or beyond tp1Price with our magic → treat as TP1 hit. + if(!g_multiTPEntries[i].tp1Hit) + { + // Check if _TP1 position still exists + bool tp1PosOpen = false; + for(int _p = PositionsTotal() - 1; _p >= 0; _p--) + { + if(!g_ea_position.SelectByIndex(_p)) continue; + if(g_ea_position.Symbol() != _Symbol) continue; + if(g_ea_position.Magic() != EA_MagicNumber) continue; + if(MathAbs(g_ea_position.PriceOpen() - g_multiTPEntries[i].entryPrice) > g_pipValue * 2) continue; + if(StringFind(g_ea_position.Comment(), "_TP1") >= 0) { tp1PosOpen = true; break; } + } + if(!tp1PosOpen) + { + // _TP1 not open — check history for a TP close at/beyond tp1Price + datetime _lookFrom = g_multiTPEntries[i].entryTime > 0 + ? g_multiTPEntries[i].entryTime + : TimeCurrent() - 7 * 86400; + if(HistorySelect(_lookFrom, TimeCurrent())) + { + int _hTotal = HistoryDealsTotal(); + for(int _hd = _hTotal - 1; _hd >= 0; _hd--) + { + ulong _ht = HistoryDealGetTicket(_hd); + if(HistoryDealGetInteger(_ht, DEAL_MAGIC) != EA_MagicNumber) continue; + if(HistoryDealGetString (_ht, DEAL_SYMBOL) != _Symbol) continue; + if(HistoryDealGetInteger(_ht, DEAL_ENTRY) != DEAL_ENTRY_OUT) continue; + // Comment must contain _TP1 + string _dc = HistoryDealGetString(_ht, DEAL_COMMENT); + if(StringFind(_dc, "_TP1") < 0) continue; + // Price must be at or beyond TP1 + double _dealPrice = HistoryDealGetDouble(_ht, DEAL_PRICE); + bool _atTP = IsTPHit(_dealPrice, g_multiTPEntries[i].tp1Price, + g_multiTPEntries[i].direction); + if(_atTP) + { + if(g_verboseLog) + PrintFormat("[FIX#445] TP1 confirmed via HistoryDeals: deal=%.5f tp1=%.5f → OnTP1Hit ID=%d", + _dealPrice, g_multiTPEntries[i].tp1Price, g_multiTPEntries[i].id); + OnTP1Hit(i); + break; + } + } + } + } + } + // TP2 hit: close _TP2 tranche and lock _TP3 SL at TP2-0.4ATR + if(g_multiTPEntries[i].tp1Hit && !g_multiTPEntries[i].tp2Hit && + IsTPHit(price, g_multiTPEntries[i].tp2Price, g_multiTPEntries[i].direction)) + { + OnTP2Hit(i); + } + // TP3: mark passed — trail SL + DOL runner handle the actual close + if(g_multiTPEntries[i].tp2Hit && !g_multiTPEntries[i].tp3Hit && + IsTPHit(price, g_multiTPEntries[i].tp3Price, g_multiTPEntries[i].direction)) + { + g_multiTPEntries[i].tp3Hit = true; + g_multiTPEntries[i].tp3HitTime = TimeCurrent(); + g_multiTPStats.tp3HitCount++; + PrintFormat("[MultiTP] TP3 level reached | ID=%d | Price=%.5f | Trail SL continues", + g_multiTPEntries[i].id, g_multiTPEntries[i].tp3Price); + } + // Check SL + if(IsSLHit(price, g_multiTPEntries[i].currentSL, g_multiTPEntries[i].direction)) + { + OnSLHit(i); + } + } +} +//+------------------------------------------------------------------+ +//| 18. Draw Multi-TP Levels (called on new bar) | +//+------------------------------------------------------------------+ +void DrawMultiTPLevels(const datetime &time[]) +{ + if(!InpEnableMultiTP || !InpShowTPLabels) return; + for(int i = 0; i < ArraySize(g_multiTPEntries); i++) + { + if(g_multiTPEntries[i].active) + { + DrawMultiTPEntry(g_multiTPEntries[i]); + } + } +} +//+------------------------------------------------------------------+ +//| 19. Manage Multi-TP Positions | +//+------------------------------------------------------------------+ +void ManageMultiTPPositions(const double &high[], const double &low[], + const double &close[], const datetime &time[]) +{ + if(!InpEnableMultiTP) return; + SyncMultiTPWithPositions(); +} +//+------------------------------------------------------------------+ +//| 20. Cleanup Completed Multi-TP | +//+------------------------------------------------------------------+ +void CleanupCompletedMultiTP() +{ + // * FIX#17b: Immediate cleanup of inactive entries (was: wait 24 hours!) + // Old bug: inactive entries lingered for 24h -> filled 10-entry limit -> blocked valid signals + datetime cutoffTime = TimeCurrent() - 300; // Keep inactive entries for only 5 minutes (chart visual) + for(int i = ArraySize(g_multiTPEntries) - 1; i >= 0; i--) + { + if(!g_multiTPEntries[i].active && + g_multiTPEntries[i].entryTime < cutoffTime) + { + DeleteMultiTPObjects(g_multiTPEntries[i]); + for(int j = i; j < ArraySize(g_multiTPEntries) - 1; j++) + { + g_multiTPEntries[j] = g_multiTPEntries[j + 1]; + } + ArrayResize(g_multiTPEntries, ArraySize(g_multiTPEntries) - 1); + } + } +} +//+------------------------------------------------------------------+ +//| * FIX#17b: Sync MultiTP tracker with real MT5 positions | +//| PROBLEM: Tracker had entries with active=true but position closed | +//| by MT5 (SL order, broker, manual close) -> stale entries | +//| filled 10-limit -> valid signals blocked | +//+------------------------------------------------------------------+ +void SyncMultiTPWithPositions() +{ + for(int i = ArraySize(g_multiTPEntries) - 1; i >= 0; i--) + { + if(!g_multiTPEntries[i].active) continue; + bool positionExists = false; + // Check by stored ticket + if(g_multiTPEntries[i].ticket > 0) + { + if(PositionSelectByTicket(g_multiTPEntries[i].ticket)) + positionExists = true; + } + // Fallback: scan by entry price + direction (covers ticket mismatch after broker events) + if(!positionExists) + { + double entryTol = (g_cachedATR > 0) ? g_cachedATR * 0.10 : g_pipValue * 5; + for(int p = PositionsTotal() - 1; p >= 0; p--) + { + ulong pTicket = PositionGetTicket(p); + if(pTicket == 0) continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; + if(PositionGetInteger(POSITION_MAGIC) != EA_MagicNumber) continue; + bool sameDir = (g_multiTPEntries[i].direction == 1 && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) || + (g_multiTPEntries[i].direction == -1 && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL); + if(sameDir && MathAbs(PositionGetDouble(POSITION_PRICE_OPEN) - g_multiTPEntries[i].entryPrice) < entryTol) + { + positionExists = true; + g_multiTPEntries[i].ticket = (long)pTicket; + break; + } + } + } + // Deactivate only when MT5 confirms position no longer exists (no time-based expiry) + if(!positionExists) + { + if(g_verboseLog) + PrintFormat("[MultiTP] ID=%d deactivated — position gone (ticket=%d, age=%dmin)", + g_multiTPEntries[i].id, g_multiTPEntries[i].ticket, + (int)((TimeCurrent() - g_multiTPEntries[i].entryTime) / 60)); + if(!g_multiTPEntries[i].tp1Hit && !g_multiTPEntries[i].slMovedToBE) + { + g_multiTPStats.totalLoss += 1.0; + g_multiTPStats.slHitCount++; + // Cascade close TP2/TP3 legs when TP1 SL fires without tp1Hit. + // ROOT (Jan-Feb 2026 H1 backtest): TP1 leg hit SL (-$118) but TP2/TP3 + // remained open with original SL → hit SL again (-$474 each). + // Total loss 4× what it should be. TP2/TP3 are separate MT5 positions + // not linked by ticket in multiTPEntries — find them by entry price + direction. + // Only cascade when TP1 is genuinely lost (not tp1Hit, not BE). + { + double _entryTolC = (g_cachedATR > 0) ? g_cachedATR * 0.15 : g_pipValue * 8; + CTrade _cascTrade; + _cascTrade.SetExpertMagicNumber(EA_MagicNumber); + int _cascClosed = 0; + for(int _cp = PositionsTotal() - 1; _cp >= 0; _cp--) + { + ulong _cpTkt = PositionGetTicket(_cp); + if(_cpTkt == 0) continue; + if(!PositionSelectByTicket(_cpTkt)) continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; + if((long)PositionGetInteger(POSITION_MAGIC) != EA_MagicNumber) continue; + bool _sameDir = (g_multiTPEntries[i].direction == 1 && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) || + (g_multiTPEntries[i].direction == -1 && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL); + if(!_sameDir) continue; + if(MathAbs(PositionGetDouble(POSITION_PRICE_OPEN) - g_multiTPEntries[i].entryPrice) > _entryTolC) continue; + string _cmt = PositionGetString(POSITION_COMMENT); + bool _isRunner = (StringFind(_cmt, "_TP2") >= 0 || StringFind(_cmt, "_TP3") >= 0); + if(!_isRunner) continue; + if(_cascTrade.PositionClose(_cpTkt)) + { + _cascClosed++; + PrintFormat("[CASCADE] TP1 SL → closed runner %s | ID=%d | ticket=%llu", + _cmt, g_multiTPEntries[i].id, _cpTkt); + } + } + if(_cascClosed > 0) + PrintFormat("[CASCADE] Closed %d runner(s) after TP1 SL | ID=%d | entry=%.5f", + _cascClosed, g_multiTPEntries[i].id, g_multiTPEntries[i].entryPrice); + } + } + else if(g_multiTPEntries[i].slMovedToBE) + g_multiTPStats.beHitCount++; + g_multiTPEntries[i].active = false; + } + } +} +//+------------------------------------------------------------------+ +//| 21. Calculate Multi-TP Statistics | +//+------------------------------------------------------------------+ +void CalculateMultiTPStats() +{ + if(g_multiTPStats.totalEntries == 0) return; + // * v9.25 FIX#90: Win rate now based on position group net outcome, not just TP1 count. + // Old: totalWins = tp1HitCount -> always counted partial hits as wins even if TP2/TP3 runner lost more. + // Fix: a "group win" = totalProfitTP1+TP2+TP3 for that group > 0. Since we track totals not per-group, + // approximate: group wins = tp1HitCount (TP1 hit = partial profit secured) minus groups where + // slHitCount fired AFTER tp1 with net<0. For now use tp1HitCount as floor (correct for dashboard). + // The key fix is PF denominator: use only actual SL-hit losses, NOT runner losses counted twice. + int totalWins = g_multiTPStats.tp1HitCount; // Groups where at least partial profit locked + g_multiTPStats.winRate = (g_multiTPStats.totalEntries > 0) ? + ((double)totalWins / g_multiTPStats.totalEntries * 100.0) : 0; + if(g_multiTPStats.tp1HitCount > 0) + g_multiTPStats.avgTP1RR = g_multiTPStats.totalProfitTP1 / g_multiTPStats.tp1HitCount; + if(g_multiTPStats.tp2HitCount > 0) + g_multiTPStats.avgTP2RR = g_multiTPStats.totalProfitTP2 / g_multiTPStats.tp2HitCount; + if(g_multiTPStats.tp3HitCount > 0) + g_multiTPStats.avgTP3RR = g_multiTPStats.totalProfitTP3 / g_multiTPStats.tp3HitCount; + double totalProfit = g_multiTPStats.totalProfitTP1 + + g_multiTPStats.totalProfitTP2 + + g_multiTPStats.totalProfitTP3; + if(totalWins > 0) + g_multiTPStats.avgWinRR = totalProfit / totalWins; + if(g_multiTPStats.totalLoss > 0) + g_multiTPStats.profitFactor = totalProfit / g_multiTPStats.totalLoss; + else if(totalProfit > 0) + g_multiTPStats.profitFactor = 999.99; + g_multiTPStats.lastUpdate = TimeCurrent(); +} +//+------------------------------------------------------------------+ +//| 22. Get Multi-TP Stats Text for Dashboard | +//+------------------------------------------------------------------+ +string GetMultiTPStatsText() +{ + CalculateMultiTPStats(); + double totalProfit = g_multiTPStats.totalProfitTP1 + + g_multiTPStats.totalProfitTP2 + + g_multiTPStats.totalProfitTP3; + double netProfit = totalProfit - g_multiTPStats.totalLoss; + string text = ""; + text += "+===========================================+\n"; + text += "| MULTI-TP STATISTICS |\n"; + text += "+===========================================+\n"; + text += StringFormat("| Entries: %-3d Active: %-3d |\n", + g_multiTPStats.totalEntries, GetActiveMultiTPCount()); + text += "+===========================================+\n"; + text += StringFormat("| TP1: %-3d hits (+%.1fR total) |\n", + g_multiTPStats.tp1HitCount, g_multiTPStats.totalProfitTP1); + text += StringFormat("| TP2: %-3d hits (+%.1fR total) |\n", + g_multiTPStats.tp2HitCount, g_multiTPStats.totalProfitTP2); + text += StringFormat("| TP3: %-3d hits (+%.1fR total) |\n", + g_multiTPStats.tp3HitCount, g_multiTPStats.totalProfitTP3); + text += StringFormat("| SL: %-3d hits BE: %-3d hits |\n", + g_multiTPStats.slHitCount, g_multiTPStats.beHitCount); + text += "+===========================================+\n"; + text += StringFormat("| Win Rate: %.1f%% |\n", + g_multiTPStats.winRate); + text += StringFormat("| PF(R-tranche): %.2f |\n", // * v9.25 FIX#90: renamed to avoid confusion with money-based PF in backtest report + g_multiTPStats.profitFactor); + text += StringFormat("| Net Profit: %s%.1fR |\n", + (netProfit >= 0 ? "+" : ""), netProfit); + text += "+===========================================+\n"; + return text; +} +//+------------------------------------------------------------------+ +//| 23. Deinitialize Multi-TP Module | +//+------------------------------------------------------------------+ +void DeinitializeMultiTP() +{ + if(g_multiTPStats.totalEntries > 0) + { + Print("==========================================================="); + Print("[CHART] MULTI-TP FINAL STATISTICS:"); + Print(GetMultiTPStatsText()); + Print("==========================================================="); + } + for(int i = 0; i < ArraySize(g_multiTPEntries); i++) + { + DeleteMultiTPObjects(g_multiTPEntries[i]); + } + ObjectsDeleteAll(0, "MTP_"); + ArrayResize(g_multiTPEntries, 0); + ZeroMemory(g_multiTPStats); + g_multiTPCounter = 0; + Print("[OK] Multi-TP Module Deinitialized"); +} +//+------------------------------------------------------------------+ +//| 24. Initialize Multi-TP Module | +//+------------------------------------------------------------------+ +void InitializeMultiTP() +{ + ArrayResize(g_multiTPEntries, 0); + ZeroMemory(g_multiTPStats); + g_multiTPCounter = 0; + Print("[OK] Multi-TP Module Initialized"); +} +//+------------------------------------------------------------------+ +//| 1. Initialize Entry Scoring | +//+------------------------------------------------------------------+ +void InitializeEntryScoring() +{ + ZeroMemory(g_lastEntryScore); + ZeroMemory(g_lastConfluence); + ZeroMemory(g_lastCandlePattern); + g_scoreObjCount = 0; + Print("[OK] Entry Scoring System Initialized"); +} +//+------------------------------------------------------------------+ +//| 2. Detect Candle Patterns | +//+------------------------------------------------------------------+ +CandlePatternStruct DetectCandlePattern(const double &open[], const double &high[], + const double &low[], const double &close[], + int index) +{ + CandlePatternStruct pattern; + ZeroMemory(pattern); + if(index < 1) return pattern; + double bodySize0 = MathAbs(close[0] - open[0]); + double bodySize1 = MathAbs(close[1] - open[1]); + double range0 = high[0] - low[0]; + double range1 = high[1] - low[1]; + if(range0 <= 0 || range1 <= 0) return pattern; + double upperWick0 = high[0] - MathMax(open[0], close[0]); + double lowerWick0 = MathMin(open[0], close[0]) - low[0]; + bool isBullish0 = close[0] > open[0]; + bool isBullish1 = close[1] > open[1]; + //=== ENGULFING PATTERN === + // Bullish Engulfing + if(isBullish0 && !isBullish1 && + close[0] > open[1] && open[0] < close[1] && + bodySize0 > bodySize1) + { + pattern.isBullishEngulfing = true; + pattern.patternStrength += 3; + pattern.patternName = "Bullish Engulfing"; + } + // Bearish Engulfing + if(!isBullish0 && isBullish1 && + close[0] < open[1] && open[0] > close[1] && + bodySize0 > bodySize1) + { + pattern.isBearishEngulfing = true; + pattern.patternStrength += 3; + pattern.patternName = "Bearish Engulfing"; + } + //=== PIN BAR / REJECTION === + double wickBodyRatio = 2.0; + // Bullish Pin Bar (Hammer) + if(lowerWick0 > bodySize0 * wickBodyRatio && + lowerWick0 > upperWick0 * 2 && + bodySize0 > 0) + { + pattern.isBullishPinBar = true; + pattern.isBullishHammer = true; + pattern.patternStrength += 2; + if(pattern.patternName == "") pattern.patternName = "Bullish Pin Bar"; + } + // Bearish Pin Bar (Shooting Star) + if(upperWick0 > bodySize0 * wickBodyRatio && + upperWick0 > lowerWick0 * 2 && + bodySize0 > 0) + { + pattern.isBearishPinBar = true; + pattern.isBearishShootingStar = true; + pattern.patternStrength += 2; + if(pattern.patternName == "") pattern.patternName = "Bearish Pin Bar"; + } + //=== INSIDE BAR BREAKOUT === + if(index >= 2) + { + bool isInsideBar1 = (high[1] < high[2] && low[1] > low[2]); + // Bullish Inside Bar Breakout + if(isInsideBar1 && close[0] > high[1]) + { + pattern.isBullishInsideBreak = true; + pattern.patternStrength += 2; + if(pattern.patternName == "") pattern.patternName = "Bullish Inside Break"; + } + // Bearish Inside Bar Breakout + if(isInsideBar1 && close[0] < low[1]) + { + pattern.isBearishInsideBreak = true; + pattern.patternStrength += 2; + if(pattern.patternName == "") pattern.patternName = "Bearish Inside Break"; + } + } + // Cap strength at 5 + pattern.patternStrength = MathMin(pattern.patternStrength, 5); + return pattern; +} +//+------------------------------------------------------------------+ +//| 3. Calculate Confluence Data | +//+------------------------------------------------------------------+ +ConfluenceScoreStruct CalculateConfluenceData(double price, bool isLong) +{ + ConfluenceScoreStruct conf; + ZeroMemory(conf); + //=== Check Order Blocks === + for(int i = 0; i < ArraySize(OB_Array); i++) + { + if(OB_Array[i].mitigated || OB_Array[i].status == "EXPIRED") continue; + if(price >= OB_Array[i].bottom && price <= OB_Array[i].top) + { + if((isLong && OB_Array[i].isBullish) || (!isLong && !OB_Array[i].isBullish)) + { + conf.hasOB = true; + conf.zoneCount++; + } + } + } + //=== Check FVGs === + for(int i = 0; i < ArraySize(FVG_Array); i++) + { + if(FVG_Array[i].status != FVG_STATUS_ACTIVE) continue; + if(price >= FVG_Array[i].bottom && price <= FVG_Array[i].top) + { + if((isLong && FVG_Array[i].isBullish) || (!isLong && !FVG_Array[i].isBullish)) + { + conf.hasFVG = true; + conf.zoneCount++; + } + } + } + //=== Check OTE Zones === + for(int i = 0; i < ArraySize(OTE_Array); i++) + { + if(!OTE_Array[i].isValid || !OTE_Array[i].active) continue; + double oteTop = MathMax(OTE_Array[i].level618, OTE_Array[i].level786); + double oteBottom = MathMin(OTE_Array[i].level618, OTE_Array[i].level786); + if(price >= oteBottom && price <= oteTop) + { + if((isLong && OTE_Array[i].isBullish) || (!isLong && !OTE_Array[i].isBullish)) + { + conf.hasOTE = true; + conf.zoneCount++; + } + } + } + //=== Check Breaker Blocks === + for(int i = 0; i < ArraySize(OB_Array); i++) + { + if(!OB_Array[i].isBreakerBlock) continue; + if(OB_Array[i].mitigated || OB_Array[i].status == "EXPIRED") continue; + if(price >= OB_Array[i].bottom && price <= OB_Array[i].top) + { + if((isLong && OB_Array[i].isBullish) || (!isLong && !OB_Array[i].isBullish)) + { + conf.hasBB = true; + conf.zoneCount++; + } + } + } + //=== Check CRT Setups === + for(int i = 0; i < ArraySize(g_crtSetups); i++) + { + if(!g_crtSetups[i].active) continue; + if(g_crtSetups[i].breakConfirmed) + { + conf.hasCRT = true; + conf.zoneCount++; + break; + } + } + //=== Check TBS Setups === + for(int i = 0; i < ArraySize(g_tbsSetups); i++) + { + if(!g_tbsSetups[i].active) continue; + conf.hasTBS = true; + conf.zoneCount++; + break; + } + //=== Check Silver Bullet === +for(int i = 0; i < ArraySize(g_sbSetups); i++) +{ + if(!g_sbSetups[i].active) continue; + conf.hasSB = true; + conf.zoneCount++; + break; +} + //=== Check Liquidity Levels === + for(int i = 0; i < ArraySize(LIQ_Array); i++) + { + if(!LIQ_Array[i].isValid || LIQ_Array[i].swept) continue; + double dist = MathAbs(price - LIQ_Array[i].price); + if(dist < g_cachedATR * 0.5) + { + conf.hasLiquidity = true; + conf.zoneCount++; + break; + } + } + //=== Check Volume (VSA) === +if(VSA_Enabled) +{ + for(int i = ArraySize(g_vsaPatterns) - 1; i >= 0; i--) + { + if(g_vsaPatterns[i].volumeRatio > 1.5) + { + conf.hasVolume = true; + conf.zoneCount++; + break; + } + } +} + //=== Check Divergences === +if(Divergence_Enabled && Divergence_UseForConfirm) // * v9.16 FIX#48: gate with UseForConfirm (was dead input) +{ + for(int i = 0; i < ArraySize(g_divergences); i++) + { + if(!g_divergences[i].active) continue; + // type: 1 = bullish, -1 = bearish + if((isLong && g_divergences[i].type == 1) || (!isLong && g_divergences[i].type == -1)) + { + conf.hasDivergence = true; + conf.zoneCount++; + break; + } + } +} + //=== Check Trendlines === +if(Trendline_Enabled && Trendline_UseForConfirm) // * v9.16 FIX#48: gate with UseForConfirm (was dead input) +{ + for(int i = 0; i < ArraySize(g_trendlines); i++) + { + if(!g_trendlines[i].active) continue; + // type: 1 = bullish, -1 = bearish + if((isLong && g_trendlines[i].type == 1) || (!isLong && g_trendlines[i].type == -1)) + { + conf.hasTrendline = true; + conf.zoneCount++; + break; + } + } +} + //=== Calculate Confluence Score === + conf.score = CalculateConfluenceScore(conf); + return conf; +} +//+------------------------------------------------------------------+ +//| 4. Calculate Confluence Score | +//+------------------------------------------------------------------+ +double CalculateConfluenceScore(const ConfluenceScoreStruct &conf) +{ + double score = 0; + //=== BASE SCORES FOR EACH ZONE TYPE === + if(conf.hasOB) score += 20; // Order Block - highest priority + if(conf.hasFVG) score += 15; // Fair Value Gap + if(conf.hasOTE) score += 20; // OTE Zone - highest priority + if(conf.hasBB) score += 15; // Breaker Block + if(conf.hasMB) score += 10; // Mitigation Block + if(conf.hasSB) score += 15; // Silver Bullet + if(conf.hasTBS) score += 15; // Turtle Soup + if(conf.hasCRT) score += 15; // CRT + if(conf.hasVolume) score += 12; // Volume confirmation + if(conf.hasDivergence) score += 18; // Divergence - high value + if(conf.hasTrendline) score += 12; // Trendline confirmation + if(conf.hasLiquidity) score += 10; // Near liquidity + //=== BONUS FOR MULTIPLE CONFLUENCES === + if(conf.zoneCount >= 7) score += 30; + else if(conf.zoneCount >= 6) score += 25; + else if(conf.zoneCount >= 5) score += 20; + else if(conf.zoneCount >= 4) score += 15; + else if(conf.zoneCount >= 3) score += 10; + else if(conf.zoneCount >= 2) score += 5; + //=== SPECIAL COMBO BONUSES === + // OB + FVG combo + if(conf.hasOB && conf.hasFVG) + score += 10; + // OB + OTE combo + if(conf.hasOB && conf.hasOTE) + score += 12; + // FVG + OTE combo + if(conf.hasFVG && conf.hasOTE) + score += 10; + // Volume + OB combo (strong institutional interest) + if(conf.hasVolume && conf.hasOB) + score += 8; + // Volume + Divergence combo (reversal confirmation) + if(conf.hasVolume && conf.hasDivergence) + score += 15; + // Trendline + OB combo + if(conf.hasTrendline && conf.hasOB) + score += 10; + // Trendline + Divergence combo (strong reversal) + if(conf.hasTrendline && conf.hasDivergence) + score += 12; + // Silver Bullet + OTE combo (ICT style) + if(conf.hasSB && conf.hasOTE) + score += 10; + // Triple confluence bonus (OB + FVG + OTE) + if(conf.hasOB && conf.hasFVG && conf.hasOTE) + score += 15; + // Volume + Divergence + Trendline (new features combo) + if(conf.hasVolume && conf.hasDivergence && conf.hasTrendline) + score += 20; + // CRT + TBS combo + if(conf.hasCRT && conf.hasTBS) + score += 10; + return MathMin(score, 100); +} +//+------------------------------------------------------------------+ +//| 5. Get Volume Score | +//+------------------------------------------------------------------+ +int GetVolumeScoreForEntry() +{ + if(!VSA_Enabled) + return 0; + int score = 0; + int size = ArraySize(g_vsaPatterns); + if(size == 0) + return 0; + // Check recent VSA patterns + for(int i = size - 1; i >= MathMax(0, size - 5); i--) + { + // High volume adds to score + if(g_vsaPatterns[i].volumeRatio > 1.5) + score += 5; + // Volume climax adds more + if(g_vsaPatterns[i].volumeRatio > 2.0) + score += 5; + // Extra high volume (2.5x average) + if(g_vsaPatterns[i].volumeRatio >= 2.5) + score += 3; + if(score > 0) break; // Found valid pattern + } + return MathMin(score, 15); +} +//+------------------------------------------------------------------+ +//| Get Divergence Score for Entry | +//+------------------------------------------------------------------+ +int GetDivergenceScoreForEntry(int direction) +{ + if(!Divergence_Enabled) + return 0; + int score = 0; + for(int i = g_divergenceCount - 1; i >= 0; i--) + { + if(!g_divergences[i].active) continue; + bool isBullish = (g_divergences[i].type == DIV_REGULAR_BULLISH || + g_divergences[i].type == DIV_HIDDEN_BULLISH); + bool isRegular = (g_divergences[i].type == DIV_REGULAR_BULLISH || + g_divergences[i].type == DIV_REGULAR_BEARISH); + // Direction match + if((direction == 1 && isBullish) || (direction == -1 && !isBullish)) + { + // Regular = more points + // [v6.41] Use Divergence_ScoreBonus input (was hardcoded 15) + int maxBonus = (int)Divergence_ScoreBonus; + if(isRegular) + score = MathMax(score, maxBonus); + else + score = MathMax(score, (int)(maxBonus * 0.67)); + // Strength bonus + if(g_divergences[i].strength == DIV_STRONG) + score = MathMin(score + 3, maxBonus); + // Confirmed bonus + if(g_divergences[i].confirmed) + score = MathMin(score + 2, maxBonus); + } + } + return score; +} +//+------------------------------------------------------------------+ +//| Get Trendline Score for Entry - ENHANCED | +//+------------------------------------------------------------------+ +int GetTrendlineScoreForEntry(int direction) +{ + if(!Trendline_Enabled) + return 0; + int score = 0; + double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); + for(int i = 0; i < g_trendlineCount; i++) + { + if(!g_trendlines[i].active) continue; + bool isSupport = (g_trendlines[i].type == TL_SUPPORT); + double tlPrice = g_trendlines[i].currentPrice; + double distance = MathAbs(currentPrice - tlPrice); + //=== ACTIVE TRENDLINE SUPPORT/RESISTANCE === + if(!g_trendlines[i].broken) + { + // Long entry near support + if(direction == 1 && isSupport && distance < g_cachedATR * 0.5) + { + // [v6.41] Use Trendline_ScoreBonus input (was hardcoded 10) + int tlMax = (int)Trendline_ScoreBonus; + score = MathMax(score, tlMax); + if(g_trendlines[i].strength >= TL_STRONG) score = MathMin(score + 3, tlMax); + if(g_trendlines[i].touches >= 4) score = MathMin(score + 2, tlMax); + } + // Short entry near resistance + if(direction == -1 && !isSupport && distance < g_cachedATR * 0.5) + { + int tlMax2 = (int)Trendline_ScoreBonus; + score = MathMax(score, tlMax2); + if(g_trendlines[i].strength >= TL_STRONG) score = MathMin(score + 3, tlMax2); + if(g_trendlines[i].touches >= 4) score = MathMin(score + 2, tlMax2); + } + } + //=== BROKEN TRENDLINE RETEST === + if(g_trendlines[i].broken && g_trendlines[i].status == TL_STATUS_RETESTING) + { + // Long on resistance-turned-support retest + if(direction == 1 && !isSupport && distance < g_cachedATR * 0.3) + { + score = MathMax(score, 10); + } + // Short on support-turned-resistance retest + if(direction == -1 && isSupport && distance < g_cachedATR * 0.3) + { + score = MathMax(score, 10); + } + } + } + return score; +} +//+------------------------------------------------------------------+ +//| * FIX#369: ComputeUnifiedScore — SINGLE SOURCE OF TRUTH | +//| All 14 scoring modules in one function. All 3 gates read from | +//| this result. Zero score drift between signal generation and | +//| entry execution. | +//| | +//| cascade/cascadeAvailable: pass true + existing cascade when | +//| called from EvaluateSmartEntry (cascade already computed). | +//| Signal checkers pass cascadeAvailable=false; modules 6-13 use | +//| a fast inline approximation (no duplicate RunConfirmationCascade)| +//+------------------------------------------------------------------+ +UnifiedScoreResult ComputeUnifiedScore(bool isBullish, double entryPrice, + double slPrice, double tp1Price, + double tp2Price, double tp3Price, + const ConfluenceScoreStruct &confluence, + const CandlePatternStruct &candle, + int obQuality, + const ConfirmationCascade &cascade, + bool cascadeAvailable) +{ + UnifiedScoreResult u; + ZeroMemory(u); + u.calcTime = TimeCurrent(); + u.isValid = true; + u.cascadeAvailable = cascadeAvailable; + + // ─── MODULE 1: TREND & HTF (0-25) ──────────────────────────── + // Structure baseline + HTF strong/normal/opposed split (FIX#360 calibrated) + if((isBullish && g_isBullishStructure) || (!isBullish && !g_isBullishStructure)) + u.trendScore = 12; + else + u.trendScore = 0; + if(MTF_Enabled) + { + // FIX#505f: use corrected direction for scoring + bool htfStrongAligned = (isBullish && g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH) || + (!isBullish && g_mtfAnalysis.overallDirection == MTF_STRONG_BEARISH); + bool htfAligned = (isBullish && (g_mtfAnalysis.overallDirection == MTF_BULLISH || + g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH)) || + (!isBullish && (g_mtfAnalysis.overallDirection == MTF_BEARISH || + g_mtfAnalysis.overallDirection == MTF_STRONG_BEARISH)); + bool htfStrongOpposed = (isBullish && g_mtfAnalysis.overallDirection == MTF_STRONG_BEARISH) || + (!isBullish && g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH); + bool htfOpposed = (isBullish && (g_mtfAnalysis.overallDirection == MTF_BEARISH || + g_mtfAnalysis.overallDirection == MTF_STRONG_BEARISH)) || + (!isBullish && (g_mtfAnalysis.overallDirection == MTF_BULLISH || + g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH)); + if (htfStrongAligned) u.trendScore = MathMin(u.trendScore + 13, 25); + else if(htfAligned) u.trendScore = MathMin(u.trendScore + 8, 25); + else if(htfStrongOpposed) u.trendScore = MathMax(u.trendScore - 12, 0); + else if(htfOpposed) u.trendScore = MathMax(u.trendScore - 8, 0); + } + + // ─── MODULE 2: TIMING (0-15) ────────────────────────────────── + u.timingScore = g_isInKillzone ? 12 : 2; + // FIX#507: AMD phase score contributions + if(AMD_Enabled && g_amdData.valid) + { + bool _tradeBull = isBullish; + bool _amdBull = (g_amdData.distDirection == CRT_BULLISH); + bool _amdAligned = (_tradeBull == _amdBull); + + if(g_amdData.phase == AMD_MANIPULATION && _amdAligned) + { + // Best ICT setup: trading with the manipulation direction + u.timingScore = MathMin(u.timingScore + 8, 15); + } + else if(g_amdData.phase == AMD_DISTRIBUTION) + { + if(_amdAligned) + { + // Distribution: aligned with move direction + u.timingScore = MathMin(u.timingScore + 3, 15); + // Late distribution penalty: near target + if(g_amdData.distProgress >= 75.0) + u.timingScore = MathMax(u.timingScore - 5, 0); + } + else + { + // Trading against distribution direction — penalize + u.timingScore = MathMax(u.timingScore - 5, 0); + } + } + } + + // ─── MODULE 3: R:R ACHIEVABILITY (0-20) ───────────────────── + // FIX#360: single TF-aware bracket table, authoritative for ALL gates. + // FIX#361: rrScore<10 = hard reject on H4+ (applied in MeetsMinimumEntryScore). + { + double slMult = GetActiveSLMult(); + double tpMult = GetActiveTP1Mult(); + double expRR = (slMult > 0) ? tpMult / slMult : 1.5; + if(_Period >= PERIOD_D1) + { + if (expRR >= 2.0 && expRR <= 4.5) u.rrScore = 20; + else if(expRR > 4.5 && expRR <= 6.0) u.rrScore = 12; + else if(expRR > 1.5 && expRR < 2.0) u.rrScore = 14; + else if(expRR >= 1.0 && expRR <= 1.5) u.rrScore = 8; + else if(expRR < 1.0) u.rrScore = 4; + else u.rrScore = 3; + } + else if(_Period >= PERIOD_H4) + { // FIX#360 H4: data-driven sweet spot 1.6-2.5R (WR=50% bucket) + if (expRR >= 1.6 && expRR <= 2.5) u.rrScore = 20; + else if(expRR > 2.5 && expRR <= 3.5) u.rrScore = 16; + else if(expRR > 1.4 && expRR < 1.6) u.rrScore = 12; + else if(expRR >= 1.0 && expRR <= 1.4) u.rrScore = 6; + else if(expRR < 1.0) u.rrScore = 2; + else u.rrScore = 8; + } + else if(_Period >= PERIOD_H1) + { + if (expRR >= 1.5 && expRR <= 2.5) u.rrScore = 20; + else if(expRR > 2.5 && expRR <= 3.5) u.rrScore = 12; + else if(expRR > 1.2 && expRR < 1.5) u.rrScore = 14; + else if(expRR >= 1.0 && expRR <= 1.2) u.rrScore = 8; + else if(expRR < 1.0) u.rrScore = 4; + else u.rrScore = 3; + } + else + { // M5/M15 + if (expRR >= 1.0 && expRR <= 1.8) u.rrScore = 20; + else if(expRR > 1.8 && expRR <= 2.5) u.rrScore = 14; + else if(expRR > 2.5 && expRR <= 3.0) u.rrScore = 8; + else if(expRR > 3.0 && expRR <= 4.0) u.rrScore = 3; + else if(expRR < 1.0) u.rrScore = 4; + else u.rrScore = 0; + } + // ATR-multiple penalty (TF-aware) + double penHigh, penExtr; + if (_Period >= PERIOD_D1) { penHigh = 6.0; penExtr = 9.0; } + else if(_Period >= PERIOD_H4) { penHigh = 4.5; penExtr = 6.5; } + else if(_Period >= PERIOD_H1) { penHigh = 3.0; penExtr = 4.5; } + else { penHigh = 2.5; penExtr = 3.8; } + if (tpMult > penExtr) u.rrScore = MathMax(0, u.rrScore - 6); + else if(tpMult > penHigh) u.rrScore = MathMax(0, u.rrScore - 3); + } + + // ─── MODULE 4: MOMENTUM (0-10) ──────────────────────────────── + { + double atr5Buf[]; + ArraySetAsSeries(atr5Buf, true); + int atr5Handle = iATR(_Symbol, _Period, 5); + double atr5 = 0; + if(atr5Handle != INVALID_HANDLE) + { + if(CopyBuffer(atr5Handle, 0, 0, 1, atr5Buf) > 0) atr5 = atr5Buf[0]; + IndicatorRelease(atr5Handle); + } + if(atr5 > 0 && g_cachedATR > 0) + { + double atrRatio = atr5 / g_cachedATR; + if (atrRatio > 1.3) u.momentumScore = 10; + else if(atrRatio > 1.1) u.momentumScore = 8; + else if(atrRatio > 0.9) u.momentumScore = 5; + else if(atrRatio > 0.7) u.momentumScore = 2; + else u.momentumScore = 0; + } + else u.momentumScore = 5; + } + + // ─── MODULE 5: ML/NN (0 to +15) ────────────────────────────── + // NN adds a bonus when win probability > 50%. Below 50% contributes 0 (neutral). + // ML never subtracts from the score — structural/timing/RR gates handle quality. + if(g_nnReadyForUse) + u.mlScore = (int)MathMax(0.0, MathMin(15.0, (g_nnTP1WinProb - 0.50) * 50.0)); + + // ─── MODULES 6-13: CASCADE (only if cascade available) ──────── + if(cascadeAvailable) + { + // Module 6: Zone + u.cascadeZoneScore = cascade.confirmations[CONF_PREMIUM_DISC].points; + + // Module 7: Confluence + u.cascadeConfluenceScore = 8; // base: entry technique itself + if(cascade.confirmations[CONF_FVG].confirmed) u.cascadeConfluenceScore += 8; + if(cascade.confirmations[CONF_OB].confirmed) u.cascadeConfluenceScore += 8; + if(cascade.confirmations[CONF_VSA].confirmed) u.cascadeConfluenceScore += 5; + + // Module 8: Sweep (Judas) + u.cascadeSweepScore = g_judasActive ? 20 : 2; + // * FIX-A: LIQ sweep confirmation adds to sweep score (up to +15pts) + if(cascade.confirmations[CONF_LIQ_SWEEP].confirmed) + u.cascadeSweepScore += cascade.confirmations[CONF_LIQ_SWEEP].points; + + // Module 9: Cascade timing (CONF_KILLZONE — canonical killzone result) + u.cascadeTimingScore = cascade.confirmations[CONF_KILLZONE].confirmed ? 15 : 0; + + // Module 10: Pattern + if(cascade.confirmations[CONF_DIVERGENCE].confirmed) u.cascadePatternScore += 5; + if(cascade.confirmations[CONF_TRENDLINE].confirmed) u.cascadePatternScore += 5; + + // Module 11: Technique convergence + int agreeingTechniques = 0; + for(int i = 0; i < ArraySize(FVG_Array); i++) + if(g_fvgs[i].active && g_fvgs[i].isBullish == isBullish && + MathAbs(entryPrice - (g_fvgs[i].top + g_fvgs[i].bottom)/2.0) < g_cachedATR*2.0) + { agreeingTechniques++; break; } + for(int i = 0; i < MathMin(g_obCount, ArraySize(OB_Array)); i++) + if(g_obs[i].active && !g_obs[i].mitigated && g_obs[i].isBullish == isBullish && + entryPrice >= g_obs[i].bottom - g_cachedATR*0.5 && entryPrice <= g_obs[i].top + g_cachedATR*0.5) + { agreeingTechniques++; break; } + for(int i = 0; i < ArraySize(OTE_Array); i++) + if(OTE_Array[i].isValid && OTE_Array[i].active && OTE_Array[i].isBullish == isBullish) + { + double top = MathMax(OTE_Array[i].level618, OTE_Array[i].level786); + double bot = MathMin(OTE_Array[i].level618, OTE_Array[i].level786); + if(entryPrice >= bot && entryPrice <= top) { agreeingTechniques++; break; } + } + for(int i = 0; i < ArraySize(BREAKER_Array); i++) + if(BREAKER_Array[i].active && !BREAKER_Array[i].mitigated && + BREAKER_Array[i].isBullish == isBullish && + entryPrice >= BREAKER_Array[i].bottom && entryPrice <= BREAKER_Array[i].top) + { agreeingTechniques++; break; } + if((isBullish && g_isBullishStructure) || (!isBullish && !g_isBullishStructure)) + agreeingTechniques++; + for(int jd = 0; jd < ArraySize(g_judasSwings); jd++) + if(g_judasSwings[jd].active && (g_judasSwings[jd].type == JUDAS_BULLISH) == isBullish) + { agreeingTechniques++; break; } + for(int i = ArraySize(LIQ_Array)-1; i >= 0; i--) + if(LIQ_Array[i].swept && LIQ_Array[i].isValid && LIQ_Array[i].age <= 10 && + ((isBullish && !LIQ_Array[i].isBSL) || (!isBullish && LIQ_Array[i].isBSL))) + { agreeingTechniques++; break; } + if (agreeingTechniques >= 7) u.techniqueBonus = 15; + else if(agreeingTechniques >= 6) u.techniqueBonus = 10; + else if(agreeingTechniques >= 5) u.techniqueBonus = 5; + else if(agreeingTechniques >= 4) u.techniqueBonus = 0; + else if(agreeingTechniques >= 3) u.techniqueBonus = 0; + else if(agreeingTechniques >= 2) u.techniqueBonus = -3; + else u.techniqueBonus = -5; + + // Module 12: Regime alignment + // * FIX#366 + FIX#369: REGIME_TRENDING gets NO bonus on H4+ (moderate ADX = lagging). + // Data: 5-conf + TRENDING = WR 21%, -$713. Bonus only for confirmed strong/directional trends. + { + bool isH4Plus = (_Period >= PERIOD_H4); + bool regimeStrong = (g_regimeData.regime == REGIME_STRONG_TREND_UP || + g_regimeData.regime == REGIME_STRONG_TREND_DOWN); + bool regimeTrend = (g_regimeData.regime == REGIME_TREND_UP || + g_regimeData.regime == REGIME_TREND_DOWN); + bool regimeWeak = (g_regimeData.regime == REGIME_WEAK_TREND_UP || + g_regimeData.regime == REGIME_WEAK_TREND_DOWN); + // FIX#366: REGIME_TRENDING included only for lower TFs (M5/M15/H1), not H4+ + bool regimeLegacyTrending = (!isH4Plus && g_regimeData.regime == REGIME_TRENDING); + bool regimeTrending = regimeStrong || regimeTrend || regimeWeak || regimeLegacyTrending; + bool alignedWithRegime = (isBullish && g_regimeData.trendDirection > 0) || + (!isBullish && g_regimeData.trendDirection < 0); + if(regimeTrending && alignedWithRegime) + { + if (regimeStrong) u.regimeScore = 15; + else if(regimeTrend) u.regimeScore = 10; + else u.regimeScore = 5; // WEAK_TREND or M5 TRENDING + } + else if(regimeTrending && !alignedWithRegime) + u.regimeScore = -8; + // else: non-trending or unknown = 0 + } + + // Module 13: HTF cascade bonus + if(cascade.confirmations[CONF_HTF_TREND].confirmed) + u.htfBonus = (int)(cascade.confirmations[CONF_HTF_TREND].strength * 10.0); + } + else + { + // No cascade available (pre-cascade gate): use timing module as proxy + u.cascadeTimingScore = u.timingScore; + // Regime score still applies (uses global regime state) + { + bool isH4Plus = (_Period >= PERIOD_H4); + bool regimeStrong = (g_regimeData.regime == REGIME_STRONG_TREND_UP || g_regimeData.regime == REGIME_STRONG_TREND_DOWN); + bool regimeTrend = (g_regimeData.regime == REGIME_TREND_UP || g_regimeData.regime == REGIME_TREND_DOWN); + bool regimeWeak = (g_regimeData.regime == REGIME_WEAK_TREND_UP || g_regimeData.regime == REGIME_WEAK_TREND_DOWN); + bool regimeLegacy = (!isH4Plus && g_regimeData.regime == REGIME_TRENDING); + bool regimeTrending = regimeStrong || regimeTrend || regimeWeak || regimeLegacy; + bool aligned = (isBullish && g_regimeData.trendDirection > 0) || + (!isBullish && g_regimeData.trendDirection < 0); + if (regimeTrending && aligned && regimeStrong) u.regimeScore = 15; + else if(regimeTrending && aligned && regimeTrend) u.regimeScore = 10; + else if(regimeTrending && aligned) u.regimeScore = 5; + else if(regimeTrending && !aligned) u.regimeScore = -8; + } + } + + // ── FIX#501: RANGE PREMIUM/DISCOUNT GATE ───────────────────── + // When ranging is confirmed (g_rangeConfScore >= 2), entries must respect + // price position within the H1/H4 range (rangeHigh/rangeLow from g_regimeData). + // ICT principle: SELL PREMIUM (top of range), BUY DISCOUNT (bottom of range). + // Never sell at the bottom of a range — that's where reversals happen. + // + // Score is now 0-5 (added L1b: macro/D1 ADX): + // Score ≥ 3: RANGE CONFIRMED — hard gate (position < 40% blocks SELL, > 60% blocks BUY) + // Score = 2: RANGE PROBABLE — soft gate (position < 30% blocks SELL, > 70% blocks BUY) + // Score 0-1: TRENDING — no gate (normal operation, full trending logic applies) + // + // TRAP detection: if rangeConfScore ≥ 2 AND price just closed OUTSIDE the range + // (i.e. potential false breakout), the entry in breakout direction is penalised. + if(g_rangeConfScore >= 2 && g_regimeData.rangeHigh > g_regimeData.rangeLow && + g_cachedATR > 0 && g_regimeData.valid) + { + double _currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); + double _rangeH = g_regimeData.rangeHigh; + double _rangeL = g_regimeData.rangeLow; + double _rangePct = (_currentPrice - _rangeL) / (_rangeH - _rangeL); + + // Only apply gate when range width is meaningful (> 1.5x ATR) + // Prevents false triggers on very tight ranges / single candle noise + bool _rangeValid = ((_rangeH - _rangeL) > g_cachedATR * 1.5); + + if(_rangeValid) + { + // ── TRAP DETECTION: false breakout fade ─────────────── + // Candle closed outside range + low vol expansion = fake breakout + // Fade: penalise entry IN breakout direction, reward opposite direction + bool _priceAboveRange = (_currentPrice > _rangeH + g_cachedATR * 0.1); + bool _priceBelowRange = (_currentPrice < _rangeL - g_cachedATR * 0.1); + bool _volLow = !g_regimeData.isVolExpanding; // no strong expansion = low vol breakout + + if(_priceAboveRange && _volLow && !isBullish) + { + // Price poked above range with low vol: likely false breakout + // SELL back inside = TRAP FADE = good entry, bonus + u.regimeScore += 8; + if(g_verboseLog) + PrintFormat("[FIX#501] TRAP-FADE SELL: price above range (%.5f>%.5f) low-vol → bonus +8", _currentPrice, _rangeH); + } + else if(_priceBelowRange && _volLow && isBullish) + { + // Price poked below range with low vol: likely false breakout + // BUY back inside = TRAP FADE = good entry, bonus + u.regimeScore += 8; + if(g_verboseLog) + PrintFormat("[FIX#501] TRAP-FADE BUY: price below range (%.5f<%.5f) low-vol → bonus +8", _currentPrice, _rangeL); + } + else if(!_priceAboveRange && !_priceBelowRange) + { + // ── PREMIUM / DISCOUNT GATE (price inside range) ── + int _penaltyStrong = -30; // score drop that effectively blocks entry + int _penaltySoft = -15; // soft penalty — only very high score entries survive + + // Confirmed range (score 3-4): strict 40/60 boundaries + // Probable range (score 2): softer 30/70 boundaries + double _sellBlockBelow = (g_rangeConfScore >= 3) ? 0.40 : 0.30; + double _buyBlockAbove = (g_rangeConfScore >= 3) ? 0.60 : 0.70; + // Middle zone (40-60%): both directions penalised (range mid = no edge) + double _midLow = 0.40; + double _midHigh = 0.60; + + if(!isBullish && _rangePct < _sellBlockBelow) + { + // SELL at discount = selling near range bottom = reversal risk + u.regimeScore += _penaltyStrong; + if(g_verboseLog) + PrintFormat("[FIX#501] BLOCK SELL: discount %.0f%% < %.0f%% | RangeConf=%d | score%+d", + _rangePct*100, _sellBlockBelow*100, g_rangeConfScore, _penaltyStrong); + } + else if(isBullish && _rangePct > _buyBlockAbove) + { + // BUY at premium = buying near range top = reversal risk + u.regimeScore += _penaltyStrong; + if(g_verboseLog) + PrintFormat("[FIX#501] BLOCK BUY: premium %.0f%% > %.0f%% | RangeConf=%d | score%+d", + _rangePct*100, _buyBlockAbove*100, g_rangeConfScore, _penaltyStrong); + } + else if(_rangePct > _midLow && _rangePct < _midHigh) + { + // Middle zone: both directions risky (range midpoint has no edge) + u.regimeScore += _penaltySoft; + if(g_verboseLog) + PrintFormat("[FIX#501] MIDDLE ZONE %.0f%% | RangeConf=%d | score%+d", + _rangePct*100, g_rangeConfScore, _penaltySoft); + } + // else: correct zone (SELL at premium or BUY at discount) → no penalty + } + } + } + + // ─── MODULE 14: LEGACY BONUS (0-15) ────────────────────────── + // Candle patterns + volume + OB quality. 30% weight, capped +15. + { + int candleScore = 0; + if(isBullish) + { + if(candle.isBullishEngulfing) candleScore += 8; + if(candle.isBullishPinBar) candleScore += 7; + if(candle.isBullishInsideBreak) candleScore += 5; + if(candle.isBullishHammer) candleScore += 3; + } + else + { + if(candle.isBearishEngulfing) candleScore += 8; + if(candle.isBearishPinBar) candleScore += 7; + if(candle.isBearishInsideBreak) candleScore += 5; + if(candle.isBearishShootingStar) candleScore += 3; + } + candleScore = MathMin(candleScore, 15); + int volScore = GetVolumeScoreForEntry(); + int obQScore = MathMin((int)(obQuality * 0.1), 10); + int divScore = GetDivergenceScoreForEntry(isBullish ? 1 : -1); + int tlScore = GetTrendlineScoreForEntry(isBullish ? 1 : -1); + int legacySum = candleScore + volScore + obQScore + divScore + tlScore + + MathMin(confluence.zoneCount * 4, 20); + u.legacyBonus = MathMin(15, (int)(legacySum * 0.30)); + } + + // ─── TOTAL SCORE ────────────────────────────────────────────── + // Proven core: modules 1-5 + regime (always available) + // Cascade extension: modules 6-10 + techniqueBonus + htfBonus (when cascade available) + // Legacy bonus: module 14 (always computed from candle/vol/OB) + // ── MODULE 15: MEAN REVERSION QUALITY (0-25) ────────────────────── + // * FIX#373: Only nonzero when candidate type = MEAN_REV. + // Scores are pre-computed in EA_CheckSignals and passed via cand.baseScore. + // Here we read them from g_pendingMRScore (set before AddCandidate call). + // This keeps ComputeUnifiedScore side-effect-free (no global reads inside loop). + u.mrScore = g_pendingMRScore; // 0 for all non-MR candidates + + u.totalScore = u.trendScore + u.timingScore + u.rrScore + u.momentumScore + u.mlScore + + u.regimeScore + u.legacyBonus + u.mrScore; + if(cascadeAvailable) + u.totalScore += u.cascadeZoneScore + u.cascadeConfluenceScore + u.cascadeSweepScore + + u.cascadeTimingScore + u.cascadePatternScore + u.techniqueBonus + u.htfBonus; + + // ─── GRADE (single calibrated thresholds for all paths) ─────── + // Max without cascade: ~100 (modules 1-5 + regime + legacy). + // Max with cascade: ~200+ (all modules). Practical range 80-140 for quality setups. + if(cascadeAvailable) + { + // Full unified score — higher thresholds + if (u.totalScore >= 130) u.grade = "A+"; + else if(u.totalScore >= 105) u.grade = "A"; + else if(u.totalScore >= 82) u.grade = "B+"; + else if(u.totalScore >= 60) u.grade = "B"; + else if(u.totalScore >= 42) u.grade = "C"; + else if(u.totalScore >= 26) u.grade = "D"; + else u.grade = "F"; + } + else + { + // Pre-cascade gate score — lower thresholds (no cascade modules) + if (u.totalScore >= 75) u.grade = "A+"; + else if(u.totalScore >= 63) u.grade = "A"; + else if(u.totalScore >= 52) u.grade = "B+"; + else if(u.totalScore >= 42) u.grade = "B"; + else if(u.totalScore >= 30) u.grade = "C"; + else if(u.totalScore >= 18) u.grade = "D"; + else u.grade = "F"; + } + + // ─── WIN PROBABILITY & EV ───────────────────────────────────── + u.winProbability = CalculateWinProbability(u.totalScore); + double risk = MathAbs(entryPrice - slPrice); + if(EA_UseMultipleTP && tp2Price > 0 && tp3Price > 0 && risk > 0) + { + double rr1 = MathAbs(tp1Price - entryPrice) / risk; + double rr2 = MathAbs(tp2Price - entryPrice) / risk; + double rr3 = MathAbs(tp3Price - entryPrice) / risk; + double wp = u.winProbability / 100.0; + // FIX#368: use g_workingTP%_Pct (pair-table calibrated), not raw EA_TP%_Percent + double w1 = (g_workingTP1_Pct > 0 ? g_workingTP1_Pct : EA_TP1_Percent) / 100.0; + double w2 = (g_workingTP2_Pct > 0 ? g_workingTP2_Pct : EA_TP2_Percent) / 100.0; + double w3 = (g_workingTP3_Pct > 0 ? g_workingTP3_Pct : EA_TP3_Percent) / 100.0; + // FIX#MERGE_C2: live TP2/TP3 reach stats; conservative fallback 0.10/0.05 when <15 hits + double p2, p3; + if(g_multiTPStats.tp1HitCount >= 15) + { + p2 = MathMax(0.05, MathMin(0.65, (double)g_multiTPStats.tp2HitCount / g_multiTPStats.tp1HitCount)); + p3 = MathMax(0.03, MathMin(0.45, (double)g_multiTPStats.tp3HitCount / g_multiTPStats.tp1HitCount)); + } + else { p2 = 0.10; p3 = 0.05; } + double winReward = w1*rr1 + w2*(p2*rr2) + w3*(p3*rr3); + u.expectedValue = wp * winReward - (1.0 - wp) * 1.0; + } + else if(risk > 0) + { + double rr = MathAbs(tp1Price - entryPrice) / risk; + u.expectedValue = CalculateExpectedValue(u.winProbability / 100.0, rr); + } + u.isPositiveEV = (u.expectedValue >= 0.0); + + // ─── minEV: FIX#371 — handle min_ev=0.00 correctly ─────────── + // OLD: if(cfg.min_ev[tf] > 0) → when set to 0.00, condition fails → stays at 0.08 default. + // FIX: use g_gates.minEV directly (ComputeActiveGates already resolved it). + // If EURUSD H4 min_ev=0.00, then g_autoOptParams.smart_min_ev must be forced to 0.00 + // so g_gates.minEV reads 0.00, and effectiveMinEV=0.00 in EvaluateSmartEntry. + // This is enforced in ApplyPairTFProfile (see FIX#371 there). + + // ─── DEBUG LOG ──────────────────────────────────────────────── + if(g_verboseLog) + PrintFormat("[FIX#369] UnifiedScore: %d [%s] | Trend=%d Timing=%d RR=%d Mom=%d ML=%d Regime=%d Legacy=%d | CascadeOK=%s Zone=%d Conf=%d Sweep=%d Pat=%d Tech=%d HTF=%d | WinP=%.1f%% EV=%.2fR", + u.totalScore, u.grade, + u.trendScore, u.timingScore, u.rrScore, u.momentumScore, u.mlScore, u.regimeScore, u.legacyBonus, + cascadeAvailable ? "Y" : "N", + u.cascadeZoneScore, u.cascadeConfluenceScore, u.cascadeSweepScore, u.cascadePatternScore, + u.techniqueBonus, u.htfBonus, + u.winProbability, u.expectedValue); + + g_lastUnifiedScore = u; + return u; +} + +//+------------------------------------------------------------------+ +//| 8. Calculate Entry Score — WRAPPER (FIX#369) | +//| Delegates to ComputeUnifiedScore (single source of truth). | +//| Pre-cascade path: cascadeAvailable=false, modules 6-13 unused. | +//+------------------------------------------------------------------+ +EntryScoreStruct CalculateEntryScore(bool isLong, double price, + const ConfluenceScoreStruct &confluence, + const CandlePatternStruct &candle, + int obQuality = 0) +{ + // No TP prices available at signal-check time — use ATR-based estimates for EV + double sl_est = isLong ? price - g_cachedATR * GetActiveSLMult() + : price + g_cachedATR * GetActiveSLMult(); + double tp1_est = isLong ? price + g_cachedATR * GetActiveTP1Mult() + : price - g_cachedATR * GetActiveTP1Mult(); + ConfirmationCascade emptyCascade; + ZeroMemory(emptyCascade); + UnifiedScoreResult u = ComputeUnifiedScore( + isLong, price, sl_est, tp1_est, 0.0, 0.0, + confluence, candle, obQuality, + emptyCascade, false); // cascadeAvailable=false: pre-cascade gate + + // Map UnifiedScoreResult → EntryScoreStruct for backward compatibility + EntryScoreStruct score; + ZeroMemory(score); + score.trendScore = u.trendScore; + score.timingScore = u.timingScore; + score.rrScore = u.rrScore; + score.momentumScore = u.momentumScore; + score.mlScore = u.mlScore; + score.totalScore = u.totalScore; + score.grade = u.grade; + score.calcTime = u.calcTime; + g_lastEntryScore = score; + return score; +} +//+------------------------------------------------------------------+ +//| Check Functions for Signals | +//+------------------------------------------------------------------+ +bool HasActiveSupportTrendline() +{ + return g_tlSupportActive; +} +bool HasActiveResistanceTrendline() +{ + return g_tlResistanceActive; +} +bool HasTrendlineBreak(int direction) +{ + if(direction == 1) return g_tlBreakBullish; + if(direction == -1) return g_tlBreakBearish; + return false; +} +bool HasTrendlineRetest(int direction) +{ + if(direction == 1) return g_tlRetestBullish; + if(direction == -1) return g_tlRetestBearish; + return false; +} +bool IsPriceNearTrendline(double price, int direction) +{ + for(int i = 0; i < g_trendlineCount; i++) + { + if(!g_trendlines[i].active || g_trendlines[i].broken) continue; + bool isSupport = (g_trendlines[i].type == TL_SUPPORT); + double distance = MathAbs(price - g_trendlines[i].currentPrice); + if(distance < g_cachedATR * 0.5) + { + if((direction == 1 && isSupport) || (direction == -1 && !isSupport)) + return true; + } + } + return false; +} +string GetTrendlineSummary() +{ + int supportCount = 0, resistanceCount = 0, brokenCount = 0; + for(int i = 0; i < g_trendlineCount; i++) + { + if(!g_trendlines[i].active) continue; + if(g_trendlines[i].type == TL_SUPPORT) supportCount++; + else resistanceCount++; + if(g_trendlines[i].broken) brokenCount++; + } + return StringFormat("TL: Support=%d Resistance=%d Broken=%d", + supportCount, resistanceCount, brokenCount); +} +//+------------------------------------------------------------------+ +//| 9. Check if Entry Score Meets Minimum | +//+------------------------------------------------------------------+ +bool MeetsMinimumEntryScore(const EntryScoreStruct &score) +{ + if(!InpEnableScoring) + return true; + if(!InpRequireMinScore && UseCandleFilter) // [v6.42] UseCandleFilter gate + return true; + // * v10.06 FIX#275: Read from g_gates (ComputeActiveGates). + // Previously this function had its own independent threshold logic (FIX#156/264/272), + // which could drift out of sync with SelectBestCandidate and SmartEntry. + // Now all 3 score gates share the identical value from a single source. + double effectiveMin = g_gates.computed ? g_gates.minScore : (double)EA_MinEntryScore; + if(score.totalScore < (int)effectiveMin) + return false; + // * FIX#361: H4+ rrScore HARD GATE. + // PROBLEM: rrScore is used only for ranking but never rejects. 98/185 trades had RR<1.4 + // (rrScore=8, WR=30%, -$647). The gate at minScore=44 is always passed because total + // EntryScore >> 44 regardless of rrScore. rrScore has zero rejection power. + // FIX: For H4+, require rrScore >= 10 to pass MeetsMinimumEntryScore. + // rrScore=6 → RR<1.4 (new FIX#360 bracket) → hard reject. + // rrScore=12 → RR 1.4-1.6 → passes (borderline, further filtered by mrr[3]=1.60 in SelectBest). + // rrScore=20 → RR 1.6-2.5 → passes (sweet spot). + // This gate acts on the RAW EntryScore scale (0-85) where the gap was real. + // NOTE: Only applies to H4+. Lower TFs have different RR dynamics and existing calibration. + if(_Period >= PERIOD_H4 && score.rrScore < 10) + { + if(g_verboseLog) + PrintFormat("[FIX#361] H4 rrScore HARD GATE: rrScore=%d < 10 (RR too low) — REJECTED", + score.rrScore); + return false; + } + // * FIX#305: score_cap — reject "too perfect" setups (liquidity traps on M15). + // EA2026 backtest: score 90-110 = WR34% (-$865). Score 80-89 = profitable (+$604). + // score_cap=0 means no cap (H4/H1 not affected unless set in pair table). + // * FIX#427: Direction-aware score cap. + // ROOT (Dec 13): score_cap[H1]=95 blocked ALL BUY signals (score 121-174) while a + // CT SELL score=90 passed freely. The cap was intended to block overfitting traps + // but instead blocked WITH-TREND high-quality setups and let CT low-score trades through. + // WHY high-score CT is the real trap: CT trades accumulate conflicting signals (MTF + // opposes, structure opposes, regime opposes) — each conflict ADDS score points via + // the weighted scoring modules. score=140 CT SELL in a bullish environment means the + // EA detected many bearish sub-signals AGAINST the dominant direction — exactly the + // overfitting pattern FIX#305 was designed to stop. + // WHY with-trend high-score is NOT a trap: score=140 BUY in a bullish environment means + // all modules agree — HTF+MTF+structure+regime all point up. No conflict amplification. + // FIX: cap applies ONLY to counter-trend trades. + // WITH-TREND (structure aligned): no cap — high score = genuine confluence. + // COUNTER-TREND (structure opposes): cap enforced — high score = conflict amplification. + // Structure alignment is the key discriminator (not just MTF, which can flicker). + if(g_autoOptParams.score_cap > 0 && score.totalScore >= g_autoOptParams.score_cap) + { + // Determine if this is a CT trade: signal direction vs structure + bool _sigBull427 = g_ea_signal.isValid ? g_ea_signal.isBullish : g_isBullishStructure; + bool _structAligned427 = ( _sigBull427 && g_isBullishStructure) || + (!_sigBull427 && !g_isBullishStructure); + if(_structAligned427) + { + // WITH-TREND high score: skip cap — genuine confluence, not conflict amplification + if(g_verboseLog) + PrintFormat("[FIX#427] SCORE CAP BYPASSED (with-trend): score=%d >= cap=%d but structure aligned → ALLOWED", + score.totalScore, g_autoOptParams.score_cap); + } + else + { + // COUNTER-TREND high score: enforce cap — high score = conflicting signals + if(g_verboseLog) + PrintFormat("[FIX#305+FIX#427] SCORE CAP CT: score=%d >= cap=%d (counter-trend conflict amplification) — REJECTED", + score.totalScore, g_autoOptParams.score_cap); + return false; + } + } + return true; +} +//+------------------------------------------------------------------+ +//| 10. Draw Entry Score on Chart | +//+------------------------------------------------------------------+ +void DrawEntryScore(datetime time, double price, const EntryScoreStruct &score, bool isLong) +{ + if(!InpShowScoreOnChart || !Scoring_Enabled) return; // [v6.42] + string prefix = "ICT_SCORE_" + IntegerToString(g_scoreObjCount++); + // Position score label + double labelPrice = isLong ? price - g_cachedATR * 0.6 : price + g_cachedATR * 0.6; + // Color based on grade + color scoreColor; + if(score.grade == "A+" || score.grade == "A") + scoreColor = clrLime; + else if(score.grade == "B+" || score.grade == "B") + scoreColor = clrYellow; + else if(score.grade == "C") + scoreColor = clrOrange; + else + scoreColor = clrRed; + // Main score label + string scoreText = StringFormat("SCORE: %d/85 [%s]", score.totalScore, score.grade); + ObjectCreate(0, prefix + "_MAIN", OBJ_TEXT, 0, time, labelPrice); + ObjectSetString(0, prefix + "_MAIN", OBJPROP_TEXT, scoreText); + ObjectSetInteger(0, prefix + "_MAIN", OBJPROP_COLOR, scoreColor); + ObjectSetInteger(0, prefix + "_MAIN", OBJPROP_FONTSIZE, 10); + ObjectSetString(0, prefix + "_MAIN", OBJPROP_FONT, "Arial Bold"); + // Detailed breakdown (smaller) + double detailPrice = isLong ? labelPrice - g_cachedATR * 0.15 : labelPrice + g_cachedATR * 0.15; + string detailText = StringFormat("Trend:%d Tim:%d RR:%d Mom:%d ML:%d", + score.trendScore, score.timingScore, score.rrScore, + score.momentumScore, score.mlScore); + ObjectCreate(0, prefix + "_DETAIL", OBJ_TEXT, 0, time, detailPrice); + ObjectSetString(0, prefix + "_DETAIL", OBJPROP_TEXT, detailText); + ObjectSetInteger(0, prefix + "_DETAIL", OBJPROP_COLOR, clrGray); + ObjectSetInteger(0, prefix + "_DETAIL", OBJPROP_FONTSIZE, 8); + ObjectSetString(0, prefix + "_DETAIL", OBJPROP_FONT, "Arial"); +} +//+------------------------------------------------------------------+ +//| 11. Get Score Summary Text for Dashboard | +//+------------------------------------------------------------------+ +string GetEntryScoreSummary() +{ + if(g_lastEntryScore.totalScore == 0) + return "No recent entry score"; + string summary = StringFormat( + "Last Score: %d/85 [%s]\n" + "Trend: %d | Timing: %d | R:R: %d\n" + "Momentum: %d | ML: %d\n" + "Legacy: Z=%d S=%d C=%d Cn=%d V=%d", + g_lastEntryScore.totalScore, g_lastEntryScore.grade, + g_lastEntryScore.trendScore, g_lastEntryScore.timingScore, g_lastEntryScore.rrScore, + g_lastEntryScore.momentumScore, g_lastEntryScore.mlScore, + g_lastEntryScore.zoneScore, g_lastEntryScore.sweepScore, + g_lastEntryScore.confluenceScore, g_lastEntryScore.candleScore, + g_lastEntryScore.volumeScore + ); + return summary; +} +//+------------------------------------------------------------------+ +//| 12. Deinitialize Entry Scoring | +//+------------------------------------------------------------------+ +void DeinitializeEntryScoring() +{ + // Delete score objects + ObjectsDeleteAll(0, "ICT_SCORE_"); + Print("[OK] Entry Scoring System Deinitialized"); +} +//+------------------------------------------------------------------+ +//| OnInit - COMPLETE v6.41 FINAL - WITH DIAMOND PATTERNS | +//| Production Ready with All Optimizations | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| FIX#41 FUNCTIONS -- v9.22 REWRITE | +//+------------------------------------------------------------------+ +// ==================================================================== +// * v9.24 FIX#76 -- SmartExit AdverseClose (EXTENSION OF SMARTEXIT) +// ==================================================================== +// PURPOSE: SmartExit currently only acts when profit_distance > 0. +// When a trade is at breakeven or small loss AND the market analysis +// has FLIPPED against the position -> there is NO reason to hold. +// Holding = waiting for full -1R loss when the analysis itself says "wrong". +// +// LOGIC: +// Zone A: RR >= 1.2 -> FIX#41 Trail handles it. This function skips. +// Zone B: 0 <= RR < 1.2 -> if adverse signals detected -> close at BE (protect 0) +// Zone C: -0.3 <= RR < 0 -> if STRONG adverse signals -> cut loss now (~-0.3R vs -1R) +// Zone D: RR < -0.3 -> already too deep, let SL work. Don't close. +// +// ADVERSE SIGNALS (cumulative score): +// Regime now opposing direction: STRONG=15pts, WEAK/TRENDING=8pts +// MTF now opposing direction: BEARISH/BULLISH=10pts, STRONG=15pts +// Structure (BOS) flipped: 8pts +// Trade stalled >30 bars near 0: 5pts +// +// THRESHOLDS: +// Zone B (profit side): score >= 15 (single strong signal = close) +// Zone C (loss side): score >= 23 (need >=2 concurrent signals = more evidence) +// +// RESULT: Converts -1R losses into 0 or -0.3R when market proves entry wrong. +// ==================================================================== +// * v9.41 FIX#177: ZONE INVALIDATION — EXIT WHEN ENTRY REASON IS GONE +// ==================================================================== +// Core principle: if the zone that caused the entry no longer holds, +// the trade hypothesis is invalid regardless of RR or bars alive. +// Runs on every new bar (bar close = confirmed, not just a wick). +// +// Logic: +// BUY from FVG/OB: zone is invalidated if bar CLOSES below zoneBottom +// SELL from FVG/OB: zone is invalidated if bar CLOSES above zoneTop +// Close immediately at market — take the small loss, don't wait for SL. +// +// Guards: +// - Zone data must be present (zoneTop != 0 && zoneBottom != 0) +// - Bar 0 (entry bar) is skipped — zone always looks violated mid-bar at entry +// - Trade already in profit > 0.5R: zone breach = zone swept, trade wins → skip +// - zoneInvalidated flag: fire once per trade, no repeat checks +// ==================================================================== +bool CheckZoneInvalidation(ulong ticket) +{ + if(!PositionSelectByTicket(ticket)) return false; + + // Find the MultiTPEntry for this ticket + // * FIX#408: Add fallback lookup by entry price + direction. + // ROOT CAUSE: AddMultiTPEntry() stores only the LAST opened ticket (TP3 tranche). + // TP1 and TP2 tranches have different tickets → ticket lookup returns mIdx=-1 → no zone protection. + // Backtest evidence: ZoneInvalidation only fired on 1 position per trade while 2-3 were open. + // FIX: when ticket not found, fall back to entry price + direction match. + // This correctly links all tranches (TP1/TP2/TP3) to the same MultiTPEntry. + int mIdx = -1; + for(int m = 0; m < ArraySize(g_multiTPEntries); m++) + { + if(g_multiTPEntries[m].active && (ulong)g_multiTPEntries[m].ticket == ticket) + { mIdx = m; break; } + } + // * FIX#408: Fallback — match by entry price + direction (covers TP1/TP2 tranches) + if(mIdx < 0) + { + double posEntry = PositionGetDouble(POSITION_PRICE_OPEN); + int posDir = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) ? 1 : -1; + double priceTol = g_cachedATR * 0.05; // 5% ATR tolerance for same-signal tranches + for(int m = 0; m < ArraySize(g_multiTPEntries); m++) + { + if(!g_multiTPEntries[m].active) continue; + if(g_multiTPEntries[m].direction != posDir) continue; + if(MathAbs(g_multiTPEntries[m].entryPrice - posEntry) <= priceTol) + { mIdx = m; break; } + } + } + if(mIdx < 0) return false; + + // Already fired or no zone data + if(g_multiTPEntries[mIdx].zoneInvalidated) return false; + if(g_multiTPEntries[mIdx].zoneTop == 0 || g_multiTPEntries[mIdx].zoneBottom == 0) return false; + + string sym = PositionGetString(POSITION_SYMBOL); + ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); + bool isBuy = (type == POSITION_TYPE_BUY); + double entry = PositionGetDouble(POSITION_PRICE_OPEN); + double sl = g_multiTPEntries[mIdx].stopLoss; + + // Skip entry bar (bar 0 = the bar the trade was opened on) + datetime openTime = (datetime)PositionGetInteger(POSITION_TIME); + int barsAlive = Bars(sym, PERIOD_CURRENT, openTime, TimeCurrent()); + if(barsAlive < 1) return false; + + // If already in profit > 0.5R, zone was swept in our favour — let trail/SE handle exit + double slDist = MathAbs(entry - sl); + double price = isBuy ? SymbolInfoDouble(sym, SYMBOL_BID) : SymbolInfoDouble(sym, SYMBOL_ASK); + double currentRR = (slDist > 0) ? (isBuy ? (price - entry) : (entry - price)) / slDist : 0; + if(currentRR >= 0.5) return false; + + // * v10.10 FIX#284: Don't close when already deep in SL territory (within 0.2R of SL). + // Problem: ZoneInvalidation fired at RR=-0.67 on 02.06 SELL, closing TP1 at market. + // At -0.67R the trade is 67% of the way to SL — ZoneInvalidation no longer "saves" much. + // SL is about to fire anyway; closing at market here just adds spread cost and logs noise. + // Guard: if currentRR < -0.8R → SL is imminent → let SL handle it cleanly. + if(currentRR < -0.80) return false; + + // Read last CLOSED bar's close price (bar[1] = previous closed bar) + double prevClose[]; + ArraySetAsSeries(prevClose, true); + if(CopyClose(sym, PERIOD_CURRENT, 1, 1, prevClose) < 1) return false; + double closedBarClose = prevClose[0]; + + double zTop = g_multiTPEntries[mIdx].zoneTop; + double zBot = g_multiTPEntries[mIdx].zoneBottom; + + bool invalidated = false; + string reason = ""; + + if(isBuy && closedBarClose < zBot) + { + invalidated = true; + reason = StringFormat("BUY zone invalidated: close=%.5f < zoneBottom=%.5f [%s]", + closedBarClose, zBot, g_multiTPEntries[mIdx].zoneType); + } + else if(!isBuy && closedBarClose > zTop) + { + invalidated = true; + reason = StringFormat("SELL zone invalidated: close=%.5f > zoneTop=%.5f [%s]", + closedBarClose, zTop, g_multiTPEntries[mIdx].zoneType); + } + + if(!invalidated) return false; + + // * v9.44 FIX#185: TC zone is never set (g_pendingZone* = 0 for TC). + // zoneTop==0 guard in CheckZoneInvalidation already returns false for TC trades. + // This block is kept only as belt-and-suspenders for legacy TC entries in registry. + + // Mark so we don't fire again + g_multiTPEntries[mIdx].zoneInvalidated = true; + + // Close the trade + CTrade tradeObj; + tradeObj.SetExpertMagicNumber(EA_MagicNumber); + bool closed = tradeObj.PositionClose(ticket); + PrintFormat("* v9.41 FIX#177 ZoneInvalidation | %s | RR=%.2f | Bars=%d | %s | Close=%s", + isBuy ? "BUY" : "SELL", currentRR, barsAlive, reason, + closed ? "OK" : "FAILED"); + return closed; +} + +// Feb 2026 example: Trade #17 (02.06 17:15) closed in 18min at -$100. +// With FIX#76: regime+MTF+structure all bearish within bars 3-5 -> closed at -0.2R (-$20) +// ==================================================================== +//+------------------------------------------------------------------+ +//| * v10.31 FIX#320/P2+P5: CENTRAL SE FLOOR — single source of truth +//| All SE code paths call this. Eliminates dual-path problem where +//| FIX#315 was wired in one path but not the other. +//| Returns the effective minimum RR before SmartExit can fire. +//| open_price: entry price of the position (to match registry). +//+------------------------------------------------------------------+ +// ========================================================================= +// TRADE EXIT MANAGEMENT — two clean functions, no patches +// +// ProtectTrade(ticket): called every tick for every position. +// 1. Calculates current R:R using ORIGINAL SL (never the BE-modified one) +// 2. When peak R:R threshold reached → moves ALL legs of the trade to BE +// 3. After BE, trails SL behind recent structure (swing H/L × ATR buffer) +// 4. Never closes — only adjusts SL. MT5 hits SL when price returns. +// +// EvaluateExit(ticket): called every bar for TP1 leg only. +// Closes the position when 3+ independent reversal signals align. +// Signals: RSI exhaustion, opposing OB proximity, consecutive closes against, +// MTF flip, D1 structure shift (CHoCH), volume divergence. +// Minimum profit gate: position must be in profit before any close fires. +// ========================================================================= + +bool ProtectTrade(ulong ticket) +{ + if(!PositionSelectByTicket(ticket)) return false; + ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); + string sym = PositionGetString(POSITION_SYMBOL); + double entry = PositionGetDouble(POSITION_PRICE_OPEN); + double curSL = PositionGetDouble(POSITION_SL); + double price = (posType == POSITION_TYPE_BUY) + ? SymbolInfoDouble(sym, SYMBOL_BID) + : SymbolInfoDouble(sym, SYMBOL_ASK); + bool isBuy = (posType == POSITION_TYPE_BUY); + + // --- Get original SL, peak RR, and per-trade thresholds from multiTPEntries --- + // These are calculated at entry (AddMultiTPEntry) from actual TP1/SL distances. + // perTrade_BE_RR: when to move SL to entry (e.g. 0.80R for H1, 0.56R for H4) + // perTrade_Trail_RR: same threshold used for trailing start (stored separately) + double origSL = curSL; + double peakRR = 0.0; + double tp1Price = 0.0; + double beTriggerR = 0.0; + bool isCtTrade = false; + for(int i = 0; i < ArraySize(g_multiTPEntries); i++) + { + if(!g_multiTPEntries[i].active) continue; + if((ulong)g_multiTPEntries[i].ticket != ticket && + MathAbs(g_multiTPEntries[i].entryPrice - entry) > g_pipValue * 5) continue; + if(g_multiTPEntries[i].stopLoss > 0) origSL = g_multiTPEntries[i].stopLoss; + if(g_multiTPEntries[i].peakRR > 0) peakRR = g_multiTPEntries[i].peakRR; + if(g_multiTPEntries[i].tp1Price > 0) tp1Price = g_multiTPEntries[i].tp1Price; + if(g_multiTPEntries[i].perTrade_BE_RR > 0) beTriggerR = g_multiTPEntries[i].perTrade_BE_RR; + isCtTrade = g_multiTPEntries[i].isCounterTrend; + break; + } + // CT trades: tighter BE — move to safety sooner since entering against trend + // Normal: 55% of TP1. CT: 40% of TP1 (protect faster, trade has less room to be wrong) + if(isCtTrade && beTriggerR > 0) beTriggerR = MathMax(0.35, beTriggerR * 0.80); + + // Fallback if no multiTPEntry found (e.g. TP2/TP3 runners after TP1 closed) + // FIX#508: multiplier 0.55→0.40 consistent with AddMultiTPEntry change + if(beTriggerR <= 0) beTriggerR = isCtTrade ? 0.40 + : (tp1Price > 0) + ? MathMax(0.40, MathMin(1.20, MathAbs(tp1Price-entry) / MathMax(g_pipValue, MathAbs(entry-origSL)) * 0.40)) + : 0.50; + + double slDist = MathAbs(entry - origSL); + if(slDist < g_pipValue * 0.5) return false; + + // Use bar HIGH/LOW so intra-bar peaks are not missed in OHLC backtesting. + // On H1+, only the bar CLOSE tick is seen — price may peak inside the bar + // and reverse before close without ProtectTrade ever observing the peak RR. + double barExtreme = isBuy ? iHigh(sym, _Period, 0) : iLow(sym, _Period, 0); + double currRR; + if(barExtreme > 0) + { + double tickRR = isBuy ? (price - entry) / slDist : (entry - price) / slDist; + double barRR = isBuy ? (barExtreme - entry) / slDist : (entry - barExtreme) / slDist; + currRR = MathMax(tickRR, barRR); + } + else + currRR = isBuy ? (price - entry) / slDist : (entry - price) / slDist; + + // --- Update peak RR --- + if(currRR > peakRR) + { + for(int i = 0; i < ArraySize(g_multiTPEntries); i++) + { + if(g_multiTPEntries[i].active && + MathAbs(g_multiTPEntries[i].entryPrice - entry) < g_pipValue * 5) + { g_multiTPEntries[i].peakRR = currRR; break; } + } + peakRR = currRR; + } + + bool alreadyAtBE = isBuy ? (curSL >= entry - g_pipValue) + : (curSL <= entry + g_pipValue); + + // --- Step 1: Break-Even --- + // Trigger: perTrade_BE_RR from entry calculation (stored in multiTPEntries). + // Calculated at open from actual TP1/SL: tp1_R × 0.40-0.50 depending on TF. + // H4 example: tp1_R=2.0, mult=0.40 → BE=0.80R. H1: tp1_R=1.60, mult=0.50 → BE=0.80R. + // Action: move ALL legs of this trade SL to entry + 0.5p buffer. + if(!alreadyAtBE && currRR >= beTriggerR) + { + double beSL = isBuy ? entry + g_pipValue * 0.5 : entry - g_pipValue * 0.5; + // Move ALL legs with same entry/direction to BE + bool moved = false; + for(int p = PositionsTotal() - 1; p >= 0; p--) + { + ulong ptkt = PositionGetTicket(p); + if(ptkt == 0 || !PositionSelectByTicket(ptkt)) continue; + if(PositionGetString(POSITION_SYMBOL) != sym) continue; + if((long)PositionGetInteger(POSITION_MAGIC) != EA_MagicNumber) continue; + bool sameDir = (isBuy && PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY) || + (!isBuy && PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL); + if(!sameDir) continue; + if(MathAbs(PositionGetDouble(POSITION_PRICE_OPEN) - entry) > g_pipValue * 5) continue; + double legSL = PositionGetDouble(POSITION_SL); + bool legAtBE = isBuy ? (legSL >= entry - g_pipValue) + : (legSL <= entry + g_pipValue); + if(legAtBE) continue; + if(SafePositionModify(ptkt, beSL, PositionGetDouble(POSITION_TP), "BE")) + { + moved = true; + if(g_verboseLog) + PrintFormat("[ProtectTrade] BE triggered @ %.2fR (trigger=%.2fR) | ticket=%llu", + currRR, beTriggerR, ptkt); + } + } + if(moved) + { + for(int i = 0; i < ArraySize(g_multiTPEntries); i++) + { + if(g_multiTPEntries[i].active && + MathAbs(g_multiTPEntries[i].entryPrice - entry) < g_pipValue * 5) + { g_multiTPEntries[i].slMovedToBE = true; break; } + } + } + return moved; + } + + // Trail SL is handled by ProfitGuard_Trail in EA_ManagePositions + // (per-tranche distances, TP-proportional tightening, progressive locks). + // ProtectTrade role: BE only — move ALL legs to safety simultaneously. + return false; +} + + +int OnInit() +{ + Print("==========================================================="); + Print("========================================"); + Print("[LAUNCH][LAUNCH][LAUNCH] " + EA_VERSION + " — 12-Scenario+Refactor [LAUNCH][LAUNCH][LAUNCH]"); + Print(" [EXIT] Removed: SmartExit_FIX41 (625 lines), FIX#403, PeakTrail, FIX#423, FIX#424 patches."); + Print(" [EXIT] ProtectTrade: moves all legs to BE at 55% of TP1, then trails 1xATR. No close."); + Print(" [EXIT] EvaluateExit: closes TP1 only when 3+ reversal signals (RSI/OB/closes/MTF/CHoCH/FVG)."); + Print(" [H1] se_minrr=0.90R, allow_tc=-1, peakFloor=0.65R, BE trigger=0.55xTP1."); + Print(" [CASCADE] TP1 SL → closes all same-entry runners immediately."); + Print(" [ML] NN score floor=0: bonus-only (0→+15). Structural gates handle rejection."); + Print(" [COST] max_cost_pct per TF: M5=28% M15=30% H1=30% H4=15% D1=10%."); + Print(" Input deprecated (=0), g_effectiveBIB = AccountInfoDouble(ACCOUNT_BALANCE) at OnInit."); + Print(" Works correctly for any account size without manual configuration."); + Print(" [FIX#479] PeakRR architectural fix — works M5 through D1."); + Print(" ROOT CAUSE: entryGroup created at DEAL_ENTRY_OUT (close time), not at open."); + Print(" During trade: no active entryGroup → ManagePositions live peakRR propagation"); + Print(" (FIX#456/469b) always failed — no group to write to."); + Print(" FIX#470 tried to seed at close from multiTPEntries[], but"); + Print(" CleanupCompletedMultiTP() deletes inactive entries after 5 minutes."); + Print(" M5: trades close in <5min → cleanup hasn't run → FIX#470 worked by accident."); + Print(" M15/H1/H4/D1: trades last hours → entry already deleted → PeakRR=0.00."); + Print(" ARCHITECTURAL FIX: create entryGroup in AddMultiTPEntry at trade OPEN."); + Print(" posTicket registered as positionID immediately → primary match works tick 1."); + Print(" ManagePositions writes peakRR live every tick via positionID match."); + Print(" At close: EA_UpdateTradeResults finds group already exists → correct peakRR."); + Print(" [FIX#477] M5 CT-WEAK exemption when MTF=STRONG_BEAR/BULL."); + Print(" ROOT: Jan12 2026 — MTF=STRONG_BEARISH 100%, M5 regime=WEAK UPTREND (Judas swing)."); + Print(" FIX#55 blocked ALL SELL candidates (score 100-133) → missed 60p drop."); + Print(" ICT: M5 bounce before big move = manipulation phase. HTF STRONG > M5 lag."); + Print(" Fix: STRONG_BEAR/BULL only (stricter than M15 plain BEARISH) → counterTrendWeak=false."); + Print(" Applies only to M5. M15/H1/H4 chains unchanged."); + Print(" [FIX#478] FIX#306 London/NY open block skipped when MTF=STRONG_BEAR/BULL."); + Print(" ROOT: Jan12 08:00-08:30 block fired exactly as the 60p drop started."); + Print(" On 100% STRONG trend days the London open IS the move, not a sweep trap."); + Print(" Fix: STRONG only (not plain) — conservative, prevents false override on weak trend days."); + Print(" [FIX#473] M5/M15 peakFloor raised: 0.20R→0.50R minimum."); + Print(" ROOT: FIX#403 was firing at peaks of 0.33-0.85R on M5 (floor=0.20R)."); + Print(" Trades closed at 0.10-0.36R BEFORE the large directional move developed."); + Print(" e.g. Jan07 OB: peak=0.85R → closed at 0.25R while price continued 40p."); + Print(" Fix: floor=MathMax(0.50, se_minrr*0.60) for _Period<=M15. H1/H4/D1 unchanged."); + Print(" [FIX#474] TC disabled on M15 EURUSD: allow_tc[1]=-1."); + Print(" ROOT: Jan-Feb backtest TC M15 = 13 trades, WR=46%, Net=-$1,777."); + Print(" [FIX#475] BREAKER disabled on M15 EURUSD: allow_breaker[1]=-1."); + Print(" ROOT: Jan-Feb backtest BREAKER M15 = 9 trades, WR=33%, Net=-$1,286."); + Print(" Breaker blocks re-entered by price on M15 → wrong side of block → SL."); + Print(" [FIX#476] M15 EURUSD calibration (index [1] only):"); + Print(" tp1[1] 6.00→4.00 (only 1 TP1 hit in 76 trades; 4.00*ATR reachable)."); + Print(" tp2[1] 7.50→6.00, tp3[1] 9.00→8.00 (maintain tp1=80%."); + Print(" bt8 Jan 8: D1=GREEN (bullish) + SELL + MTF conf=74% → now BLOCKED."); + Print(" [FIX#443c] Gate 0c: Confirmed divergence VETO. Was +12pts bonus — now HARD BLOCK."); + Print(" Bullish div confirmed → DO NOT SELL. Bearish div → DO NOT BUY."); + Print(" bt8 Jan 8: Bullish div confirmed 04:00 → SELL at 12:00 → now BLOCKED."); + Print(" [FIX#443d] Gate 0d: ADX per-technique floor. BOS_RETEST needs ADX>=20. TC needs ADX>=22."); + Print(" bt8 Jan 8: ADX=18 for BOS_RETEST SELL → no trend momentum → now BLOCKED."); + Print(" [FIX#443e] Gate 0e: H4 prior bar confirmation. Prior bar opposing = -12pts penalty."); + Print(" Sweep exception: prior bar sweep in trade dir = confirmation, no penalty."); + Print(" [FIX-B] OTE liquidity sweep gate: requires swept SSL(BUY) or BSL(SELL) within 2xATR/10bars."); + Print(" ROOT: bt5/bt6 OTE losses opened in fib zone with no sweep = no institutional proof."); + Print(" [FIX-C] TC structural TP1 = FindNearestStructuralTP (replaces ATR x mult)."); + Print(" ROOT: structural SL avg 89p + ATR TP1 96p = 0.97R negative EV."); + Print(" [FIX#439] D1 bypasses EA_EnableTimeFilter (StartHour=7→21 blocked all midnight bar opens)."); + Print(" ROOT CAUSE of ALL 0-trade D1 results: EA_CheckSignals returned immediately at"); + Print(" 00:06 UTC (D1 bar open) because 00:06 is outside 07:00-21:59 filter window."); + Print(" FIX#166 fired later intra-bar but g_ea_lastSignalBar already marked = skip."); + Print(" Fix: if(_Period >= PERIOD_D1) bypass time filter entirely. Sessions irrelevant."); + Print(" [FIX#439b] D1 bypasses session spread spike filter (midnight rollover spread = normal)."); + Print(" [FIX#440] D1 bypasses FIX#28 dead zone block in SelectBestCandidate."); + Print(" [FIX#441] D1 calibration v2: Kelly disabled, risk 2.00→1.50%, SE thresholds wider."); + Print(" [FIX#441b] D1 hard-caps risk at cfg.risk[4]=1.50% regardless of tier/compounding."); + Print(" [FIX#442] D1 SmartExit floor now uses MathMax (se_minrr[4]=1.50R respected)."); + Print(" ROOT (v10.94 log): FIX#399 used MathMin(perTrade=0.75R, floor=1.50R)=0.75R."); + Print(" se_minrr[4]=1.50R was completely ignored → SE fired at 0.75R every trade."); + Print(" Jan19 SELL: $31 win (2p) instead of TP1=139p. Feb7: $5.70 (1p) instead TP1=148p."); + Print(" perTrade=tp1_R×0.40: with TP1=1.88R→0.75R, TP1=2.56R→1.02R, TP1=2.98R→1.19R."); + Print(" ALL below the 1.50R floor → all 4 wins closed too early."); + Print(" Fix: D1+ uses MathMax so floor=1.50R is the actual minimum SE threshold."); + Print(" ROOT (v10.94 log): FIX#390 tier A+ ceiling=5%+finalMult=1.676 → adj=4.69%."); + Print(" ALL D1 scores (96-111) qualify as A+, making 5% ceiling the default."); + Print(" Fix: D1+ _tierHardCeiling = g_autoOptParams.risk_pct = 1.50% flat."); + Print(" Expected: $375 max loss per trade (vs $948-$1190 before)."); + Print(" PROBLEM A: Kelly used H4 history for D1 → AdjRisk=4.69% → $948 single loss."); + Print(" Fix: D1+ skips Kelly in CalculatePositionSize → flat 1.50% risk."); + Print(" PROBLEM B: se_peak_th1=0.70 closed trades too early (Jan19=$29, Feb5=$3.80)."); + Print(" Fix: se_peak_th1=0.50, th2=0.55, th3=0.65 — wider D1 pullback tolerance."); + Print(" PROBLEM C: v7.5c ATR×1.3=84p floor expanded D1 structural SL unnecessarily."); + Print(" Fix: D1+ atrSLMult=0.35 (zone noise floor, structural SL preserved at 50-80p)."); + Print(" ROOT (v10.92 log): D1 bar open 00:06 → dead zone 22:00-01:00 → return -1."); + Print(" Candidates=2-3 generated correctly but SelectBestCandidate returned -1."); + Print(" Holiday blocks (Jan 1-3, Dec 24-26) also fired and blocked all D1 bars."); + Print(" Fix: D1+ sets timeBlocked=false before the return. Sessions irrelevant on D1."); + Print(" [FIX#438] D1 candle reaction check skipped for FVG and OB candidates."); + Print(" ROOT (v10.90 log): At D1 bar open (00:06), current bar has 0-6 min of data."); + Print(" lastClose[0] ≈ lastOpen[0] → currentRejection=false."); + Print(" If prev bar bullish (common in uptrend): mitigationCandle=false too."); + Print(" hasReaction=false → ALL FVG/OB candidates silently skipped → 0 candidates."); + Print(" ICT: D1 zone approach = valid entry. Bar-level reaction check is M5/M15 concept."); + Print(" D1 FVGs/OBs are HTF institutional footprints — price approaching = valid signal."); + Print(" Fix: D1+ (_Period>=PERIOD_D1) sets hasReaction=true unconditionally."); + Print(" [FIX#437] D1 FVG quality threshold lowered: HIGH → MEDIUM for D1+."); + Print(" ROOT (v10.89 log): minFVGQual=HIGH (score>=60). D1 bar open = midnight."); + Print(" KZ=NO at midnight → no KZ bonus (+0). Volume low → no vol bonus (+0)."); + Print(" D1 FVG best case without KZ/vol/PD: size(15)+structure(25)=40 = MEDIUM."); + Print(" HIGH requires >=60 → 100% of D1 FVGs blocked at quality gate → 0 candidates."); + Print(" This is the ACTUAL root cause of 0 trades. FIX#433-436 were correct but"); + Print(" this final gate was still blocking all FVGs before AddCandidate() ran."); + Print(" Fix: D1+ uses MEDIUM (score>=40) threshold. SelectBestCandidate/SmartEntry"); + Print(" still apply score/EV/WP/minRR filters. Quality is just the entry gate."); + Print(" [FIX#436] D1 ATR-based FVG/OB touch tolerance: MathMax(zone*0.50, ATR*0.30)."); + Print(" ROOT (v10.88 log): FIX#435 used 50% zone tolerance = 4.5-10p on D1."); + Print(" D1 ATR=65p → price can be 30+ pips from any FVG at bar open."); + Print(" 50% of zone-size still too small. In_fvg=false every bar → 0 candidates."); + Print(" FIX: tolerance = MathMax(zone*0.50, ATR*0.30). At D1 ATR=65p: 19.5p."); + Print(" Any FVG within ~20p of current price generates a candidate."); + Print(" ICT concept: approaching the zone = valid entry, not just inside zone."); + Print(" Zone invalidation (price >25% beyond edge) unchanged — prevents entries"); + Print(" into already-mitigated zones. Same fix applied to OB tolerance."); + Print(" [FIX#435] D1 FVG/OB zone touch tolerance widened: 10% → 50% for D1+."); + Print(" ROOT: fvg_tolerance = zone_size * 0.10. D1 FVG = 9-20p → tolerance = 0.9-2.0p."); + Print(" D1 bar opens ONCE per day. If FVG is 3+ pips from bar open → in_fvg=false."); + Print(" This caused Candidates=0 on EVERY D1 bar despite 22-26 active FVGs present."); + Print(" Fix: _Period>=D1 uses 50% tolerance = 4.5-10p margin."); + Print(" ICT: entering FVG within 1/4 ATR of zone edge = valid. 50% zone = ~5-10p at D1."); + Print(" Same fix applied to OB touch tolerance for consistency."); + Print(" M15/H1/H4: unchanged at 10% (tight TF = exact zone reaction required)."); + Print(" [FIX#434] D1 TP clamp ceiling raised: 6.0 → 8.0×ATR (tp2:8→10, tp3:11→13)."); + Print(" ROOT: FIX#433 set tp1[4]=6.50×ATR > old ceiling 6.0 → clamped to 6.0."); + Print(" After clamp: RR=6.0/2.50=2.40 (still above mrr=1.70, not a blocker)."); + Print(" But ceiling 8.0 gives margin for vol-adjusted SL expansion without re-hitting."); + Print(" [FIX#433] EURUSD D1 calibration — index[4] ONLY. No other TF touched."); + Print(" ROOT 1: tp1[4]=5.50×ATR, sl[4]=3.00×ATR → RR=1.833."); + Print(" AutoOpt min_rr=2.25, FIX#19 clamps to 1.91. RR=1.833<1.91 → ALL rejected."); + Print(" Fix: sl[4]=2.50×ATR, tp1[4]=6.50×ATR → RR=2.60. mrr[4]=1.70."); + Print(" tp2[4]=9.00, tp3[4]=12.0. ValidatePairTFConfigs: 2.60>=1.70 OK."); + Print(" ROOT 2: min_conf default=60 → D1 raw scores avg 35-50 → 0 candidates."); + Print(" Fix: min_conf[4]=40 (same lesson as M5 FIX#357)."); + Print(" ROOT 3: min_ev default → NN not calibrated for D1 → false rejections."); + Print(" Fix: min_ev[4]=0.00 (disable, mirrors H4 FIX#359)."); + Print(" ROOT 4: spread[4]=40p → midnight spike >40p blocks analysis."); + Print(" Fix: spread[4]=50p."); + Print(" Additional: max_positions[4]=1 | tp1_pct=60/25/15 | se_minrr=1.50R"); + Print(" block_tc_choppy=1 | choppy_min_conf=50 | mtf_hard_block=1 | sl_min=30p"); + Print(" [FIX#432] Structural CT hard block in EvaluateSmartEntry Gate 2."); + Print(" ROOT (Jan 5 BUY, Jan 9 SELL): FIX#428 detected CT by structure → soft CT"); + Print(" thresholds applied (EV≥0.20R, score≥58, WP≥60%) → trades PASSED → both SL."); + Print(" WHY hard: structure (HH/HL sequence) requires multiple bars. MTF flickers."); + Print(" ICT: never sell into bullish structure. This is not a soft gate."); + Print(" Fix: structure opposes + no CHoCH → counterTrendBlocked=true immediately."); + Print(" CHoCH exception: D1 CHoCH confirms direction → structure shifting (stale)"); + Print(" → soft CT thresholds still apply (original FIX#428 behavior preserved)."); + Print(" [FIX#431] A+ exception (FIX#37/391) denied in CHOPPY/RANGING context."); + Print(" ROOT (Jan 5, Jan 9): FIX#397 CHOPPY blocks 5 OTE candidates (continue),"); + Print(" but other OTE candidates in same loop reach A+ exception → trade opens."); + Print(" FIX#397 is a hard block. A+ exception purpose = R:R relaxation only."); + Print(" OTE/TC/FVG in CHOPPY have no edge regardless of score — momentum entries"); + Print(" require momentum. Score=90+ cannot create momentum from a ranging market."); + Print(" Fix: when CHOPPY block condition is met, A+ exception denied → hard block."); + Print(" [FIX#430] CT pattern exception denied: FIX#344/396 bypass disabled for CT trades."); + Print(" ROOT (Dec 13): CT SELL score=121 + Inv H&S [score=90] → FIX#344 bypassed pattern block."); + Print(" FIX#344 premise (\"high score = all aligned\") is FALSE for CT trades."); + Print(" CT score=121 = many conflicting signals. Pattern correctly warned of wrong direction."); + Print(" Fix: FIX#344 exception disabled when trade direction opposes H4 structure."); + Print(" CHoCH override: if D1 CHoCH confirms trade direction → structure shifting → exception allowed."); + Print(" [FIX#429] H1 CT-WEAK MTF confidence gate: plain BULL/BEAR MTF requires confidence >= 65%."); + Print(" ROOT (Dec 13): MTF=BEARISH at 55% confidence triggered CT exemption (barely bearish)."); + Print(" MTF_STRONG_BULLISH/BEARISH: no threshold needed (directional strength confirmed)."); + Print(" [FIX#428] Structure-based CT detection in EvaluateSmartEntry Gate 2."); + Print(" ROOT (Dec 13): MTF=BEARISH but regime not yet flagged → isCounterTrend=false."); + Print(" Trade opened CT into live bullish structure (H4 higher highs/lows)."); + Print(" Fix: if trade direction directly opposes H4 structure → treat as CT in Gate 2."); + Print(" CHoCH override: D1 CHoCH confirms direction → structure is lagging, not opposing."); + Print(" [FIX#427] Direction-aware score cap (FIX#305 + direction)."); + Print(" ROOT (Dec 13): score_cap[H1]=95 blocked BUY score=121-174, allowed CT SELL score=90."); + Print(" CT high score = conflicting signals amplified. With-trend high score = genuine confluence."); + Print(" Fix: cap enforced ONLY for CT trades (structure opposes). With-trend: no cap."); + Print(" [FIX#426] SmartExit D1 CHoCH opposing criterion (+20pts)."); + Print(" ROOT (Dec 12): SELL open → EA detected CHoCH Bullish 1 bar later → SmartExit never saw it."); + Print(" Trade peaked at 0.98R then reversed fully (market officially bullish). Net = SL."); + Print(" Fix: if g_d1CHoCH_Valid AND CHoCH opposes open trade AND rr>0 → score +20pts."); + Print(" [FIX#425] H1 CT-WEAK structure veto (mirrors FIX#402/402b for H4)."); + Print(" ROOT (Dec 13): FIX#243 H1 exemption fired on MTF alone, ignored structure=BULLISH.");\ + Print(" Fix: if H4 structure directly opposes CT direction → veto CT exemption."); + Print(" CHoCH override: D1 CHoCH confirms direction → structure shifting → exemption valid."); + Print(" ROOT (bt6 Jan 8): Structure=BULL + OTE SELL + MTF=BEARISH → CTExempt=YES → LOSS."); + Print(" MTF flickering bearish 1-2 bars inside bullish structure = noise, not reversal."); + Print(" Fix: if H4 structure directly opposes trade (BULL+SELL or BEAR+BUY) → no CT exemption."); + Print(" ICT rule: never CT without confirmed BOS/CHoCH in trade direction."); + Print(" [FIX#403] SmartExit peak trail: closes if peak>=0.60R and retreat>=35% — no signals needed."); + Print(" ROOT (Jan 16 SELL): peakRR=0.99R, floor=1.20R → floor gate blocked Criterion 6."); + Print(" floor gate fires BEFORE peak check → peak retreat criterion = dead code for H4."); + Print(" Fix: pure price-action check BEFORE floor gate. Peak>=0.60R + retreat>=35% → close."); + Print(" Jan 16 SELL: would close ~0.64R (+$280) instead of SL (-$313) = $593 difference."); + Print(" [FIX#404] HALT daily reset: g_currentStreak reset to 0 at daily boundary."); + Print(" ROOT (kavala v10.67): FIX#398 comment says 'resets daily' but daily reset block"); + Print(" never reset g_currentStreak. Result: 4 losses → HALT permanent forever."); + Print(" Evidence: last trade Jan 16 2024, then 15 months = 0 trades (forced stop Apr 2025)."); + Print(" Fix: if g_currentStreak<0 at daily boundary → reset to 0. Win streaks preserved."); + Print(" [FIX-A] CONF_LIQ_SWEEP cascade slot 13 (0-15pts)."); + Print(" ICT: swept SSL near BUY = institutional demand absorbed. BSL near SELL = supply absorbed."); + Print(" H4 FVG disabled (FIX#203) → sweep fills the H4 confirmation gap."); + Print(" Sweep within 2×ATR of entry, within last 8 bars. Added to cascadeSweepScore."); + Print(" [FIX#405] Streak reset timing + minConf H4 fix."); + Print(" BUG#1: FIX#404 was in PerformMaintenanceTasks (OnTick) AFTER OnNewBar returns."); + Print(" First bar of each new day: FIX#398 still saw old streak → blocked → reset too late."); + Print(" BUG#2: FIX#404 used TimeCurrent() (broker time), OnNewBar uses TimeGMT() → async."); + Print(" Fix: g_currentStreak + g_lossStreakForMTF reset in OnNewBar daily block (GMT, BEFORE EA_CheckSignals)."); + Print(" BUG#3: FIX#271 set H4 cascade minRequired=2 ('FVG disabled'). FIX#322b re-enabled H4 FVG."); + Print(" Cap=2 too permissive — any 2 confirmations pass. Raised to 3 in ComputeActiveGates + SelectBestCandidate."); + Print("[LAUNCH] EA ANGEL " + EA_VERSION + " - STARTING | Scenario System Active | 2026.04.01"); + Print(" [FIX#343]: M15 CT-WEAK exemption: STRONG-only -> BULLISH||STRONG (mirrors H1 FIX#243)"); + Print(" [FIX#344]: Pattern gate exception: score>=90 & regime!=CHOPPY bypasses FIX#99a block"); + Print(" [FIX#345]: req_trend M15+M5 forced OFF (was hard block, undo CT trades after FIX#343)"); + Print(" [FIX#346]: AutoOpt quality cap now pair-table-aware: cap=min(pairBase+15,85) not flat 72"); + Print(" [FIX#348]: g_workingMinEntryQuality assigned AFTER FIX#346 cap (was before = got uncapped value)"); + Print(" [FIX#349]: FIX#60 SmartEntry TrendBypass REMOVED. Redundant+harmful: WP<50% EV<0 CT trades entered. Covered by FIX#176+225+343+232."); + Print(" [FIX#350]: M5 score_cap 78→90. 705 signals blocked (avg 85.5), window 75-77=0 trades. 10-sample cap replaced by 959-trade reality."); + Print(" [FIX#351]: M5 SL 1.50→1.80×ATR. Spread=2p was 44%% of SL room at 1.50. 1.80 gives real noise buffer."); + Print(" [FIX#352]: M5 TP1/TP2/TP3 2.50/3.20/4.00→3.50/4.50/6.00. mrr 1.50→1.80. WR=29%% needs RR>2.43."); + Print(" [FIX#353]: M15 mrr 1.50→1.80. WR=24%% needs RR>3.17. tp_pct set 60/25/15 (secure profit fast)."); + Print(" [FIX#354]: M5 tp1 3.50→4.50 ROOT CAUSE: SL×volMult > tp1/mrr in every vol regime"); + Print(" [FIX#355]: FULL TP/MRR RECAL: M5 tp1=5.00/mrr=1.80 M15 tp1=6.00/mrr=1.50 H1 tp1=5.50/mrr=1.30"); + Print(" [FIX#356]: TP CLAMP RAISED: M5 3.0→6.0 M15 3.0→7.0 H1 3.5→6.5. ROOT: hardcoded clamp < pair table TP → RR>44). 98 low-RR trades now blocked."); + Print(" [FIX#362]: Double regime dedup in AddCandidate H4+. Root: regimeBonus(18-20) + CONF_REGIME cascade(10) = same TRENDING signal twice."); + Print(" DATA: 5-conf+TRENDING = WR21%,-$713. Fix: subtract CONF_REGIME pts from regimeBonus on H4+."); + Print(" [FIX#363]: OB signal disable per TF via pair table allow_ob[5]. EURUSD H4: -1 (OB 7W/16L WR=30%,-$905 — structural trap zones)."); + Print(" [FIX#365]: H4+ perTrade_BE_RR multiplier 0.50→0.40. Root: BE=1.0R missed 3 near-miss SELL (peak 0.83-0.99R → SL). Now 0.80R catches them."); + Print(" [FIX#366]: TC H4+ restricted to STRONG_TREND (REGIME_TRENDING removed). Root: TRENDING=moderate ADX, 13W/19L WR=41% in survivors."); + Print(" [FIX#367]: D1 CHoCH lookback 3→2 (6-day→4-day lag). Lower TFs and other pairs unaffected."); + Print(" [FIX#368]: EV formula: g_workingTP1_Pct instead of EA_TP1_Percent. Root: w1=25% not 50% -> EV=-0.13R blocked ~2000 valid trades."); + Print(" [FIX#369]: UNIFIED SCORING SYSTEM - ComputeUnifiedScore() - 14 modules - single source for ALL 3 gates."); + Print(" Replaced: CalculateEntryScore 5-arg (signals), EvaluateSmartEntry inline, dead 1-arg overload."); + Print(" [FIX#370]: EvaluateSmartEntry wrapper uses ComputeUnifiedScore. Gates retained. Score drift eliminated."); + Print(" [FIX#371]: min_ev=0.00 propagation fixed. cfg.min_ev>=0 always applied; <0=sentinel for category default."); + Print(" [FIX#372]: BOS_RETEST disabled EURUSD H4. Post-filter: 1W/8L WR=11%,-$283. Gate in BOTH paths: CheckBOSRetestSignal AND EA_CheckSignals section 10."); + Print(" [FIX#373]: MEAN_REV mode — new TECH_MEAN_REV=11 technique for CHOPPY/RANGING regimes."); + Print(" Section 11 in EA_CheckSignals: RSI extreme + range boundary + liquidity sweep."); + Print(" TP1=midpoint (50%), TP2=opposite extreme. SL=beyond extreme+0.4ATR."); + Print(" AdverseClose: ADX regime-change exit if market breaks out of range mid-trade."); + Print(" EURUSD H4: mr_enabled=1, RSI 32/68, SL×0.4ATR, min range 1.8×ATR."); + Print(" [FIX#373v2]: MEAN_REV → ICT SWEEP-FIRST ARCHITECTURE."); + Print(" PRIMARY TRIGGER: BSL/SSL liquidity sweep outside range + bar closes back inside."); + Print(" RSI extreme demoted to +10pts confirmation (was mandatory gate → 0 trades)."); + Print(" Score cap stays at 25. Root cause of dead signal was RSI mandatory gate (now removed)."); + Print(" mrScore=25 → composite≈123 → passes gate=44, competes fairly vs OB(116)/FVG(97)."); + Print(" Sweep recency gate: sweepTime >= bar[-3] (stale sweeps ignored)."); + Print(" SL anchored to sweepPrice (actual liquidity depth, not rangeHigh/Low)."); + Print(" [FIX#374]: CT EV ADX-continuous scaling in EvaluateSmartEntry."); + Print(" minEVForCounter *= (1 + clamp((ADX-25)/75, 0,1) × 2.5)."); + Print(" ADX=25: ×1.0 (unchanged) ADX=50: ×1.33 ADX=84: ×2.12."); + Print(" Jan 2024 case: ADX=84 → minEV 0.08→0.17R blocks low-quality CT entries."); + Print(" [FIX#375]: CT hard gate when ADX>50 + no recent sweep near entry."); + Print(" ICT: institutional entries against strong trend require sweep evidence."); + Print(" ADX>50 AND no swept LIQ within 1.5×ATR of entry in last 5 bars → BLOCK."); + Print(" [FIX#376]: PosSize Step3 DD uses g_currentDailyDD (was local floating-only formula)."); + Print(" OLD: (balance-equity)/balance = 0% after realized loss with no open trades."); + Print(" NEW: g_currentDailyDD = CalculateDailyDrawdown() = both realized+floating."); + Print(" Consistent with FTMO monitoring and EA_MaxDailyDrawdownPercent gate."); + Print(" [FIX#377]: tp1/2/3Lots guard: g_workingTP_Pct=0 fallback to EA_TP_Percent inputs."); + Print(" Prevents 0-lot split → minLot floor → wrong position ratios on TP legs."); + Print(" [FIX#373]: AdverseClose 3-part fix (H4+ early close bug)."); + Print(" 1) barsAlive guard H4+: <2 (was <1 — MT5 Bars includes both endpoints, fired on entry bar)."); + Print(" 2) Pre-existing adverse signals discounted: regime/struct at entry = 4pts, new flip = 8pts."); + Print(" 3) Zone-B threshold H4+: 23 for ALL trades (was 15 for normal — regime+struct=16 fired instantly)."); + Print(" FIX#243: H1 CT-WEAK exemption added (mirrors H4 FIX#225) | WEAK+MTF_BEAR -> H1 SELL ALLOWED"); + Print(" FIX#343: M15 CT-WEAK exemption extended to BULLISH||STRONG (was STRONG-only) | min_conf[1]:78->60 | min_conf[2]:65->50"); + Print(" ROOT CAUSE: H1 had no exemption -> 787 FIX#233 0-candidate bars in backtest"); + Print(" OB/OTE SELL Score=77-103 with MTF=BEAR all blocked in WEAK UPTREND on H1"); + Print(" FIX#244: slDist recalculated after FIX#188 ATR fallback (68 NOISE REJECT $0.00 fixed)"); + Print(" ROOT CAUSE: slDist=0 before FIX#188 -> NOISE REJECT fired on $0.00<$0.00"); + Print(" Fix: slDist = MathAbs(cand.SL - entry) after FIX#188 block"); + Print(" FIX#245: Step10 tp_rr uses TF-scaled value as floor (not raw InpTP1_RR)"); + Print(" ROOT CAUSE: H1 tp1_rr *1.35 from Step2 lost when Step10 used MathMax(InpTP1_RR, derived)"); + Print(" Fix: MathMax(g_autoOptParams.tp1_rr, derived) -- Step10 can only widen"); + Print(" FIX#246: Step10 preserves Steps 3-5 session/vol risk adjustments (not reset to EA_RiskPercent)"); + Print(" ROOT CAUSE: risk_pct=EA_RiskPercent in Step10 wiped vol(*0.80)+session(*0.76)"); + Print(" FIX#221 autoOptFactor was always 1.0 -> Asian session full-risk trades"); + Print(" FIX#247: EURUSD H4 mrr 1.80->1.30 (BOS_RETEST effective R:R=1.30 was always rejected)"); + Print(" FIX#250: GBPUSD H4 tp1 3.80->4.00 + mrr 1.65->1.50 (4.7% margin was critical, OB R:R 1.55-1.65)"); + Print(" FIX#251: AUDUSD/NZDUSD H4 mrr 1.80->1.45 (ZERO MARGIN: tp1/sl=1.80=mrr, FIX#19 clamp to 1.53, all rejected)"); + Print(" FIX#252: USDJPY H4 mrr 1.80->1.45 (same ZERO MARGIN as AUDUSD)"); + Print(" FIX#253: USDCAD H4 mrr 1.80->1.45"); + Print(" FIX#254: USDCHF H4 mrr 1.80->1.45"); + Print(" FIX#255: Crosses H4 mrr 1.80->1.60 (1% margin, FIX#19 clamp to 1.55)"); + Print(" FIX#256: US500/US100 H4 mrr 1.80->1.55 (tpMult=1.1 gives 1.98, regime mults reduce to 1.55-1.65)"); + Print(" FIX#257: BTCUSD/ETHUSD H4 mrr 1.35->1.25 (Crypto OB R:R naturally 1.25-1.35)"); + Print(" FIX#258: NATGAS/USOIL H4 mrr 1.80->1.60 (no tpMult bonus, 2.2% margin always clipped by spread)"); + Print(" FIX#259: FIX#165 H4 confluence cap 0.75->0.65 (OB-only TF, OB confluence naturally 0.60-0.70)"); + Print(" FIX#260: SelectBestCandidate H4 minScore cap 45->42 (OB-only: score 40-44 setups valid with R:R)"); + Print(" FIX#261: WEAK regime H4 minRR *0.85 case added (H4 in WEAK weeks, no case = all candidates rejected)"); + Print(" FIX#262: OB strength cap H4 0.65->0.58 H1 0.68->0.62 (H4 OBs natural strength 0.55-0.65, Conf=2 diagnostic = all rejected here)"); + Print(" FIX#263: SmartEntry TF-aware effectiveMinConf cap: H4=42 H1=48 M5=46 (was uncapped = 60->70 threshold, blocked ALL H4 OB scores 35-50)"); + Print(" FIX#264: MeetsMinimumEntryScore H4 cap 45->42 (sync with FIX#260/263, all 3 gates now consistent)"); + Print(" FIX#265: H4 counter-structure counterFloor 70->50 (Structure=BULL but SELL valid when D1/MTF bear, FIX#232 already vetted CT-WEAK)"); + Print(" FIX#266: SmartEntry FIX#263 cap now uses pair table min_conf (H4 EURUSD=36) not hardcoded 42. All 3 gates (Select/MeetsMin/SmartEntry) consistent."); + Print(" FIX#267: H4+ counter-structure penalty=0 (was +8-10). FIX#232+225 already vetted CT direction. No double-penalty on H4."); + Print(" FIX#268: H4+ counterFloor from pair table (EURUSD=36) not hardcoded 50. score=40>=36 PASSES (same as SelectBestCandidate). H4 OB deadlock resolved."); + Print(" FIX#269: SelectBestCandidate absolute floor 40->36 for H4+ (pair table min_conf=36 is real floor, 40 blocked score=36-39)"); + Print(" FIX#270: SelectBestCandidate TF cap from pair table min_entry_quality (H4=36) not hardcoded 42. Sync with FIX#266/272."); + Print(" FIX#271: EvaluateCascadeConfirmations H4 minRequired cap 3->2 (FVG disabled H4 = Structure+KZ = max 2, cap=3 was impossible)"); + Print(" FIX#272: MeetsMinimumEntryScore H4 cap from pair table (36) not hardcoded 42. All 3 gates now identical threshold source."); + Print(" FIX#273: pairThresholds.minConfirmations Major=4 preserved (M15 value). H4 capped to 2 by FIX#271 in cascade."); + Print(" FIX#274: Dashboard _dash_minScore H4 cap from pair table (36) not hardcoded 45. Visual consistency."); + Print(" FIX#275: ComputeActiveGates() single-source refactor. g_gates struct. All 4 gates read same values."); + Print(" MeetsMin/SelectBest/SmartEntry/Cascade all read g_gates.minScore+minConf+counterFloor."); + Print(" FIX#276: HTF gate (FIX#16c) + weak penalty (FIX#56) added to g_gates."); + Print(" htfMinScoreNeutral=minScore(36) htfMinScoreOpposed=minScore+5(41) weakPenalty=5(H4)/15(M15)"); + Print(" H4 SELL MTF=NEUTRAL score=38: was 38<40=BLOCKED, now 38>=36=PASSES"); + Print(" [FIX#378-382]: Market Context Engine + Smart Exit 2.0 active."); + Print(" FIX#378: ComputeMarketContext() gates direction + technique multipliers per bar."); + Print(" FIX#379: ApplyContextMultiplierToCandidate() boosts/reduces technique scores."); + Print(" FIX#380: UpgradeCandidateTPsToStructural() replaces ATR TPs with structure levels."); + Print(" FIX#381: SmartExit_ZoneAware() closes if entry zone (OB/FVG/BOS) is invalidated."); + Print(" FIX#382: SmartExit_StructureTrail() trails SL behind swing lows/highs after TP1."); + Print(" [FIX#277]: H4 AutoOpt breakeven_rr floor 0.35R->0.50R (fallback path safety, perTrade 0.95R still dominates)."); + Print(" FIX#278: EA_TP2_BE_Threshold routed through g_workingBE_TP2_RR (AutoOpt-aware)."); + Print(" g_workingBE_TP2_RR = MAX(input=2.5, tp2_rr*0.85). TF scaling applied on top."); + Print(" FIX#279: EURUSD-ONLY min_conf 36->34 (GetPairTFConfig EURUSD). BOS_RETEST score=35 miss by 1pt resolved."); + Print(" FIX#280: FIX#104a div block H4+ threshold 65->80 (ALL H4+). Confirmed divs still block any strength."); + Print(" 02.12 bearish div strength=65 blocked 3-5 BUY candidates incorrectly on H4 swing TF."); + Print(" FIX#281: FIX#130 pattern block cap 72h->24h for H4 (ALL H4+). Inv H&S blocked SELL 3 days."); + Print(" FIX#282: EURUSD-ONLY min_ev=0.05 via cfg.min_ev->g_gates.minEV->effectiveMinEV."); + Print(" TC Score=70 WinP=57.7% EV=0.07R: was BLOCKED (0.07<0.08). Now 0.07>=0.05 PASSES."); + Print(" ROOT CAUSE: VOL_EXTREME+WEAK_DOWN regime mults reduce BOS_RETEST R:R to 1.30-1.36"); + Print(" Old mrr=1.80 blocked ALL H4 BOS_RETEST candidates. Same fix as FIX#218 for Gold."); + Print(" FIX#248: TF-aware divergence expiry (H4: 20 candles->5, H1: 10, D1: 3)"); + Print(" ROOT CAUSE: H4 expiry=3.3 days -> HIDDEN BULL active 4+ days (stale divergence)"); + Print(" H4 divergence resolves in 1 trading day (5 bars=20h). 20 candles=too long."); + Print(" FIX#249: SelectBestCandidate minConf synced with FIX#170 TF cap for H4+"); + Print(" ROOT CAUSE: cascade used cap=3 (FIX#170) but SelectBestCandidate showed 'need 4'"); + Print(" Fix: Apply same MathMin(minConf,3) for H4+ in SelectBestCandidate"); + Print(" FIX#138: atr_at_entry + sl_dist_cached fields in MultiTPEntry struct"); + Print(" FIX#139: Trail distance uses cached ATR (not live) + capped at 50% SL dist"); + Print(" FIX#140: Post-clamp actual R:R validation -- reject if TP1 < SL after clamp"); + Print(" FIX#141: SL max width check -- M5 max 2.5xATR, M15 max 2.8xATR before entry"); + Print(" FIX#142: TP proportional to actual SL (TP1 >= sl_dist x MinRR floor)"); + Print(" FIX#143: FIX41 LOCK3 uses atr_at_entry + trail capped at 50% SL dist"); + Print(" FIX#144: SmartExit RSI uses g_cachedRSI (not new iRSI handle every call)"); + Print(" FIX#145: FVG/OB SL capped at 2.0xATR at source + FIX#141 backstop at 2.5xATR"); + Print(" FIX#146: AutoOpt_MaxRiskOverride is HARD CAP for ALL PosSize/Kelly paths"); + Print(" FIX-A: Score cap 72/85 (was cumulative += -> threshold > max = zero trades)"); + Print(" FIX-B: UpdateAutoOpt on every OnNewBar (backtest: OnTimer fires 0-2x total)"); + Print(" FIX-C: Skip repeated failed SL in ProfitGuardTrail (MODIFY FAILED loop fix)"); + Print(" FIX-D: TF-aware minScore cap (H4<=45, H1<=50, M5<=48 -- EA_MinEntryScore=60 blocks H4)"); + Print(" FIX-E: H4+ exempt from FIX#55 counter-trend block (WEAK lasts weeks on H4 = zero trades)"); + Print(" FIX#156: MeetsMinimumEntryScore TF-aware (was EA_MinEntryScore=60 hardcoded, blocked ALL H4 FVG/OB)"); + Print(" FIX#157: M15 BreakEven floor 0.35->1.80R (was 1.30R from tp1_rr*0.65, Excel target=1.8R)"); + Print(" FIX#158: rrScore TF+pair-aware (H4 sweet spot 1.8-3.5R | pip penalty->ATR-mult penalty)"); + Print(" FIX#159: CRITICAL BuildCandidateSLTP FIX#15g TP clamp synced with FIX#153:"); + Print(" H4 maxTP1 1.5->4.5xATR, D1 1.2->6.0, H1 2.0->3.5, M15 2.5->3.0."); + Print(" ROOT CAUSE of 0-1 H4 trades: cand.rr=0.75 < minRR=1.80 (clamped before R:R check)."); + Print(" FIX#162a: FIX#15d skipped when pair profile loaded (was overwriting correct pair+TF TP values)."); + Print(" FIX#162b: R:R safety floor in FVG/OB/TC candidate builder now uses GetActiveMinRR() not EA_MinRR."); + Print(" ROOT CAUSE of H4 OB=1.45 R:R: SL=2.20xATR, TP1=3.20xATR -> R:R=1.455 < minRR=1.80."); + Print(" Floor was EA_MinRR=1.3 -> expanded TP to 1.3xSL, still < 1.80 -> REJECT."); + Print(" TP values with generic 1.5xATR -> R:R=1.45 -> all H4 candidates REJECT)."); + Print(" Pair profile is now the authority for TP per pair+TF. Manual mode still clamped."); + Print(" FIX#160a: Dashboard Entry Score Panel: TF-aware PASS/FAIL threshold + correct label."); + Print(" FIX#160b: Dashboard MTF label now dynamic per TF (M5->MTF M15, H4->MTF D1, etc.)."); + Print(" FIX#160c: AutoOpt Panel MinScore: shows both stored and effective (TF-capped) values."); + Print(" FIX-B: Counter-trend WinP EV-adaptive (55-62% based on EV)"); + Print(" FIX#124: ApplyPairTFProfile symbol stripping (dot-strip before 6-char cap)"); + Print(" FIX#124b: UNKNOWN block -> one-time log + category fallback (no more spam)"); + Print(" FIX#124c: NATGAS/NGAS/USOIL/BRENT added to pair profile table"); + Print(" FIX#125: H&S STALE spam -> dedup per headTime (was 200+ lines/bar)"); + Print(" FIX#126: Trendline angle ATR-normalized (was ~89 for all index symbols)"); + Print(" FIX#127/129: Stale pattern high-score override, trail RR calculation"); + Print(" FIX#128/130/131: FIX#99a spam dedup, 72h SELL block cap, phantom Multi-TP"); + Print(" FIX#132: Regime throttle per-regime counter (choppy trades no longer blocked by trend count)"); + Print(" FIX#133: Per-category MinEV gate (Major=0.08R..Index=0.15R, was flat 0.25R)"); + Print(" FIX#134: Counter-trend EV 0.45->0.20R Forex / 0.60->0.35R Index"); + Print(" FIX#136: EnableML default=false (H4 NN underfitted on limited data)"); + Print(" FIX#135a: ProfitLock_BE lockDist=riskDist->bufferDist (was instant SL at BE threshold)"); + Print(" FIX#135b: FIX#130 bypass log dedup (was N times/bar per candidate)"); + Print(" FIX#137: GBPUSD M5 scalping dead zone fixed:"); + Print(" (A) VOL_EXTREME TF_CAT_SCALP: quality gate +25 instead of allow_scalping=false"); + Print(" (B) spread_ratio>2.0/3.0 TF_CAT_SCALP: quality gate instead of hard block"); + Print(" (C) FIX#73 TC WEAK threshold: 110->105 (Score=108 was blocked unfairly)"); + Print(" (D) AutoOpt_MinScoreFloor: 50->40 (FVG/OB 46-52/85 now allowed)"); + Print(" FIX-C1: Trail label ACTIVE (was misleadingly 'DISABLED')"); + Print(" FIX-C2: LOCK3 ATR-based + regime-adaptive trail"); + Print(" FIX-C3: SmartExit multi-criterion (RSI+OB+divergence+age)"); + Print(" FIX-D: R:R Score uses actual expectedRR not raw multiplier"); + Print(" FIX-E: AutoOpt_RecalcMinutes 1 = auto (1 candle per TF: M5=5m M15=15m H1=60m H4=240m D1=1440m)"); + Print(" FIX-F: EvaluateSmartEntry grades recalibrated for real max~153"); + Print(" FIX-G: WinProbability normalizes score to 0-85 range"); + Print(" FIX#155: SmartExit original_sl -- entry-price fallback after TP1 partial close (runner ticket mismatch fix)"); + Print(" FIX#156: Single-position filter extended to REGIME_TREND_UP/DOWN (was only WEAK/CHOPPY/VOLATILE)"); + Print(" FIX#157: Freeze level guard before trail modify -- prevents MODIFY FAILED 10016 spam"); + Print(" FIX#158: TP-proportional real-time trail -- trail_dist = fraction of entry->TP1/TP2/TP3 distance; CHoCH tightens to 0.5xATR"); + Print(" FIX#163/172: GetPairTFMinRR implemented -- unified min_rr table ALL pairs x ALL TFs (M1/M5/M15/H1/H4/D1)"); + Print(" FIX#164: AutoOpt/Manual clean separation -- AutoOpt=false uses EA inputs only, no profile override"); + Print(" FIX#165: Confluence cascade cap (H4:0.75 H1:0.77 M15:0.79) + OB decouple (H4:0.65)"); + Print(" FIX#166: H4+ intra-bar zone detection -- CheckSignals fires once per bar on FVG/OB touch"); + Print(" FIX#167: FIX#56 TF-aware (H4: score+5 not +15, R:R x1.00 not x1.05)"); + Print(" FIX#168: FIX#100 always log BE/Trail/SE thresholds (removed EnableDebugMode gate)"); + Print(" FIX#169: ALL pattern types set strongestPatternTime (MTB/Tri/FP/Wedge/Diamond/V)"); + Print(" -> FIX#108 stale check now works for Double Bottom (was only H&S)"); + Print(" FIX#170: minRequired confirmations TF-aware (D1:2 H4:3 H1+: profile value)"); + Print(" -> SELL Score=60/85 RR=1.84 Quality=89% no longer blocked by '3 conf need 4'"); + Print(" FIX#171: FIX#165 once-per-bar log (was every 5s via OnTimer = 6383 prints/bar spam)"); + Print(" FIX#173: FVG/OB zone-aware SL minimum (was 2.20xATR=99p, now 0.35xATR noise floor on H4)"); + Print(" -> FVG structural SL preserved (ICT invalidation) instead of exploding to 99p"); + Print(" FIX#174: Remove min_rr *1.10 double TF scaling on H4 (FIX#172 already TF-aware)"); + Print(" -> min_rr stays 1.80 (GetPairTFMinRR) not 1.98 (*1.10 was double-dipping)"); + Print(" FIX#176: H4+ direction filter -- STRONG MTF no longer overrides regime blocks on H4+"); + Print(" EvaluateSmartEntry: mtfOverridesRegime=false when regime opposes trade on H4+"); + Print(" SelectBestCandidate: FIX-E replaced -- CT-WEAK needs STRONG MTF, not blanket exempt"); + Print(" Symmetric: BUY in WEAK_DOWN needs STRONG_BULL, SELL in WEAK_UP needs STRONG_BEAR"); + Print(" FIX#175: FIX#105c ELITE threshold 105->130 for H4+ counter-trend TC in WEAK regimes"); + Print(" -> H4 TC BUY in WEAK DOWNTREND needs score>=130 (was 105, too permissive)"); + Print(" ========================================"); + Print(" v10.69 FIXES:"); + Print(" FIX#DETECT-LIMIT: detectionLimit hardcoded 200 -> MathMax(g_workingFVG_MaxAge,200)"); + Print(" ROOT: H4 g_workingFVG_MaxAge=720 but scanner capped at 200 bars = FVG=0 every bar"); + Print(" FIX#FVGCOUNT: g_fvgCount synced via CountActiveFVGs() after UpdateFVGStatus()"); + Print(" ROOT: g_fvgCount=ArraySize (includes EXPIRED/FILLED) -> stale count"); + Print(" FIX#HOLIDAY: Low-liquidity filter added: Jan 1-3 (New Year), Dec 24-26 (Christmas)"); + Print(" ROOT: Jan 2 2024 loss -$498 (3 positions, WinRate=0%) on post-NYE session"); + Print(" FIX#402b: structureOpposes veto now respects D1 CHoCH confirmation"); + Print(" ROOT: valid CT trades blocked when D1 CHoCH confirmed structure shift"); + Print(" but g_isBullishStructure stale 1-2 bars -> CHoCH trades zero'd out"); + Print(" FIX#FVGAge-default: FVG_MaxAge default 60->300 (was M5 era, now H4-appropriate)"); + Print(" ========================================"); + Print(" v10.70 FIXES:"); + Print(" FIX#405: Streak reset timing — moved to OnNewBar GMT daily block (before EA_CheckSignals)."); + Print(" Was in PerformMaintenanceTasks (OnTick, runs AFTER OnNewBar) → first bar always blocked."); + Print(" Also fixed time source: FIX#404 used TimeCurrent() vs OnNewBar TimeGMT() → 2h async."); + Print(" FIX#405: H4 cascade minRequired raised 2→3 (FIX#322b re-enabled FVG → 3 confirmations available)."); + Print(" Cap=2 after FVG re-enable was too permissive. Updated in ComputeActiveGates + SelectBestCandidate."); + Print(" [FIX#406] D1 CHoCH stale detection — multi-factor exception replaces score-only FIX#329."); + Print(" PROBLEM: score>=85 fired on 100% of H4 trades (all score 88-144) → D1 gate disabled."); + Print(" ANALYSIS: Dec2023 D1=BEAR+H4=BULL+MTF=BULL → price rallied +250p → FIX#329 CORRECT."); + Print(" Jan2024 D1=BEAR+H4=BEAR+MTF=BEAR → price fell -220p → FIX#329 WRONG."); + Print(" FIX: exception only when H4 Structure AND MTF BOTH oppose D1 (D1 genuinely stale)."); + Print(" If H4+MTF confirm D1 → gate holds firm. If H4+MTF oppose D1 → D1 lagging → allow."); + Print(" Covers all market states: trend, reversal, continuation, divergence between TFs."); + Print("========================================"); + Print("==========================================================="); + // =============================================================== + // [LOCK] TERMINAL & ENVIRONMENT CHECKS (NEW) + // =============================================================== + if(!TerminalInfoInteger(TERMINAL_CONNECTED)) + { + Print("[WARN] WARNING: Terminal not connected to server"); + // Continue anyway - will connect later + } + if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) + { + Print("[i] INFO: Trading not allowed in terminal settings"); + Print(" This is OK for indicator-only mode"); + } + // Check if running in Strategy Tester + bool isTesting = MQLInfoInteger(MQL_TESTER); + bool isOptimization = MQLInfoInteger(MQL_OPTIMIZATION); + bool isVisualMode = MQLInfoInteger(MQL_VISUAL_MODE); + // =============================================================== + // * FIX#469: LIVE-READY AUTO-CONFIGURATION + // When running live (not in tester), automatically apply settings + // that are safe and performant for real trading: + // 1. Disable verbose logs — they cause latency and expose logic + // 2. Set trade deviation — allows execution within 3 pip slippage + // 3. Disable async mode — prevents parallel order conflicts on multi-instance + // These override user inputs silently in live mode. + // In tester: user inputs apply as normal (for debugging). + // =============================================================== + if(!isTesting) + { + // Force-disable all verbose logging for live performance + // (AutoOpt_ShowLog and EnableDebugMode are inputs — we shadow them via globals) + // Note: we cannot reassign input vars in MQL5, so we use the working globals + // that all runtime code reads. The inputs remain as user-visible UI only. + g_liveMode = true; + Print(StringFormat("[LIVE] EA ANGEL %s running LIVE on %s %s — verbose logs suppressed", + EA_VERSION, _Symbol, EnumToString(_Period))); + Print(StringFormat("[LIVE] Instance: Magic=%d | Pair=%s | TF=%s", + EA_MagicNumber, _Symbol, EnumToString(_Period))); + } + else + { + g_liveMode = false; + // * FIX#469: Enable verbose logging only in standard backtest (not optimization, not live) + // Optimization runs are silent by design — log floods would fill disk and slow all cores. + g_verboseLog = !isOptimization && (AutoOpt_ShowLog || EnableDebugMode); + Print("[CHART] Running in Strategy Tester"); + if(isOptimization) Print(" Mode: Optimization"); + else if(isVisualMode) Print(" Mode: Visual Testing"); + else Print(" Mode: Standard Backtest"); + } + // * v9.31 FIX#113: Multi-TF optimizer TF filter + if(MQLInfoInteger(MQL_OPTIMIZATION) && Opt_Timeframe != PERIOD_CURRENT) + { + if(_Period != Opt_Timeframe) + return(INIT_PARAMETERS_INCORRECT); // Silent skip -- wrong TF for this pass + PrintFormat("[FIX#113] Multi-TF pass accepted: %s %s", _Symbol, EnumToString(_Period)); + } + // =============================================================== + // [CHART] BASIC INDICATOR SETUP - DISABLED (EA Mode) + // =============================================================== + // SetIndexBuffer(0, MABuffer, INDICATOR_DATA); + ArraySetAsSeries(MABuffer, true); + // IndicatorSetInteger(INDICATOR_DIGITS, _Digits); + // IndicatorSetString(INDICATOR_SHORTNAME, "ICT Professional v6.41"); + // PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_LINE); + // PlotIndexSetInteger(0, PLOT_LINE_COLOR, clrGold); + // PlotIndexSetInteger(0, PLOT_LINE_WIDTH, 2); + // PlotIndexSetString(0, PLOT_LABEL, "ICT MA"); + // PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, MAPeriod); + // =============================================================== + // [SEARCH] SYMBOL INFORMATION + // =============================================================== + g_digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); + g_point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); + g_pipValue = (g_digits == 3 || g_digits == 5) ? g_point * 10 : g_point; + g_tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); + g_tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); + g_minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); + g_maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); + g_lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); + // Validate symbol info + if(g_point <= 0 || g_tickSize <= 0 || g_minLot <= 0) + { + Print("[X] ERROR: Invalid symbol information!"); + Print(" Point: ", g_point, " TickSize: ", g_tickSize, " MinLot: ", g_minLot); + return INIT_FAILED; + } + Print("[CHART] Symbol Info:"); + Print(" Symbol: ", _Symbol); + Print(" Digits: ", g_digits); + Print(" Point: ", DoubleToString(g_point, g_digits)); + Print(" Pip Value: ", DoubleToString(g_pipValue, g_digits)); + Print(" Tick Value: ", DoubleToString(g_tickValue, 4)); + Print(" Min Lot: ", DoubleToString(g_minLot, 2)); + Print(" Max Lot: ", DoubleToString(g_maxLot, 2)); + Print(" Lot Step: ", DoubleToString(g_lotStep, 2)); + // =============================================================== + // [OK] INPUT VALIDATION + // =============================================================== + if(!ValidateInputs()) + { + Print("[X] ERROR: Invalid input parameters!"); + return INIT_PARAMETERS_INCORRECT; + } + Print("[OK] Input parameters validated"); + // Initialize debug mode from input + g_debugMode = EnableDebugMode; + if(g_debugMode) Print("[BUG] Debug Mode: ENABLED"); + // =============================================================== + // [PKG] INITIALIZE ALL ARRAYS + // =============================================================== + if(!InitializeAllArrays()) + { + Print("[X] ERROR: Failed to initialize arrays!"); + return INIT_FAILED; + } + Print("[OK] Arrays initialized"); + // =============================================================== + // [NEW] INITIALIZE ADVANCED MODULE ARRAYS (NEW) + // =============================================================== + if(!InitializeAdvancedModuleArrays()) + { + Print("[WARN] WARNING: Some advanced arrays failed to initialize"); + // Continue - non-critical + } + // =============================================================== + // [UP] CREATE INDICATOR HANDLES + // =============================================================== + g_maHandle = iMA(_Symbol, _Period, MAPeriod, 0, MAMethod, MAPrice); + if(g_maHandle == INVALID_HANDLE) + { + Print("[X] Failed to create MA handle. Error: ", GetLastError()); + return INIT_FAILED; + } + // * FIX#466: ATR handle ALWAYS created — it is core infrastructure, not an optional feature. + // BEFORE: guarded by (UseATRFilter || EnableRiskMgmt || EnableSignals). + // ROOT CAUSE: If all three are false, g_atrHandle = INVALID_HANDLE → g_cachedATR = 0. + // Consequence: FIX#141 SL gate computes ratio = sl_width / 0 → NaN or ∞ → + // ALWAYS rejects OR NEVER rejects. AutoOpt, SmartExit, ManagePositions, AddCandidate + // all use g_cachedATR for critical decisions. A zero ATR silently breaks all of them. + // Fix: create ATR handle unconditionally. Feature flags still gate their specific logic, + // but the ATR value is always available as shared infrastructure. + { + g_atrHandle = iATR(_Symbol, _Period, 14); + if(g_atrHandle == INVALID_HANDLE) + { + Print("[X] Failed to create ATR handle. Error: ", GetLastError()); + return INIT_FAILED; + } + } + if(UseRSIFilter) + { + g_rsiHandle = iRSI(_Symbol, _Period, RSIPeriod, PRICE_CLOSE); + if(g_rsiHandle == INVALID_HANDLE) + { + Print("[X] Failed to create RSI handle. Error: ", GetLastError()); + return INIT_FAILED; + } + } + // * v7.8: TC EMA handles + g_tcEnabled = EnableTrendCont; // * v7.8: copy input to mutable working var + if(g_tcEnabled) + { + g_tcEmaFastHandle = iMA(_Symbol, _Period, TC_EMA_Fast, 0, MODE_EMA, PRICE_CLOSE); + g_tcEmaSlowHandle = iMA(_Symbol, _Period, TC_EMA_Slow, 0, MODE_EMA, PRICE_CLOSE); + g_tcRSIHandle = iRSI(_Symbol, _Period, TC_RSI_Period, PRICE_CLOSE); // * v9.16 FIX#48: TC-specific RSI (was using main RSI handle with RSIPeriod) + if(g_tcEmaFastHandle == INVALID_HANDLE || g_tcEmaSlowHandle == INVALID_HANDLE) + { + Print("* v7.8 WARNING: Failed to create TC EMA handles - TrendCont disabled"); + g_tcEnabled = false; // modify working var, NOT input + } + } + Print("[OK] Indicator handles created"); + // =============================================================== + // [FIX] INITIALIZE WORKING VARIABLES + // =============================================================== + InitializeWorkingVariables(); + // =============================================================== + // [GEAR] TIMEFRAME ADAPTATION + // =============================================================== + AdaptParametersToTimeframe(); + // =============================================================== + // [NEW] SYMBOL AUTO-DETECTION & OPTIMIZATION (v6.0) + // =============================================================== + if(PAIR_OptimizationEnabled && PAIR_AutoDetect) + { + AutoDetectSymbolAndOptimize(); + } + else + { + Print("[i] Symbol Auto-Optimization: DISABLED in settings"); + } + // =============================================================== + // [NEW] INITIALIZE KILLZONES (v5.0) + // =============================================================== + if(EnableKillzones) + { + InitializeKillzones(); + InitializeDSTInfo(); + Print("[OK] ICT Killzones initialized with DST support"); + } + // =============================================================== + // [NEW] INITIALIZE NEURAL NETWORK (v5.0) + // =============================================================== + if(EnableML) + { + if(!InitializeNeuralNetwork()) + { + Print("[WARN] WARNING: Neural Network initialization failed"); + Print(" ML features will use fallback model"); + } + else + { + Print("[OK] Neural Network initialized"); + PrintNNArchitecture(); + // Try to load pre-trained model + if(ML_LoadModel && LoadNeuralNetwork()) + { + Print("[OK] Pre-trained model loaded"); + g_nnTrained = true; + } + } + if(ShowPricePrediction) + { + InitializePredictionBuffers(); + } + if(ShowProbabilityHeatmap) + { + InitializeHeatmap(); + } + } + // =============================================================== + // [NEW] INITIALIZE PERSISTENCE (v5.0) + // =============================================================== + if(EnableDataPersistence) + { + if(!InitializePersistence()) + { + Print("[WARN] WARNING: Persistence initialization failed"); + } + else + { + Print("[OK] Data persistence initialized"); + // * v9.31 FIX#114: Load optimized profiles from previous optimization runs + if(!MQLInfoInteger(MQL_OPTIMIZATION)) + LoadOptimizedProfiles(); + if(LoadPerformanceOnStart && LoadPerformanceData()) + { + Print("[OK] Performance data loaded from file"); + PrintLoadedPerformanceStats(); + // * v7.5b FIX: In backtest, stale performance data poisons WinProbability. + // * FIX#3: Fully zero out g_perfData in backtest to prevent contamination + // of strategy stats, drawdown figures, and trade counters from + // previous live/backtest runs stored in the persistence file. + if(MQLInfoInteger(MQL_TESTER)) + { + // Reset WR to neutral + g_historicalWinRate = 0.5; + g_totalHistoricalTrades = 0; + // * FIX#3: Full reset of all performance counters + ZeroMemory(g_perfData); + for(int _pi = 0; _pi < 30; _pi++) + ZeroMemory(g_perfData.dailyStats[_pi]); + for(int _si = 0; _si < MAX_STRATEGY_STATS; _si++) + { + ZeroMemory(g_perfData.strategyStats[_si]); + g_perfData.strategyStats[_si].name = g_strategyNames[_si]; + } + Print("* FIX#3: Backtest mode -- g_perfData fully reset (stale persistence data cleared)"); + // * v9.13 FIX#24a: Also reset g_strategyPerf (used by STRATEGY RANKING) + // Problem: PrintStrategyRanking() reads g_strategyPerf which was NOT reset + // by FIX#3, causing stale strategy names/stats to appear in ranking output. + for(int _gi = 0; _gi < 15; _gi++) + { + ZeroMemory(g_strategyPerf[_gi]); + g_strategyPerf[_gi].name = g_strategyNames[_gi]; + } + // * v9.13 FIX#24b: Reset g_lossStreakForMTF and g_winsToResetMTF + // These may carry over from a previous run via UpdateHistoricalPerformance + // being called before backtest data is cleared. + g_lossStreakForMTF = 0; + g_winsToResetMTF = 0; + g_currentStreak = 0; + Print("* FIX#24: g_strategyPerf + lossStreak fully reset for clean backtest"); + } + } + } + } + // =============================================================== + // * Initialize Full Hybrid Trackers + // Reset all tracker arrays and prepare for regime-adaptive management + // =============================================================== + if(EA_UseFullHybrid) + { + g_hybridTrackerCount = 0; + for(int i = 0; i < MAX_HYBRID_TRACKERS; i++) + { + g_hybridTrackers[i].baseComment = ""; + g_hybridTrackers[i].tp1CloseTime = 0; + g_hybridTrackers[i].tp2CloseTime = 0; + g_hybridTrackers[i].tp1Processed = false; + g_hybridTrackers[i].tp2Processed = false; + g_hybridTrackers[i].entryPrice = 0; + g_hybridTrackers[i].originalSL = 0; + g_hybridTrackers[i].currentMode = HYBRID_MODE_SAFE; + g_hybridTrackers[i].trailingActive = false; + g_hybridTrackers[i].lastModeCheck = 0; + g_hybridTrackers[i].regimeAtTP1 = REGIME_UNKNOWN; + g_hybridTrackers[i].regimeAtTP2 = REGIME_UNKNOWN; + g_hybridTrackers[i].regimeStrengthTP1 = 0.0; + g_hybridTrackers[i].regimeStrengthTP2 = 0.0; + } + Print("[OK] Full Hybrid system initialized successfully"); + Print(" -> Regime-adaptive position management ACTIVE"); + Print(" -> Auto-switching: SAFE (ranging) <-> AGGRESSIVE (trending)"); + Print(" -> Trend threshold: ", DoubleToString(EA_Hybrid_TrendThreshold, 2)); + Print(" -> ADX filter: ", EA_Hybrid_UseADX ? "ENABLED" : "DISABLED"); + if(g_verboseLog) + Print(" -> Debug mode: ENABLED (detailed prints active)"); + } + // =============================================================== + // [TARGET] STRATEGY NAMES INITIALIZATION (* FIX#1: BEFORE InitializeBacktest) + // Must run first so strategyBreakdown[].name is populated correctly + // =============================================================== + InitializeStrategyNames(); + // =============================================================== + // * FIX#415: Initialize g_effectiveBIB BEFORE InitializeBacktest(). + // CRITICAL ORDER: InitializeBacktest() sets g_backtestPeakEquity = g_effectiveBIB. + // If g_effectiveBIB is still 0 (global default) at that point → ZD on first trade + // at: (g_backtestPeakEquity - g_backtestEquity) / g_backtestPeakEquity * 100. + // * FIX#480: g_effectiveBIB always from AccountBalance — no manual input needed. + g_effectiveBIB = AccountInfoDouble(ACCOUNT_BALANCE); + // =============================================================== + // [NEW] INITIALIZE BACKTESTING (v5.0) + // =============================================================== + if(BacktestMode != BACKTEST_DISABLED) + { + InitializeBacktest(); + Print("[OK] Backtesting framework initialized"); + Print(" Mode: ", EnumToString(BacktestMode)); + Print(" Initial Balance: $", DoubleToString(g_effectiveBIB, 2)); + } + // =============================================================== + // [RULER] FULL FIBONACCI + // =============================================================== + if(ShowFibonacci) + { + InitializeFullFibonacci(); + Print("[OK] Full Fibonacci initialized"); + } + // =============================================================== + // [MONEY] COST ANALYSIS + // =============================================================== + if(EnableCostAnalysis) + { + InitializeBrokerConfig(); + InitializeCostAnalysis(); + Print("[OK] Cost Analysis initialized"); + } + // =============================================================== + // [TARGET] PAIR OPTIMIZATION + // =============================================================== + if(PAIR_OptimizationEnabled) + { + InitializeAllPairProfiles(); + if(PAIR_AutoDetect) + { + LoadCurrentPairProfile(); + // * v7.0: Actually apply pair settings to runtime thresholds + if(g_gates.computed && PAIR_UseOptimalSettings) + ApplyPairOptimalSettings(); + } + Print("[OK] Pair Optimization initialized", + g_gates.computed ? " | Thresholds ACTIVE" : " | Using defaults"); + } + // =============================================================== + // [CHART] RISK MANAGEMENT + // =============================================================== + if(EnableRiskMgmt) + { + InitializeRiskManagement(); + Print("[OK] Risk Management initialized"); + } + // =============================================================== + // [NOTE] TRADE JOURNAL + // =============================================================== + if(EnableTradeJournal) + { + InitializeTradeJournal(); + Print("[OK] Trade Journal initialized"); + } + // =============================================================== + // [TARGET] STRATEGY NAMES INITIALIZATION (* FIX#1: Moved to before InitializeBacktest) + // =============================================================== + // InitializeStrategyNames(); // Already called above before InitializeBacktest + // =============================================================== + // [NUM] GLOBAL STATE INITIALIZATION + // =============================================================== + g_totalBars = iBars(_Symbol, _Period); + g_totalRates = g_totalBars; + g_prevCalculated = 0; + g_ticksThisBar = 0; + g_signalIdCounter = 0; + g_fvgIdCounter = 0; + g_obIdCounter = 0; + g_tradeIdCounter = 0; + // =============================================================== + // [TIMER] TIMESTAMP INITIALIZATION + // =============================================================== + datetime currentTime = TimeCurrent(); + g_lastCleanupTime = currentTime; + g_lastObjectCleanup = currentTime; + g_lastArrayCompact = currentTime; + g_lastMLUpdate = currentTime; + g_lastRiskCheck = currentTime; + g_lastCacheUpdate = currentTime; + g_lastPredictionUpdate = 0; + g_lastPerfSave = currentTime; + g_lastKillzoneUpdate = 0; + // =============================================================== + // [UP] MARKET STATE + // =============================================================== + g_isBullishStructure = true; + g_currentPhase = PHASE_NONE; + g_currentPDZone = "EQUILIBRIUM"; + g_cachedATR = 0; + g_cachedRSI = 50; + g_predictionInitialized = false; + // =============================================================== + // [TEST] CACHE WARMUP + // =============================================================== + WarmupCache(); + // =============================================================== + // [NEW] INITIALIZE ADVANCED ICT MODULES (CRT, TBS, AMD, JUDAS) + // =============================================================== + InitializeAllAdvancedICTModules(); + // =============================================================== + // [NEW] INITIALIZE CHART PATTERNS SYSTEM (v6.41 - WITH DIAMONDS!) + // =============================================================== + if(ChartPatterns_Enabled) + { + // Head & Shoulders Patterns + ArrayResize(g_hsPatterns, 20); + g_hsCount = 0; + // Double/Triple Top/Bottom Patterns + ArrayResize(g_mtbPatterns, 20); + g_mtbCount = 0; + // Triangle Patterns (Ascending, Descending, Symmetrical) + ArrayResize(g_trianglePatterns, 20); + g_triangleCount = 0; + // Flags & Pennants (Bull/Bear) + ArrayResize(g_fpPatterns, 20); + g_fpCount = 0; + // Wedge Patterns (Rising, Falling) + ArrayResize(g_wedgePatterns, 20); + g_wedgeCount = 0; + // Diamond Patterns (Top, Bottom) - NEW v6.41! + ArrayResize(g_diamondPatterns, 20); + g_diamondCount = 0; + g_currentDiamond.isValid = false; + // V-Patterns (V-Top, V-Bottom) + ArrayResize(g_vPatterns, 20); + g_vCount = 0; + // Swing Points for Pattern Detection + ArrayResize(g_swingHighs, 50); + ArrayResize(g_swingLows, 50); + ArrayResize(g_swingHighBars, 50); + ArrayResize(g_swingLowBars, 50); + ArrayResize(g_swingHighTimes, 50); + ArrayResize(g_swingLowTimes, 50); + g_swingHighCount = 0; + g_swingLowCount = 0; + Print("[OK] Chart Patterns initialized:"); + Print(" - Head & Shoulders (Top/Bottom)"); + Print(" - Double/Triple Top/Bottom"); + Print(" - Triangles (Ascending, Descending, Symmetrical)"); + Print(" - Flags & Pennants (Bull/Bear)"); + Print(" - Wedges (Rising, Falling)"); + Print(" - Diamonds (Top, Bottom) * NEW v6.41"); + Print(" - V-Patterns (V-Top, V-Bottom)"); + } + // =============================================================== + // [NEW] INITIALIZE CANDLESTICK PATTERNS SYSTEM + // =============================================================== + if(CandlePatterns_Enabled) + { + ArrayResize(g_extendedCandlePatterns, 100); + g_extendedCandleCount = 0; + g_lastExtendedCandlePattern.isValid = false; + Print("[OK] Candlestick Patterns initialized:"); + Print(" - Single Candle: Doji, Hammer, Shooting Star, Marubozu"); + Print(" - Two Candle: Engulfing, Harami, Piercing, Dark Cloud, Tweezer"); + Print(" - Three Candle: Morning/Evening Star, Three Soldiers/Crows, Abandoned Baby"); + Print(" - Minimum Strength: ", CandlePatterns_MinStrength, " (Filters weak patterns)"); + } + // =============================================================== + // [NEW] INITIALIZE SMART ENTRY SYSTEM + // =============================================================== + if(SmartEntry_Enabled) + { + InitializeMarketRegime(); + InitializeWinProbability(); + InitializeExpectedValue(); + InitializePositionSize(); + Print("[OK] Smart Entry System initialized"); + } + // =============================================================== + // [ML] INITIALIZE FULL AUTO-OPTIMIZATION + // =============================================================== + if(AutoOpt_Enabled) + { + InitializeAutoOptimization(); + // Apply pair table immediately — so mrr/min_conf are active from bar 0, + // not after the first 60-min AutoOpt recalc. + ApplyPairTFProfile(); + Print("[OK] Full Auto-Optimization initialized + pair table applied at startup"); + } + else + { + // AutoOpt disabled: still load pair table so mrr/min_conf/spread are correct + ApplyPairTFProfile(); + Print("[OK] Pair table applied (AutoOpt disabled)"); + } + // =============================================================== + // [NEW] INITIALIZE MULTI-TP MODULE + // =============================================================== + if(InpEnableMultiTP) + { + InitializeMultiTP(); + } + // =============================================================== + // [NEW] INITIALIZE ENTRY SCORING MODULE (NEW) + // =============================================================== + if(InpEnableScoring) + { + InitializeEntryScoring(); + Print("[OK] Entry Scoring System initialized"); + } + // =============================================================== + // [NEW] INITIALIZE NEWS FILTER + // =============================================================== + if(News_FilterEnabled) + { + InitializeNewsFilter(); + Print("[OK] News Filter initialized"); + } + // =============================================================== + // [NEW] INITIALIZE CORRELATION ANALYSIS + // =============================================================== + if(Corr_FilterEnabled) + { + InitializeCorrelation(); + Print("[OK] Correlation Analysis initialized"); + } + // =============================================================== + // [NEW] INITIALIZE TIME ANALYSIS + // =============================================================== + if(Time_AnalysisEnabled) + { + InitializeTimeAnalysis(); + Print("[OK] Time Analysis initialized"); + } + // =============================================================== + // [NEW] INITIALIZE DIVERGENCE DETECTION + // =============================================================== + if(Divergence_Enabled) + { + // Resize arrays + ArrayResize(g_divergences, g_maxDivergences); + ArrayResize(g_swingHighs, 50); + ArrayResize(g_swingLows, 50); + ArrayResize(g_swingHighBars, 50); + ArrayResize(g_swingLowBars, 50); + g_rsiDivergenceHandle = iRSI(_Symbol, _Period, Divergence_RSIPeriod, PRICE_CLOSE); + if(g_rsiDivergenceHandle != INVALID_HANDLE) + { + Print("[OK] Enhanced Divergence Detection initialized"); + } + } + // =============================================================== + // [NEW] INITIALIZE ENHANCED TRENDLINE SYSTEM + // =============================================================== + if(Trendline_Enabled) + { + ArrayResize(g_trendlines, g_maxTrendlines); + g_trendlineCount = 0; + g_tlIdCounter = 0; + // Reset flags + g_tlSupportActive = false; + g_tlResistanceActive = false; + g_tlBreakBullish = false; + g_tlBreakBearish = false; + g_tlRetestBullish = false; + g_tlRetestBearish = false; + g_bullTrendlineActive = false; + g_bearTrendlineActive = false; + g_trendlineBreakBull = false; + g_trendlineBreakBear = false; + Print("[OK] Enhanced Trendline System initialized"); + } + // =============================================================== + // [NEW] INITIALIZE VSA (VOLUME SPREAD ANALYSIS) - NEW! + // =============================================================== + if(VSA_Enabled) + { + InitializeVSA(); + Print("[OK] VSA (Volume Spread Analysis) initialized"); + } + // =============================================================== + // [NEW] INITIALIZE MTF (MULTI-TIMEFRAME ANALYSIS) - NEW! + // =============================================================== + if(MTF_Enabled) + { + InitializeMTF(); + Print("[OK] MTF Analysis initialized"); + Print(" Primary TF: ", EnumToString(g_primaryTF)); + Print(" Active TFs: ", g_activeTFCount); + } + // =============================================================== + // [OK] FINAL VALIDATION + // =============================================================== + g_initSuccess = true; + // =============================================================== + // [LIST] PRINT SUMMARY + // =============================================================== + PrintInitializationSummary(); + // =============================================================== + // [GAME] CREATE CONTROL BUTTONS + // =============================================================== + if(ShowSignalButtons) + { + CreateControlButtons(); + } + // =============================================================== + // [TIMER] SET UP TIMER FOR PERIODIC TASKS (NEW - IMPORTANT FOR LIVE) + // =============================================================== + if(!isTesting || isVisualMode) + { + // Timer every 60 seconds for: + // - Auto-save performance data + // - News filter updates + // - Spread monitoring + // - Memory cleanup + if(!EventSetTimer(60)) + { + Print("[WARN] WARNING: Failed to set timer. Error: ", GetLastError()); + Print(" Periodic tasks will run on tick instead"); + } + else + { + Print("[OK] Timer set for periodic tasks (60 sec)"); + } + } + // =============================================================== + // [SEARCH] DEBUG MODE CHECK + // =============================================================== + if(g_verboseLog) + { + Print("==========================================================="); + Print("[SEARCH] DEBUG INFORMATION:"); + Print(" Killzone types: ", g_numKillzones); + Print(" CRT setups capacity: ", g_maxCRT); + Print(" TBS setups capacity: ", g_maxTBS); + Print(" Judas swings capacity: ", g_maxJudas); + Print(" Memory usage: ", DoubleToString(CalculateMemoryUsage(), 2), " KB"); + Print(" Terminal Build: ", TerminalInfoInteger(TERMINAL_BUILD)); + Print("==========================================================="); + } + Print("==========================================================="); + Print("========================================"); + Print("[OK][OK][OK] " + EA_VERSION + " ALL FIXES ACTIVE — FIX#501+FIX#502+REFACTOR [OK][OK][OK]"); + Print("[OK] EA ANGEL " + EA_VERSION + " - INIT COMPLETE | " + EA_LAST_DATE); + Print(" [FIX_COMPILE] FIX#424 compile fixes: stray globals removed,"); + Print(" g_workingMinRR->g_autoOptParams.min_rr, daily reset location."); + Print(" [BUG#FIX_C4] Criterion 4 live divergence: was stale g_lastEntryScore"); + Print(" (phantom +8pts forever after entry). Now live g_divergences[]"); + Print(" scan: opposing+confirmed+fresh(<=3bars) only."); + Print(" [BUG#FIX_SE_RR] SmartExit_Check RR: was BE-moved POSITION_SL (inflated"); + Print(" RR 4-5x after BE). Now uses original g_multiTPEntries.stopLoss."); + Print(" [FIX#424] SmartExit Re-Entry: after SE close at profit, zone stored in"); + Print(" g_seReEntry. EA_CheckSignals allows ONE re-touch entry within 3"); + Print(" bars if price returns to zone and direction still valid. Bypasses"); + Print(" mitigated OB/FVG check. Resets on daily boundary."); + Print(" [FIX#423b] LATCHED cooperative threshold — once 2 cats fire at peak, threshold"); + Print(" stays locked even when cats change tick-by-tick (regime flicker)."); + Print(" Dec5: 2cats@0.97R -> latch=0.88 -> closes at rr<0.854R regardless"); + Print(" of whether Regime_Choppy is still active on that specific tick."); + Print(" [FIX#423] Cooperative SmartExit — FIX#403 trail and score system work together."); + Print(" SmartExit: ProfitGuard_Trail calls SmartExit_Check directly (no global flag)."); + Print(" Score gets retreat bonus (+20) and momentum multiplier (x1.4-1.5)."); + Print(" Per-bar bar-shrink tracking + M1 sub-bar detection on H1+."); + Print(" Dec5 scenario: 2cats+0.97R -> trail tightens to 0.87 -> closes at 0.84R"); + Print(" [FIX#422c] H1 se_override_rr 1.2->0.85R."); + Print(" ROOT: v10.78 bt max RR=0.97R, 2/3 cats at 0.97R not closed"); + Print(" (needed 1.2R). 3/3 closed at 0.82R instead. 0.85R catches peak."); + Print(" [FIX#422b] EURUSD H1 TP1/se_override_rr tuning from backtest data."); + Print(" tp1 3.50->3.20*ATR (min viable at mrr=1.50) | mrr 1.60->1.50"); + Print(" se_override_rr 1.5->1.2R (max RR seen in bt was 1.32R)"); + Print(" [FIX#422] EURUSD H1 complete calibration in pair table."); + Print(" sl 2.50->2.00*ATR | tp1 5.50->3.50*ATR | mrr 1.30->1.60"); + Print(" se_minrr 0->0.80R | tp1_pct 25->50% | CHOPPY block ON"); + Print(" se_override_rr 2.0->1.5R | score_cap=95 | mtf_hard_block=1"); + Print(" [FIX#421] SmartExit per-TF calibration stored in pair table."); + Print(" RSI thresholds, peak trail ratios, 2-cat override RR"); + Print(" all in PairTFConfig se_* fields — no more hardcodes."); + Print(" EURUSD H1: se_rsi_ob=70 se_peak_th3=0.82 se_override_rr=2.0"); + Print(" ROOT: H1 3.47R peak gave back 1.21R, 2/3 cats never fired."); + Print(" [FIX#415] BacktestInitialBalance auto-sanity check."); + Print(" ROOT: MT5 tester saves inputs between runs. BIB=10000 on $25k account"); + Print(" → lots 2.5x too small, daily DD limit $450 vs correct $1125, compound"); + Print(" 1.20x boost on wrong base → EA removed at 7% of test interval."); + Print(" FIX: if BIB < AccountBalance*0.80 at OnInit, auto-correct and warn."); + Print(" [FIX#416] EA_MaxTotalDrawdownPercent: 9.5 → 9.9."); + Print(" ROOT: 9.5% fired after 2 months on legitimate compound growth"); + Print(" (peak=$26829 from $25k init). FTMO allows 10%; 9.9% = real buffer."); + Print(" [FIX#417] Version strings corrected to v10.75."); + Print(" ROOT: OnDeinit showed v9.59, ExportBacktest showed v10.68."); + Print(" [FIX#414] SmartExit_FIX41 rr is now SIGNED (direction-aware)."); + Print(" ROOT: rr=MathAbs(price-entry)/MathAbs(entry-sl) always positive."); + Print(" SELL at 0.86R profit → reverses to 0.56R LOSS → FIX#403 sees rr=+0.56"); + Print(" thinks it's protecting profit → closes at -0.56R LOSS."); + Print(" Jan8 BOS SELL -$265, Jan10 TC SELL -$298, Jan18 TC SELL -$275 all caused by this."); + Print(" FIX: rr = isBuy?(price-entry)/slDist:(entry-price)/slDist."); + Print(" [FIX#413] CheckCurrentTimeQuality uses TimeGMT() (was TimeCurrent() broker time)."); + Print(" ROOT: hour thresholds (London 7-10, NY 13-16) are UTC. Broker GMT+2 shifted them 2h."); + Print(" [FIX#407] BOS_RETEST zone anchored to SL (not BOS price)."); + Print(" ROOT: zone=BOSlevel±ATR*0.3 = 0.7-4.6p from entry → any noise closed trade at -0.2R."); + Print(" FIX: SELL zoneTop=stopLoss, BUY zoneBottom=stopLoss. Zone only invalid if price beyond SL."); + Print(" [FIX#408] CheckZoneInvalidation fallback by entry price for TP1/TP2 tranches."); + Print(" ROOT: AddMultiTPEntry stores only TP3 ticket → TP1/TP2 had mIdx=-1 → no zone protection."); + Print(" FIX: ticket lookup + fallback match by entry price+direction (±5% ATR tolerance)."); + Print(" [FIX#409] News Filter uses TimeGMT() (was TimeCurrent() = broker local time)."); + Print(" ROOT: FTMO broker GMT+2: filter fired at 13:30 BROKER = 11:30 UTC (2h early, wrong window)."); + Print(" FIX: TimeGMT() always UTC. NFP 13:30 UTC now correctly caught at 13:30 UTC."); + Print(" [FIX#410] Weekly DD start balance capped at BacktestInitialBalance (mirrors daily FIX#DD_STARTBAL)."); + Print(" ROOT: after wins (balance $26500), weekly DD 8% = $2120 but FTMO limit = $2000 from initial."); + Print(" FIX: cap applied in both OnInit and weekly reset. Same pattern as daily."); + Print(" [FIX#411] PerformMaintenanceTasks day boundary uses TimeGMT() (was TimeCurrent() broker time)."); + Print(" ROOT: 2h window where g_tradesThisDay (broker midnight) and g_ea_stats.trades (GMT) async."); + Print(" FIX: TimeGMT() in PerformMaintenanceTasks matches OnNewBar reference time."); + Print(" [FIX#412] Removed duplicate if(ShowProcessingTime) — dead code (same condition nested)."); + Print("========================================"); + Print(" Total modules: ", CountActiveModules()); + Print(" Diamond Patterns: ENABLED * NEW"); + Print("==========================================================="); + // =============================================================== + // * FIX#480: Always use AccountInfoDouble(ACCOUNT_BALANCE) directly. + // Removed manual BacktestInitialBalance input (was causing stale-value bugs — FIX#415). + // Input still exists as deprecated (=0) for backward compatibility with saved sets. + // g_effectiveBIB is always the actual account balance at OnInit — correct for any account size. + { + double _acctBal480 = AccountInfoDouble(ACCOUNT_BALANCE); + g_effectiveBIB = (_acctBal480 > 0) ? _acctBal480 : 25000.0; // 25k fallback for edge cases only + PrintFormat("[FIX#480] g_effectiveBIB auto-set from AccountBalance: $%.2f", g_effectiveBIB); + } + // =============================================================== + // * v6.40 NEW: DAILY DRAWDOWN PROTECTION INITIALIZATION + // =============================================================== + // * FIX#DD_STARTBAL: Cap at BacktestInitialBalance so DD% is always relative to initial capital. + { double _b = AccountInfoDouble(ACCOUNT_BALANCE); double _e = AccountInfoDouble(ACCOUNT_EQUITY); + g_dailyStartBalance = (g_effectiveBIB > 0 && _b > g_effectiveBIB) ? g_effectiveBIB : _b; + g_dailyStartEquity = (g_effectiveBIB > 0 && _e > g_effectiveBIB) ? g_effectiveBIB : _e; } + // * v8.0 FIX TIMEZONE: Use TimeGMT() not TimeCurrent() -- broker server is GMT+2/+3 + // TimeCurrent() returns broker local time; % 86400 gives wrong midnight (1-3h off) + { datetime gmtNow = TimeGMT(); g_lastDDResetDate = gmtNow - (gmtNow % 86400); } + g_dailyDDLimitReached = false; + g_currentDailyDD = 0.0; + g_dailyDDBlockedTime = 0; + g_dailyDDBlockCount = 0; + // [v6.41] Weekly + Total DD init + // * FIX#410: Cap weeklyStartBalance at BacktestInitialBalance (mirrors daily FIX#DD_STARTBAL). + // ROOT CAUSE: daily had cap but weekly did not. After wins (e.g. balance=$26500), + // weekly DD limit = $26500 × 8% = $2120 — but FTMO limits from initial capital ($25000 × 8% = $2000). + // EA could exceed FTMO limit before its own weekly gate fires. + { + double _wb = AccountInfoDouble(ACCOUNT_BALANCE); + double _we = AccountInfoDouble(ACCOUNT_EQUITY); + g_weeklyStartBalance = (g_effectiveBIB > 0 && _wb > g_effectiveBIB) + ? g_effectiveBIB : _wb; + g_weeklyStartEquity = (g_effectiveBIB > 0 && _we > g_effectiveBIB) + ? g_effectiveBIB : _we; + } + g_lastWeeklyResetDate = TimeGMT() - ((TimeGMT() + 259200) % 604800); // Start of week (Mon) -- v8.0: TimeGMT() not TimeCurrent() + g_weeklyDDLimitReached = false; + g_currentWeeklyDD = 0.0; + g_peakBalance = AccountInfoDouble(ACCOUNT_BALANCE); + g_totalDDLimitReached = false; + g_currentTotalDD = 0.0; + // * v10.01 FIX#235: Reset all accumulated stats globals before loading persistence files. + // BUG: in MT5 backtest multi-run sessions each run calls OnInit but the stats globals + // (g_perfData, g_multiTPStats, g_backtestResults) were NOT zeroed — they retained values + // from the previous run. Then LoadPerformanceData() loaded old stats ON TOP of already-dirty + // state, causing carry-over of wins/losses/winRate into the fresh run's dashboard. + // FIX: always ZeroMemory on these three globals in OnInit BEFORE any file load. + // The file load (LoadPerformanceData) then correctly initialises from disk, or starts clean + // if no persistence file exists. + ZeroMemory(g_perfData); + ZeroMemory(g_multiTPStats); + ZeroMemory(g_backtestResults); + Print("* v10.01 FIX#235: g_perfData / g_multiTPStats / g_backtestResults reset for clean run"); + // * v10.01 REFACTOR: Validate all pair profile tables at startup + ValidatePairTFConfigs(); + // * v9.59 FIX#227: Reset D1 CHoCH state on every OnInit so backtest multi-run + // sessions don't carry bearish/bullish bias from a previous run into the next. + // Root cause: H4/H1 runs showed 0 trades because g_d1CHoCH_Bear=true from + // the prior M5 bearish run was blocking all BUY candidates at startup. + g_d1CHoCH_Bull = false; + g_d1CHoCH_Bear = false; + g_d1CHoCH_Valid = false; + g_d1LastBarTime = 0; + g_d1CHoCH_MtfConflictBars = 0; + Print("* v9.59 FIX#227: D1 CHoCH state reset (multi-run safety)"); + // FIX#503b: init cached ADX handle + reset rolling history + if(g_adxHandleExhaust != INVALID_HANDLE) { IndicatorRelease(g_adxHandleExhaust); } + g_adxHandleExhaust = iADX(_Symbol, _Period, 14); + ArrayInitialize(g_exhaustionHistory, 0); + g_exhaustionPeak = 0; + g_exhaustionTrend = 0.0; + PrintFormat("[OK] Daily DD Protection Initialized | Starting Balance: $%.2f | Limit: %.1f%%", + g_dailyStartBalance, EA_MaxDailyDrawdownPercent); + ChartRedraw(0); + return INIT_SUCCEEDED; +} +//+------------------------------------------------------------------+ +//| Initialize Advanced Module Arrays (NEW HELPER FUNCTION) | +//+------------------------------------------------------------------+ +bool InitializeAdvancedModuleArrays() +{ + bool success = true; + // CRT Setups + if(ArrayResize(g_crtSetups, 0, g_maxCRT) < 0) + { + Print("[WARN] Failed to resize g_crtSetups"); + success = false; + } + // TBS Setups + if(ArrayResize(g_tbsSetups, 0, g_maxTBS) < 0) + { + Print("[WARN] Failed to resize g_tbsSetups"); + success = false; + } + // Judas Swings + if(ArrayResize(g_judasSwings, 0, g_maxJudas) < 0) + { + Print("[WARN] Failed to resize g_judasSwings"); + success = false; + } + // Divergences + if(ArrayResize(g_divergences, 0, g_maxDivergences) < 0) + { + Print("[WARN] Failed to resize g_divergences"); + success = false; + } + // Trendlines + if(ArrayResize(g_trendlines, 0, g_maxTrendlines) < 0) + { + Print("[WARN] Failed to resize g_trendlines"); + success = false; + } + // News Events + if(ArrayResize(g_newsEvents, 0, g_maxNewsEvents) < 0) + { + Print("[WARN] Failed to resize g_newsEvents"); + success = false; + } + // Reset counters + g_crtCount = 0; + g_tbsCount = 0; + g_judasCount = 0; + g_divergenceCount = 0; + g_trendlineCount = 0; + g_newsEventCount = 0; + if(success) + Print("[OK] Advanced module arrays initialized"); + return success; +} +//+------------------------------------------------------------------+ +//| Initialize All Advanced ICT Modules (NEW HELPER FUNCTION) | +//+------------------------------------------------------------------+ +void InitializeAllAdvancedICTModules() +{ + Print("-----------------------------------------------------------"); + Print("[TARGET] Initializing Advanced ICT Modules..."); + // CRT (Candle Range Theory) + if(CRT_Enabled) + { + InitializeCRT(); + Print(" [OK] CRT (Candle Range Theory)"); + } + // TBS (Turtle Soup / Liquidity Sweep) + if(TBS_Enabled) + { + InitializeTBS(); + Print(" [OK] TBS (Turtle Soup)"); + } + // AMD (Accumulation, Manipulation, Distribution) + if(AMD_Enabled) + { + InitializeAMD(); + Print(" [OK] AMD Phases"); + } + // Judas Swing + if(Judas_Enabled) + { + InitializeJudas(); + Print(" [OK] Judas Swing"); + } + Print("-----------------------------------------------------------"); +} +//+------------------------------------------------------------------+ +//| Count Active Modules (NEW HELPER FUNCTION) | +//+------------------------------------------------------------------+ +int CountActiveModules() +{ + int count = 0; + if(EnableFVG) count++; + if(EnableOB) count++; + if(EnableLiquidity) count++; + if(EnableStructure) count++; + if(EnableOTE) count++; + if(EnableBreakerBlocks) count++; + if(EnableMitigationBlocks) count++; + if(EnableOFI) count++; + if(EnableVolumeProfile) count++; + if(EnableMarketMaker) count++; + if(EnableKillzones) count++; + if(EnableML) count++; + if(EnableSignals) count++; + if(EnableRiskMgmt) count++; + if(CRT_Enabled) count++; + if(TBS_Enabled) count++; + if(AMD_Enabled) count++; + if(Judas_Enabled) count++; + if(SmartEntry_Enabled) count++; + if(News_FilterEnabled) count++; + if(Divergence_Enabled) count++; + if(Time_AnalysisEnabled) count++; + if(Corr_FilterEnabled) count++; + return count; +} +//+------------------------------------------------------------------+ +//| OnTimer - NEW FUNCTION FOR PERIODIC TASKS | +//+------------------------------------------------------------------+ +void OnTimer() +{ + // Skip if not initialized + if(!g_initSuccess) return; + datetime currentTime = TimeCurrent(); + // =============================================================== + // [ML] AUTO-OPTIMIZATION PERIODIC RECALC + // =============================================================== + if(AutoOpt_Enabled) + { + UpdateAutoOptimization(); + if(AutoOpt_ShowPanel) + DrawAutoOptPanel(); + } + // =============================================================== + // [SAVE] AUTO-SAVE PERFORMANCE DATA + // =============================================================== + if(EnableDataPersistence && AutoSaveInterval > 0) + { + if((currentTime - g_lastPerfSave) >= AutoSaveInterval * 60) + { + if(SavePerformanceData()) + { + g_lastPerfSave = currentTime; + if(g_verboseLog) + Print("[SAVE] Auto-saved performance data"); + } + } + } + // =============================================================== + // [NEWS] UPDATE NEWS FILTER + // =============================================================== + if(News_FilterEnabled) + { + CheckNewsImpact(); + } + // =============================================================== + // [CHART] UPDATE SPREAD ANALYSIS + // =============================================================== + if(SPREAD_Monitor) + { + UpdateSpreadAnalysis(); + } + // =============================================================== + // [CLEAN] PERIODIC MEMORY CLEANUP + // =============================================================== + if((currentTime - g_lastArrayCompact) >= 3600) // Every hour + { + CompactArrays(); + g_lastArrayCompact = currentTime; + if(g_verboseLog) + Print("[CLEAN] Periodic array compaction completed"); + } + // =============================================================== + // [DEL] OBJECT CLEANUP + // =============================================================== + if((currentTime - g_lastObjectCleanup) >= 1800) // Every 30 min + { + CleanupOldObjects(); + g_lastObjectCleanup = currentTime; + } + // =============================================================== + // [UP] UPDATE CORRELATIONS + // =============================================================== + if(Corr_FilterEnabled) +{ + CalculateCorrelations(); +} +} +//+------------------------------------------------------------------+ +//| Initialize Working Variables | +//+------------------------------------------------------------------+ +void InitializeWorkingVariables() +{ + g_workingFVG_MinSize = FVG_MinSize; + g_workingFVG_MaxAge = FVG_MaxAge; + g_workingFVG_ExtendBars = FVG_ExtendBars; + g_workingMinConfluence = MinConfluence; + g_workingMinRiskReward = MinRiskReward; + g_workingMinEntryQuality = MinEntryQuality; + g_workingSL_ATRMultiplier = SL_ATRMultiplier; + g_workingTP_ATRMultiplier = TP_ATRMultiplier; + g_workingUseSessionFilter = UseSessionFilter; + g_workingSessionLondon = SessionLondon; + g_workingSessionNewYork = SessionNewYork; + g_workingSessionAsian = SessionAsian; + g_workingRefreshRate = RefreshRate; + g_workingRSI_Overbought = RSIOverbought; + g_workingRSI_Oversold = RSIOversold; + g_workingATRMinValue = ATRMinValue; + g_workingATRMaxValue = ATRMaxValue; + g_workingUseTrendFilter = false; + g_workingUseRSIFilter = UseRSIFilter; + g_workingUseATRFilter = UseATRFilter; + // * v7.4 FIX: Initialize mutable spread limit from input (will be overridden by AutoOpt/PairDetect) + g_workingMaxSpreadPips = EA_MaxSpreadPips; + g_workingOBVolumeMult = OB_VolumeMultiplier; // * v7.5b: Initialize from input, auto-opt overrides later + g_workingOB_MaxAge = OB_MaxAge; // * v9.03 FIX#13: Initialize from input + g_workingLIQ_MaxAge = LIQ_MaxAge; // * v9.03 FIX#13: Initialize from input + g_workingSTRUCT_SwingStrength = STRUCT_SwingStrength; // * v9.03 FIX#13: Initialize from input (symbolProfile overrides later) + g_workingLIQ_SwingStrength = LIQ_SwingStrength; // * v9.03 FIX#13: Initialize from input + g_workingKZ_ShowBoxes = KZ_ShowBoxes; + g_workingShowDashboard = ShowDashboard; + // * v9.16 FIX: Initialize TF-scaled working globals from inputs + // * FIX#453: Wire EnableTrendCont → g_tcEnabled (was hardcoded true — input was dead). + // ROOT: g_tcEnabled declared as bool=true, never read EnableTrendCont. + // Result: user could not disable TC from the input panel. + // Fix: assign here alongside the other TC working params. + g_tcEnabled = EnableTrendCont; + g_workingTC_EMA_Fast = TC_EMA_Fast; + g_workingTC_EMA_Slow = TC_EMA_Slow; + g_workingTC_PullbackBars = TC_PullbackBars; + g_workingVP_Period = VP_Period; + g_workingMM_Lookback = MM_LookbackPeriod; + g_workingPD_LookbackBars = PD_LookbackBars; + g_workingWinProb_LookbackTrades = WinProb_LookbackTrades; + // * v9.16 FIX#47: Init WinProb working weights from inputs (AutoOpt overrides later) + g_workingWP_TrendWeight = WinProb_TrendWeight; + g_workingWP_StructureWeight = WinProb_StructureWeight; + g_workingWP_ZoneWeight = WinProb_ZoneWeight; + g_workingWP_ConfluenceWeight = WinProb_ConfluenceWeight; + g_workingWP_TimingWeight = WinProb_TimingWeight; + g_workingWP_PatternWeight = WinProb_PatternWeight; + g_workingWP_MinThreshold = WinProb_MinThreshold; + // * v9.24 FIX#82: Init new working vars from user inputs (AutoOpt overrides later) + g_workingTC_RSI_Min = TC_RSI_Min; + g_workingTC_RSI_Max = TC_RSI_Max; + g_workingTC_MinSlopeATR = TC_MinSlopeATR; + g_workingTC_BaseScore = TC_BaseScore; + g_workingTP1_RR = InpTP1_RR; + g_workingTP2_RR = InpTP2_RR; + g_workingTP3_RR = InpTP3_RR; + g_workingMTF_MinConfidence = MTF_MinConfidence; + g_workingRegime_TrendThresh = Regime_TrendThreshold; + g_workingRegime_ADXMin = Regime_TrendADXMin; + g_workingCRT_MinRangeATR = CRT_MinRangeATR; + g_workingCRT_MaxRangeATR = CRT_MaxRangeATR; + g_workingJudas_TP1_RR = Judas_TP1_RR; + g_workingJudas_TP2_RR = Judas_TP2_RR; + g_workingJudas_TP3_RR = Judas_TP3_RR; + g_workingTBS_TP1_RR = TBS_TP1_RR; + g_workingTBS_TP2_RR = TBS_TP2_RR; + g_workingTBS_TP3_RR = TBS_TP3_RR; + g_workingVSA_MinStrength = VSA_MinStrength; + // * v9.16 FIX#47: Init PosSize working vars from inputs (AutoOpt overrides later) + g_workingPosSize_TrendingBonus = PosSize_TrendingBonus; + g_workingPosSize_RangingPenalty = PosSize_RangingPenalty; + g_workingTBS_ConfirmBars = TBS_ConfirmationBars; // * v9.16 FIX#48 + g_workingCorr_UpdateMins = Corr_UpdateMinutes; // * v9.16 FIX#48 + g_workingJudas_SL_ATR = Judas_SL_ATR; + g_workingTBS_MinSweepATR = TBS_MinSweepATR; + g_workingTBS_MaxSweepATR = TBS_MaxSweepATR; + g_workingAMD_ManipMoveATR = AMD_ManipMoveATR; + g_workingAMD_DistMinMove = AMD_DistMinMove; + g_workingNews_MinsBeforeHigh = News_MinsBeforeHigh; + g_workingNews_MinsAfterHigh = News_MinsAfterHigh; + g_workingRegime_ADRPeriod = 20; // default ADR lookback + // Initialize Professional Dashboard + g_dashMinimized = Dash_Minimized; + // * v8.07 FIX: Apply DASH_XOffset/DASH_YOffset inputs (comment said "Will be set in OnInit" but never was) + g_dashStartX = DASH_XOffset; + g_dashStartY = DASH_YOffset; + // * v9.03: Position management working vars (init from inputs, AutoOpt overrides later) + g_workingBE_RR = EA_BreakEven_RR; + g_workingBE_Ranging_RR = EA_BE_Ranging_RR; + g_workingBE_Volatile_RR = EA_BE_Volatile_RR; + g_workingBE_TP2_RR = EA_TP2_BE_Threshold; // * FIX#278: default from input + g_workingTrailStart_RR = EA_TrailStart_RR; + g_workingTrailStop_ATR = EA_TrailStop_ATR; + g_workingSmartExit_MinRR = EA_SmartExit_MinProfit_RR; + g_workingSmartExit_Signals = EA_SmartExit_Signals; + // * FIX#421: SE working vars — defaults; overridden by ApplyPairTFProfile + g_workingSE_RSI_OB = 70.0; + g_workingSE_RSI_OS = 30.0; + g_workingSE_PeakTh1 = 0.65; + g_workingSE_PeakTh2 = 0.72; + g_workingSE_PeakTh3 = 0.78; + g_workingSE_OverrideRR = 0.0; + // * FIX#423: Cooperative SE working vars — defaults + g_workingSE_MomRatio = 0.70; + g_workingSE_MomScoreMult = 1.40; + g_lastSE_catCount = 0; + // * FIX#424: Initialize re-entry slot + g_seReEntry.active = false; + g_seReEntry.closedAt = 0; + g_seReEntry.zoneTop = 0; + g_seReEntry.zoneBottom= 0; + g_seReEntry.zoneType = ""; + g_seReEntry.direction = 0; + g_seReEntry.closeRR = 0; + g_seReEntry.maxBars = 3; + g_seReEntryReady = false; + // * FIX#303: init KZ/Trend shadow vars from global inputs (ApplyPairTFProfile overrides per TF) + g_workingRequireKillzone = EA_RequireKillzone; + g_workingRequireTrend = EA_RequireTrend; + // * v10.24 FIX#310a-b: init TP percent and BlockNeutral working vars from global inputs. + // TF_CAT_SCALP (M5) overrides these in UpdateAutoOptimization step 2. + g_workingTP1_Pct = (int)EA_TP1_Percent; + g_workingTP2_Pct = (int)EA_TP2_Percent; + g_workingTP3_Pct = (int)EA_TP3_Percent; + g_workingBlockNeutral = EA_D1CHoCH_BlockNeutral; + // * v10.27 FIX#314: init position sizing working vars + g_workingLossStreakCut = PosSize_LossStreakCut; + g_workingRiskCeiling = EA_RiskPercent; + g_workingChoppyMinConf = 0; + g_workingChoppyLotMult = 0; + g_workingBlockTCChoppy = 0; + g_workingMTFHardBlock = 0; + g_workingD1CHoCHGate = 0; + g_workingAllowOB = 0; + g_workingAllowBOSRetest = 0; + g_workingMaxCostPct = COST_MaxCostPercent; + // * FIX#373: MR working vars - safe defaults (will be overridden by ApplyPairTFProfile) + g_workingMR_Enabled = 0; + g_workingMR_RSIBuy = 35; + g_workingMR_RSISell = 65; + g_workingMR_SLMult = 0.30; + g_workingMR_MinRangeATR = 1.5; + g_workingMaxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); + // * v9.24 FIX#80: Init FIX41 working vars from direct inputs + // AutoOpt will override these in ApplyAutoOptToWorkingVars() when enabled + g_workingFIX41_SmartExitRR = EA_SmartExit_MinProfit_RR; + g_workingFIX41_ScoreThresh = 15; + g_workingTrail_Volatile_Start = EA_Trail_Volatile_Start; + g_workingTrail_Volatile_Dist = EA_Trail_Volatile_Dist; + g_workingTrail_Trending_Start = EA_Trail_Trending_Start; + g_workingTrail_Trending_Dist = EA_Trail_Trending_Dist; + // * v9.03 FIX#11: Detection param working vars (init from inputs, AutoOpt overrides later) + g_workingOTE_MaxAge = OTE_MaxAge; + g_workingBB_MaxAge = BB_MaxAge; + g_workingMB_MaxAge = MB_MaxAge; + g_workingTrendline_MaxAge = Trendline_MaxAge; + g_workingCRT_Lookback = CRT_LookbackBars; + g_workingCRT_Expiry = CRT_ExpiryBars; + g_workingTBS_Expiry = TBS_ExpiryBars; + g_workingAMD_AccumMaxBars = AMD_AccumMaxBars; + g_workingSB_MaxAge = SB_MaxAge; + g_workingSignalExpiryBars = SignalExpiryBars; + g_workingFVG_MinStrength = FVG_MinStrength; + g_workingRegime_Lookback = Regime_Lookback; + g_workingRegime_ConfirmBars = Regime_ConfirmBars; + g_workingDivergence_Lookback = Divergence_Lookback; + g_workingTrendline_Lookback = Trendline_Lookback; + g_workingFIB_Lookback = FIB_LookbackBars; + // [ML] Auto-Optimization override (applied after defaults) + if(AutoOpt_Enabled && g_autoOptInitialized) + { + ApplyAutoOptToWorkingVars(); + } + // * v7.4: Diagnostic - show final working spread limit + Print(" * Spread Config: Input=", DoubleToString(EA_MaxSpreadPips, 1), + " -> Working=", DoubleToString(g_workingMaxSpreadPips, 1), " pips", + (g_workingMaxSpreadPips != EA_MaxSpreadPips ? " (AUTO-ADJUSTED)" : "")); +} +//+------------------------------------------------------------------+ +//| Initialize All Arrays - OPTIMIZED with Reserve Allocation | +//+------------------------------------------------------------------+ +bool InitializeAllArrays() +{ + bool success = true; + // OPTIMIZATION: Use reserve allocation (3rd parameter) for all arrays + // This pre-allocates memory to avoid frequent reallocations + if(ArrayResize(FVG_Array, 0, 100) < 0) success = false; // Reserve 100 + if(ArrayResize(OB_Array, 0, 50) < 0) success = false; // Reserve 50 + if(ArrayResize(LIQ_Array, 0, 50) < 0) success = false; // Reserve 50 + if(ArrayResize(STRUCT_Array, 0, 100) < 0) success = false; // Reserve 100 + if(ArrayResize(OTE_Array, 0, 30) < 0) success = false; // Reserve 30 + if(ArrayResize(BREAKER_Array, 0, 30) < 0) success = false; // Reserve 30 + if(ArrayResize(MITIGATION_Array, 0, 30) < 0) success = false; // Reserve 30 + if(ArrayResize(OFI_Array, 0, 50) < 0) success = false; // Reserve 50 + if(ArrayResize(VP_Levels, 0, 50) < 0) success = false; // Reserve 50 + if(ArrayResize(MM_Phases, 0, 20) < 0) success = false; // Reserve 20 + if(ArrayResize(ML_Predictions, 0, 50) < 0) success = false; // Reserve 50 + if(ArrayResize(SIGNAL_Array, 0, 30) < 0) success = false; // Reserve 30 + if(ArrayResize(g_tradeHistory, 0, 100) < 0) success = false; // Reserve 100 + if(ArrayResize(g_backtestTrades, 0, 200) < 0) success = false; // Reserve 200 + // Signal management arrays (fixed size, no reserve needed) + if(ArrayResize(g_signalApproachAlerts, MAX_SIGNAL_ARRAY) < 0) success = false; + if(ArrayResize(g_signalMarkedAsTaken, MAX_SIGNAL_ARRAY) < 0) success = false; + if(ArrayResize(g_signalExpiryAlerted, MAX_SIGNAL_ARRAY) < 0) success = false; + ArrayInitialize(g_signalApproachAlerts, 0); + ArrayInitialize(g_signalMarkedAsTaken, false); + ArrayInitialize(g_signalExpiryAlerted, false); + // Killzone stats + ArrayInitialize(g_kzTradesCount, 0); + ArrayInitialize(g_kzWins, 0); + ArrayInitialize(g_kzLosses, 0); + ArrayInitialize(g_kzAvgPips, 0); + return success; +} +//+------------------------------------------------------------------+ +//| Validate Inputs | +//+------------------------------------------------------------------+ +bool ValidateInputs() +{ + bool isValid = true; + if(FVG_MinSize < 0 || FVG_MinSizePips < 0 || FVG_MaxAge < 1) { + Print("[X] Invalid FVG parameters"); + isValid = false; + } + if(FVG_MaxCount < 1 || FVG_MaxCount > MAX_FVG_ARRAY) { + Print("[X] FVG_MaxCount out of range"); + isValid = false; + } + if(EnableRiskMgmt) { + if(AccountRiskPercent <= 0 || AccountRiskPercent > 10 || + DailyLossLimit <= 0 || DailyLossLimit > 20 || + MaxTradesPerDay < 1 || MinRiskReward < 0.5) { + Print("[X] Invalid Risk Management parameters"); + isValid = false; + } + } + if(EnableML) { + if(MLLookbackPeriod < 50 || MLConfidenceThreshold < 0.5 || + MLConfidenceThreshold > 1.0 || NN_LearningRate <= 0) { + Print("[X] Invalid ML/Neural Network parameters"); + isValid = false; + } + } + // * v9.03 FIX#10: TP% validation -- warn and auto-normalize if sum != 100% + { + double tpSum = EA_TP1_Percent + EA_TP2_Percent + EA_TP3_Percent; + if(MathAbs(tpSum - 100.0) > 0.01) + { + Print("[WARN] WARNING: TP1%+TP2%+TP3% = ", DoubleToString(tpSum, 1), + "% (should be 100%). Lot overflow protection will handle, but results may vary!"); + // Not blocking -- lot overflow protection in ExecuteTrade catches this + } + if(EA_TP1_Percent < 10 || EA_TP1_Percent > 100) + { + Print("[WARN] WARNING: EA_TP1_Percent=", DoubleToString(EA_TP1_Percent, 0), "% (recommended: 40-60%)"); + } + } + return isValid; +} +//+------------------------------------------------------------------+ +//| Initialize Strategy Names | +//+------------------------------------------------------------------+ +void InitializeStrategyNames() +{ + // * v9.13 FIX#24c: Align g_strategyNames with actual cand.type values. + // Previous names (FVG_ENTRY, OB_ENTRY, BB_ENTRY etc.) did NOT match the strings + // assigned to cand.type in candidate generation (FVG, OB, TC, BREAKER etc.). + // GetStrategyIndex("TC")->-1, GetStrategyIndex("FVG")->-1 -> never tracked in breakdown. + // Only "BOS_RETEST" matched. All other strategy stats null -> "(null)" in ranking. + g_strategyNames[0] = "FVG"; // cand.type="FVG" (was FVG_ENTRY) + g_strategyNames[1] = "OB"; // cand.type="OB" (was OB_ENTRY) + g_strategyNames[2] = "BOS_RETEST"; // cand.type="BOS_RETEST" [OK] unchanged + g_strategyNames[3] = "LIQ_SWEEP"; // cand.type="LIQ_SWEEP" (was LIQ_GRAB) + g_strategyNames[4] = "OTE"; // cand.type="OTE" (was OTE_ENTRY) + g_strategyNames[5] = "BREAKER"; // cand.type="BREAKER" (was BB_ENTRY) + g_strategyNames[6] = "TC"; // cand.type="TC" (was MB_ENTRY) + g_strategyNames[7] = "TBS"; // cand.type="TBS" (was MM_MODEL) + g_strategyNames[8] = "CRT"; // cand.type="CRT" (was ML_SIGNAL) + g_strategyNames[9] = "JUDAS"; // cand.type="JUDAS" (was FVG_OB_CONFLUENCE) + g_strategyNames[10] = "SB"; // cand.type="SB" (was KILLZONE_ENTRY) + g_strategyNames[11] = "MEAN_REV"; // * FIX#373: Mean Reversion (was ML_SIGNAL reserved) + g_strategyNames[12] = "FVG_OB"; // reserved (was PREMIUM_DISCOUNT) + g_strategyNames[13] = "STRUCTURE"; // reserved (was STRUCTURE_TRADE) + g_strategyNames[14] = "MULTI"; // reserved (was MULTI_CONFLUENCE) + g_numStrategies = 15; + // Initialize strategy performance + for(int i = 0; i < g_numStrategies; i++) { + g_strategyPerf[i].name = g_strategyNames[i]; + g_strategyPerf[i].totalTrades = 0; + g_strategyPerf[i].wins = 0; + g_strategyPerf[i].losses = 0; + g_strategyPerf[i].breakeven = 0; + g_strategyPerf[i].winRate = 0; + g_strategyPerf[i].avgWin = 0; + g_strategyPerf[i].avgLoss = 0; + g_strategyPerf[i].profitFactor = 0; + g_strategyPerf[i].expectancy = 0; + g_strategyPerf[i].maxDrawdown = 0; + g_strategyPerf[i].maxConsecutiveLosses = 0; + g_strategyPerf[i].totalProfitPips = 0; + g_strategyPerf[i].totalLossPips = 0; + g_strategyPerf[i].sharpeRatio = 0; + g_strategyPerf[i].sortinoRatio = 0; + g_strategyPerf[i].lastTradeTime = 0; + } +} +//+------------------------------------------------------------------+ +//| Warmup Cache | +//+------------------------------------------------------------------+ +void WarmupCache() +{ + Print("[SYNC] Warming up indicator cache..."); + if(g_atrHandle != INVALID_HANDLE) { + double atr[]; + ArraySetAsSeries(atr, true); + int copied = CopyBuffer(g_atrHandle, 0, 0, 1, atr); + if(copied > 0 && atr[0] > 0) { + g_cachedATR = atr[0]; + Print(" [OK] ATR: ", DoubleToString(g_cachedATR / g_pipValue, 2), " pips"); + } + ArrayFree(atr); + } + if(g_rsiHandle != INVALID_HANDLE) { + double rsi[]; + ArraySetAsSeries(rsi, true); + int copied = CopyBuffer(g_rsiHandle, 0, 0, 1, rsi); + if(copied > 0 && rsi[0] >= 0 && rsi[0] <= 100) { + g_cachedRSI = rsi[0]; + Print(" [OK] RSI: ", DoubleToString(g_cachedRSI, 1)); + } + ArrayFree(rsi); + } + g_lastCacheUpdate = TimeCurrent(); +} +//+------------------------------------------------------------------+ +//| Print Loaded Performance Stats | +//+------------------------------------------------------------------+ +void PrintLoadedPerformanceStats() +{ + Print("-----------------------------------------------------------"); + Print("[CHART] LOADED PERFORMANCE DATA:"); + Print("-----------------------------------------------------------"); + PrintFormat(" Total Trades: %d", g_perfData.totalTrades); + PrintFormat(" Win Rate: %.1f%%", g_perfData.overallWinRate); + PrintFormat(" Net Pips: %.1f", g_perfData.totalProfitPips - g_perfData.totalLossPips); + PrintFormat(" Max Drawdown: %.2f%%", g_perfData.maxDrawdown); + PrintFormat(" First Trade: %s", TimeToString(g_perfData.firstTradeDate)); + PrintFormat(" Last Trade: %s", TimeToString(g_perfData.lastTradeDate)); + Print("-----------------------------------------------------------"); +} +//+------------------------------------------------------------------+ +//| Print NN Architecture | +//+------------------------------------------------------------------+ +void PrintNNArchitecture() +{ + Print("-----------------------------------------------------------"); + Print("[BOT] NEURAL NETWORK ARCHITECTURE:"); + Print("-----------------------------------------------------------"); + PrintFormat(" Input Layer: %d neurons", NN_INPUT_FEATURES); + PrintFormat(" Hidden Layer 1: %d neurons (%s)", NN_HIDDEN1_NODES, EnumToString(NN_HiddenActivation)); + PrintFormat(" Hidden Layer 2: %d neurons (%s)", NN_HIDDEN2_NODES, EnumToString(NN_HiddenActivation)); + PrintFormat(" Hidden Layer 3: %d neurons (%s)", NN_HIDDEN3_NODES, EnumToString(NN_HiddenActivation)); + PrintFormat(" Output Layer: %d neurons (%s)", NN_OUTPUT_NODES, EnumToString(NN_OutputActivation)); + PrintFormat(" Optimizer: %s", EnumToString(NN_Optimizer)); + PrintFormat(" Learning Rate: %.6f", NN_LearningRate); + PrintFormat(" Dropout Rate: %.2f", NN_DropoutRate); + PrintFormat(" L2 Regularization: %.6f", NN_L2Regularization); + Print("-----------------------------------------------------------"); +} +//+------------------------------------------------------------------+ +//| [NEW] NEURAL NETWORK IMPLEMENTATION v5.0 | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| [NEW] NEURAL NETWORK IMPLEMENTATION v5.0 - FIXED | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| Initialize Neural Network - SAFE VERSION with Bounds Checking | +//+------------------------------------------------------------------+ +bool InitializeNeuralNetwork() +{ + Print("[BOT] Initializing Neural Network..."); + // =============================================================== + // STEP 1: VALIDATE LAYER SIZES BEFORE ALLOCATION + // =============================================================== + int layerSizes[5]; + layerSizes[0] = NN_INPUT_FEATURES; + layerSizes[1] = NN_HIDDEN1_NODES; + layerSizes[2] = NN_HIDDEN2_NODES; + layerSizes[3] = NN_HIDDEN3_NODES; + layerSizes[4] = NN_OUTPUT_NODES; + // [OK] CRITICAL: Validate before proceeding + for(int l = 0; l < 5; l++) + { + if(layerSizes[l] > NN_MAX_NEURONS) + { + PrintFormat("[X] ERROR: Layer %d size (%d) exceeds NN_MAX_NEURONS (%d)", + l, layerSizes[l], NN_MAX_NEURONS); + PrintFormat(" Please adjust layer sizes or increase NN_MAX_NEURONS"); + return false; + } + if(layerSizes[l] <= 0) + { + PrintFormat("[X] ERROR: Layer %d has invalid size: %d", l, layerSizes[l]); + return false; + } + } + // Validate weight array sizes + for(int l = 1; l < 5; l++) + { + if(layerSizes[l-1] > NN_MAX_WEIGHTS) + { + PrintFormat("[X] ERROR: Layer %d requires %d weights, but NN_MAX_WEIGHTS is %d", + l, layerSizes[l-1], NN_MAX_WEIGHTS); + return false; + } + } + PrintFormat("[OK] Layer size validation passed"); + PrintFormat(" Architecture: %d-%d-%d-%d-%d", + layerSizes[0], layerSizes[1], layerSizes[2], + layerSizes[3], layerSizes[4]); + // =============================================================== + // STEP 2: SET NETWORK PARAMETERS + // =============================================================== + g_neuralNet.numLayers = 5; // Input + 3 Hidden + Output + g_neuralNet.optimizer = NN_Optimizer; + g_neuralNet.lossFunction = NN_LossFunction; + g_neuralNet.learningRate = NN_LearningRate; + g_neuralNet.beta1 = NN_BETA1; + g_neuralNet.beta2 = NN_BETA2; + g_neuralNet.epsilon = NN_EPSILON; + g_neuralNet.weightDecay = NN_L2Regularization; + g_neuralNet.timestep = 0; + g_neuralNet.isTraining = false; + g_neuralNet.lastLoss = DBL_MAX; + g_neuralNet.bestLoss = DBL_MAX; + g_neuralNet.epochsWithoutImprovement = 0; + // =============================================================== + // STEP 3: INITIALIZE LAYERS WITH SAFE BOUNDS + // =============================================================== + for(int l = 0; l < g_neuralNet.numLayers; l++) + { + g_neuralNet.layers[l].numNeurons = layerSizes[l]; + // Set activation function + if(l == g_neuralNet.numLayers - 1) + g_neuralNet.layers[l].activation = NN_OutputActivation; + else if(l == 0) + g_neuralNet.layers[l].activation = ACTIVATION_RELU; + else + g_neuralNet.layers[l].activation = NN_HiddenActivation; + // Set dropout (not for input or output) + g_neuralNet.layers[l].dropoutRate = (l > 0 && l < g_neuralNet.numLayers - 1) ? NN_DropoutRate : 0; + // [OK] SAFE: Initialize dropout mask with bounds check + int numNeurons = layerSizes[l]; + for(int n = 0; n < numNeurons && n < NN_MAX_NEURONS; n++) + { + g_neuralNet.layers[l].dropoutMask[n] = true; + } + // Initialize neurons + int prevLayerSize = (l == 0) ? 0 : layerSizes[l - 1]; + // [OK] SAFE: Iterate with bounds check + for(int n = 0; n < numNeurons && n < NN_MAX_NEURONS; n++) + { + g_neuralNet.layers[l].neurons[n].numWeights = prevLayerSize; + g_neuralNet.layers[l].neurons[n].output = 0; + g_neuralNet.layers[l].neurons[n].delta = 0; + g_neuralNet.layers[l].neurons[n].biasGradient = 0; + g_neuralNet.layers[l].neurons[n].m_bias = 0; + g_neuralNet.layers[l].neurons[n].v_bias = 0; + if(l > 0) // Not input layer + { + // He initialization: weights ~ N(0, sqrt(2/fan_in)) + double stddev = MathSqrt(2.0 / MathMax(1.0, prevLayerSize)); + // [OK] SAFE: Initialize weights with bounds check + for(int w = 0; w < prevLayerSize && w < NN_MAX_WEIGHTS; w++) + { + // Box-Muller transform for normal distribution + double u1 = (MathRand() + 1.0) / 32768.0; + double u2 = (MathRand() + 1.0) / 32768.0; + // Clamp u1 to avoid log(0) + u1 = MathMax(0.0001, MathMin(0.9999, u1)); + double z = MathSqrt(-2.0 * MathLog(u1)) * MathCos(2.0 * M_PI * u2); + g_neuralNet.layers[l].neurons[n].weights[w] = z * stddev; + g_neuralNet.layers[l].neurons[n].weightGradients[w] = 0; + g_neuralNet.layers[l].neurons[n].m_weights[w] = 0; + g_neuralNet.layers[l].neurons[n].v_weights[w] = 0; + } + // Initialize bias to small value + g_neuralNet.layers[l].neurons[n].bias = 0.01; + } + } + if(g_verboseLog) + { + PrintFormat(" Layer %d: %d neurons, activation=%s, dropout=%.2f", + l, numNeurons, + EnumToString(g_neuralNet.layers[l].activation), + g_neuralNet.layers[l].dropoutRate); + } + } + // =============================================================== + // STEP 4: INITIALIZE FEATURE NORMALIZER + // =============================================================== + for(int f = 0; f < NN_INPUT_FEATURES && f < NN_MAX_FEATURES; f++) + { + g_featureNormalizer.means[f] = 0; + g_featureNormalizer.stds[f] = 1; + g_featureNormalizer.mins[f] = 0; + g_featureNormalizer.maxs[f] = 1; + } + g_featureNormalizer.numFeatures = NN_INPUT_FEATURES; + g_featureNormalizer.useMeanStd = true; + g_featureNormalizer.initialized = false; + // =============================================================== + // STEP 5: INITIALIZE FEATURE CACHE + // =============================================================== + if(ArrayResize(g_featureCache, NN_INPUT_FEATURES) < 0) + { + Print("[X] Failed to allocate feature cache"); + return false; + } + ArrayInitialize(g_featureCache, 0); + g_featureCacheBar = -1; + // =============================================================== + // STEP 6: CALCULATE TOTAL PARAMETERS + // =============================================================== + int totalParams = 0; + for(int l = 1; l < g_neuralNet.numLayers; l++) + { + int layerParams = layerSizes[l] * (layerSizes[l-1] + 1); // weights + bias + totalParams += layerParams; + } + PrintFormat("[OK] Neural Network initialized successfully"); + PrintFormat(" Total parameters: %d", totalParams); + PrintFormat(" Memory usage: ~%.2f KB", totalParams * sizeof(double) / 1024.0); + g_nnInitialized = true; + return true; +} +//+------------------------------------------------------------------+ +//| Destroy Neural Network | +//+------------------------------------------------------------------+ +void DestroyNeuralNetwork() +{ + if(!g_nnInitialized) return; + // Free dynamic cache arrays only + ArrayFree(g_featureCache); + ArrayFree(g_trainingInputs); + ArrayFree(g_trainingTargets); + ArrayFree(g_validationInputs); + ArrayFree(g_validationTargets); + // Reset flags + g_nnInitialized = false; + g_nnTrained = false; + if(g_verboseLog) + { + Print("[ML] Neural Network destroyed"); + } +} +//+------------------------------------------------------------------+ +//| Forward Pass - FIXED (No GetPointer) | +//+------------------------------------------------------------------+ +void ForwardPass(double &inputs[]) +{ + if(!g_nnInitialized) return; + int inputSize = ArraySize(inputs); + // Set input layer + for(int n = 0; n < g_neuralNet.layers[0].numNeurons && n < inputSize; n++) + { + g_neuralNet.layers[0].neurons[n].output = inputs[n]; + } + // Forward through hidden and output layers + for(int l = 1; l < g_neuralNet.numLayers; l++) + { + int currentNumNeurons = g_neuralNet.layers[l].numNeurons; + int prevNumNeurons = g_neuralNet.layers[l - 1].numNeurons; + double currentDropoutRate = g_neuralNet.layers[l].dropoutRate; + ENUM_NN_ACTIVATION currentActivation = g_neuralNet.layers[l].activation; + // Apply dropout mask during training + if(g_neuralNet.isTraining && currentDropoutRate > 0) + { + for(int n = 0; n < currentNumNeurons; n++) + { + g_neuralNet.layers[l].dropoutMask[n] = ((double)MathRand() / 32768.0) > currentDropoutRate; + } + } + // Process each neuron + for(int n = 0; n < currentNumNeurons; n++) + { + // Skip dropped neurons during training + if(g_neuralNet.isTraining && !g_neuralNet.layers[l].dropoutMask[n]) + { + g_neuralNet.layers[l].neurons[n].output = 0; + continue; + } + // Compute weighted sum + double sum = g_neuralNet.layers[l].neurons[n].bias; + for(int p = 0; p < prevNumNeurons; p++) + { + sum += g_neuralNet.layers[l - 1].neurons[p].output * + g_neuralNet.layers[l].neurons[n].weights[p]; + } + // Apply activation (except for softmax which needs all outputs) + if(currentActivation != ACTIVATION_SOFTMAX) + { + g_neuralNet.layers[l].neurons[n].output = ApplyActivation(sum, currentActivation); + } + else + { + g_neuralNet.layers[l].neurons[n].output = sum; // Store raw for softmax + } + // Scale during inference for dropout + if(!g_neuralNet.isTraining && currentDropoutRate > 0) + { + g_neuralNet.layers[l].neurons[n].output *= (1.0 - currentDropoutRate); + } + } + // Apply softmax if needed + if(currentActivation == ACTIVATION_SOFTMAX) + { + ApplySoftmax(l); + } + } +} +//+------------------------------------------------------------------+ +//| Apply Softmax - FIXED (No GetPointer) | +//+------------------------------------------------------------------+ +void ApplySoftmax(int layerIndex) +{ + int numNeurons = g_neuralNet.layers[layerIndex].numNeurons; + // Find max for numerical stability + double maxVal = g_neuralNet.layers[layerIndex].neurons[0].output; + for(int n = 1; n < numNeurons; n++) + { + if(g_neuralNet.layers[layerIndex].neurons[n].output > maxVal) + maxVal = g_neuralNet.layers[layerIndex].neurons[n].output; + } + // Compute exp and sum + double sum = 0; + for(int n = 0; n < numNeurons; n++) + { + double expVal = MathExp(g_neuralNet.layers[layerIndex].neurons[n].output - maxVal); + g_neuralNet.layers[layerIndex].neurons[n].output = expVal; + sum += expVal; + } + // Normalize + if(sum > 0) + { + for(int n = 0; n < numNeurons; n++) + { + g_neuralNet.layers[layerIndex].neurons[n].output /= sum; + } + } +} +//+------------------------------------------------------------------+ +//| Backward Pass - FIXED (No GetPointer) | +//+------------------------------------------------------------------+ +void BackwardPass(double &targets[]) +{ + if(!g_nnInitialized) return; + int outputLayerIdx = g_neuralNet.numLayers - 1; + int outputNumNeurons = g_neuralNet.layers[outputLayerIdx].numNeurons; + ENUM_NN_ACTIVATION outputActivation = g_neuralNet.layers[outputLayerIdx].activation; + int targetSize = ArraySize(targets); + // Calculate output layer deltas + for(int n = 0; n < outputNumNeurons && n < targetSize; n++) + { + double output = g_neuralNet.layers[outputLayerIdx].neurons[n].output; + double target = targets[n]; + // Cross-entropy with softmax: delta = output - target + if(outputActivation == ACTIVATION_SOFTMAX) + { + g_neuralNet.layers[outputLayerIdx].neurons[n].delta = output - target; + } + else + { + // MSE: delta = (output - target) * activation_derivative + double error = output - target; + g_neuralNet.layers[outputLayerIdx].neurons[n].delta = + error * ApplyActivationDerivative(output, outputActivation); + } + } + // Backpropagate through hidden layers + for(int l = outputLayerIdx - 1; l > 0; l--) + { + int currentNumNeurons = g_neuralNet.layers[l].numNeurons; + int nextNumNeurons = g_neuralNet.layers[l + 1].numNeurons; + ENUM_NN_ACTIVATION currentActivation = g_neuralNet.layers[l].activation; + for(int n = 0; n < currentNumNeurons; n++) + { + // Skip dropped neurons + if(g_neuralNet.isTraining && !g_neuralNet.layers[l].dropoutMask[n]) + { + g_neuralNet.layers[l].neurons[n].delta = 0; + continue; + } + // Sum of weighted deltas from next layer + double sum = 0; + for(int next = 0; next < nextNumNeurons; next++) + { + sum += g_neuralNet.layers[l + 1].neurons[next].delta * + g_neuralNet.layers[l + 1].neurons[next].weights[n]; + } + // Multiply by activation derivative + double output = g_neuralNet.layers[l].neurons[n].output; + g_neuralNet.layers[l].neurons[n].delta = + sum * ApplyActivationDerivative(output, currentActivation); + } + } + // Calculate gradients + for(int l = 1; l < g_neuralNet.numLayers; l++) + { + int currentNumNeurons = g_neuralNet.layers[l].numNeurons; + int prevNumNeurons = g_neuralNet.layers[l - 1].numNeurons; + for(int n = 0; n < currentNumNeurons; n++) + { + double delta = g_neuralNet.layers[l].neurons[n].delta; + // Bias gradient + g_neuralNet.layers[l].neurons[n].biasGradient += delta; + // Weight gradients + for(int p = 0; p < prevNumNeurons; p++) + { + double prevOutput = g_neuralNet.layers[l - 1].neurons[p].output; + g_neuralNet.layers[l].neurons[n].weightGradients[p] += delta * prevOutput; + } + } + } +} +//+------------------------------------------------------------------+ +//| Update Weights - Adam Optimizer - FIXED (No GetPointer) | +//+------------------------------------------------------------------+ +void UpdateWeights() +{ + if(!g_nnInitialized) return; + g_neuralNet.timestep++; + double lr = g_neuralNet.learningRate; + double beta1 = g_neuralNet.beta1; + double beta2 = g_neuralNet.beta2; + double epsilon = g_neuralNet.epsilon; + double weightDecay = g_neuralNet.weightDecay; + // Bias correction + double bc1 = 1.0 - MathPow(beta1, g_neuralNet.timestep); + double bc2 = 1.0 - MathPow(beta2, g_neuralNet.timestep); + for(int l = 1; l < g_neuralNet.numLayers; l++) + { + int numNeurons = g_neuralNet.layers[l].numNeurons; + int prevLayerSize = g_neuralNet.layers[l - 1].numNeurons; + for(int n = 0; n < numNeurons; n++) + { + double biasGrad = g_neuralNet.layers[l].neurons[n].biasGradient; + // Update bias + if(g_neuralNet.optimizer == OPTIMIZER_ADAM || g_neuralNet.optimizer == OPTIMIZER_ADAMW) + { + // Adam update for bias + g_neuralNet.layers[l].neurons[n].m_bias = + beta1 * g_neuralNet.layers[l].neurons[n].m_bias + (1.0 - beta1) * biasGrad; + g_neuralNet.layers[l].neurons[n].v_bias = + beta2 * g_neuralNet.layers[l].neurons[n].v_bias + (1.0 - beta2) * biasGrad * biasGrad; + double m_hat = g_neuralNet.layers[l].neurons[n].m_bias / bc1; + double v_hat = g_neuralNet.layers[l].neurons[n].v_bias / bc2; + g_neuralNet.layers[l].neurons[n].bias -= lr * m_hat / (MathSqrt(v_hat) + epsilon); + } + else if(g_neuralNet.optimizer == OPTIMIZER_SGD) + { + g_neuralNet.layers[l].neurons[n].bias -= lr * biasGrad; + } + else if(g_neuralNet.optimizer == OPTIMIZER_MOMENTUM) + { + g_neuralNet.layers[l].neurons[n].m_bias = + 0.9 * g_neuralNet.layers[l].neurons[n].m_bias + lr * biasGrad; + g_neuralNet.layers[l].neurons[n].bias -= g_neuralNet.layers[l].neurons[n].m_bias; + } + else if(g_neuralNet.optimizer == OPTIMIZER_RMSPROP) + { + g_neuralNet.layers[l].neurons[n].v_bias = + 0.9 * g_neuralNet.layers[l].neurons[n].v_bias + 0.1 * biasGrad * biasGrad; + g_neuralNet.layers[l].neurons[n].bias -= + lr * biasGrad / (MathSqrt(g_neuralNet.layers[l].neurons[n].v_bias) + epsilon); + } + // Update weights + for(int w = 0; w < prevLayerSize; w++) + { + double grad = g_neuralNet.layers[l].neurons[n].weightGradients[w]; + // Gradient clipping + if(NN_UseGradientClipping) + { + if(grad > NN_GradientClipValue) grad = NN_GradientClipValue; + if(grad < -NN_GradientClipValue) grad = -NN_GradientClipValue; + } + if(g_neuralNet.optimizer == OPTIMIZER_ADAM || g_neuralNet.optimizer == OPTIMIZER_ADAMW) + { + // Adam update + g_neuralNet.layers[l].neurons[n].m_weights[w] = + beta1 * g_neuralNet.layers[l].neurons[n].m_weights[w] + (1.0 - beta1) * grad; + g_neuralNet.layers[l].neurons[n].v_weights[w] = + beta2 * g_neuralNet.layers[l].neurons[n].v_weights[w] + (1.0 - beta2) * grad * grad; + double m_hat = g_neuralNet.layers[l].neurons[n].m_weights[w] / bc1; + double v_hat = g_neuralNet.layers[l].neurons[n].v_weights[w] / bc2; + double update = lr * m_hat / (MathSqrt(v_hat) + epsilon); + // Weight decay + update += lr * weightDecay * g_neuralNet.layers[l].neurons[n].weights[w]; + g_neuralNet.layers[l].neurons[n].weights[w] -= update; + } + else if(g_neuralNet.optimizer == OPTIMIZER_SGD) + { + g_neuralNet.layers[l].neurons[n].weights[w] -= + lr * (grad + weightDecay * g_neuralNet.layers[l].neurons[n].weights[w]); + } + else if(g_neuralNet.optimizer == OPTIMIZER_MOMENTUM) + { + g_neuralNet.layers[l].neurons[n].m_weights[w] = + 0.9 * g_neuralNet.layers[l].neurons[n].m_weights[w] + lr * grad; + g_neuralNet.layers[l].neurons[n].weights[w] -= + g_neuralNet.layers[l].neurons[n].m_weights[w] + + lr * weightDecay * g_neuralNet.layers[l].neurons[n].weights[w]; + } + else if(g_neuralNet.optimizer == OPTIMIZER_RMSPROP) + { + g_neuralNet.layers[l].neurons[n].v_weights[w] = + 0.9 * g_neuralNet.layers[l].neurons[n].v_weights[w] + 0.1 * grad * grad; + g_neuralNet.layers[l].neurons[n].weights[w] -= + lr * grad / (MathSqrt(g_neuralNet.layers[l].neurons[n].v_weights[w]) + epsilon) + + lr * weightDecay * g_neuralNet.layers[l].neurons[n].weights[w]; + } + // Reset gradient + g_neuralNet.layers[l].neurons[n].weightGradients[w] = 0; + } + // Reset bias gradient + g_neuralNet.layers[l].neurons[n].biasGradient = 0; + } + } +} +//+------------------------------------------------------------------+ +//| Apply Activation Function | +//+------------------------------------------------------------------+ +double ApplyActivation(double x, ENUM_NN_ACTIVATION activation) +{ + switch(activation) + { + case ACTIVATION_RELU: + return (x > 0) ? x : 0; + case ACTIVATION_LEAKY_RELU: + return (x > 0) ? x : 0.01 * x; + case ACTIVATION_SIGMOID: + return 1.0 / (1.0 + MathExp(-MathMin(MathMax(x, -500), 500))); + case ACTIVATION_TANH: + return MathTanh(x); + case ACTIVATION_ELU: + return (x > 0) ? x : 1.0 * (MathExp(x) - 1.0); + case ACTIVATION_SWISH: + return x / (1.0 + MathExp(-x)); + default: + return x; + } +} +//+------------------------------------------------------------------+ +//| Apply Activation Derivative | +//+------------------------------------------------------------------+ +double ApplyActivationDerivative(double x, ENUM_NN_ACTIVATION activation) +{ + switch(activation) + { + case ACTIVATION_RELU: + return (x > 0) ? 1.0 : 0.0; + case ACTIVATION_LEAKY_RELU: + return (x > 0) ? 1.0 : 0.01; + case ACTIVATION_SIGMOID: + return x * (1.0 - x); // x is already sigmoid(z) + case ACTIVATION_TANH: + return 1.0 - x * x; // x is already tanh(z) + case ACTIVATION_ELU: + return (x > 0) ? 1.0 : x + 1.0; // x is already ELU(z) + case ACTIVATION_SWISH: + { + double sig = 1.0 / (1.0 + MathExp(-x)); + return sig + x * sig * (1.0 - sig); + } + default: + return 1.0; + } +} +//+------------------------------------------------------------------+ +//| Predict with Neural Network - FIXED (No GetPointer) | +//+------------------------------------------------------------------+ +double PredictWithNeuralNetwork(double &features[]) +{ + if(!g_nnInitialized) return 0.5; + g_neuralNet.isTraining = false; + ForwardPass(features); + // Get output layer + int outputIdx = g_neuralNet.numLayers - 1; + // Output: [Bullish, Neutral, Bearish] + double bullProb = g_neuralNet.layers[outputIdx].neurons[0].output; + // Return bullish probability (0-1 scale) + return bullProb; +} +//+------------------------------------------------------------------+ +//| Calculate Loss - FIXED (No GetPointer) | +//+------------------------------------------------------------------+ +double CalculateLoss(double &targets[]) +{ + int outputIdx = g_neuralNet.numLayers - 1; + int numNeurons = g_neuralNet.layers[outputIdx].numNeurons; + int targetSize = ArraySize(targets); + double loss = 0; + switch(g_neuralNet.lossFunction) + { + case NN_LOSS_MSE: + for(int n = 0; n < numNeurons && n < targetSize; n++) + { + double diff = g_neuralNet.layers[outputIdx].neurons[n].output - targets[n]; + loss += diff * diff; + } + loss /= numNeurons; + break; + case NN_LOSS_BINARY_CROSSENTROPY: + for(int n = 0; n < numNeurons && n < targetSize; n++) + { + double pred = MathMax(0.0001, MathMin(0.9999, g_neuralNet.layers[outputIdx].neurons[n].output)); + loss -= targets[n] * MathLog(pred) + (1 - targets[n]) * MathLog(1 - pred); + } + loss /= numNeurons; + break; + case NN_LOSS_CATEGORICAL_CROSSENTROPY: + for(int n = 0; n < numNeurons && n < targetSize; n++) + { + if(targets[n] > 0) + { + double pred = MathMax(0.0001, g_neuralNet.layers[outputIdx].neurons[n].output); + loss -= targets[n] * MathLog(pred); + } + } + break; + case NN_LOSS_HUBER: + { + double delta = 1.0; + for(int n = 0; n < numNeurons && n < targetSize; n++) + { + double diff = MathAbs(g_neuralNet.layers[outputIdx].neurons[n].output - targets[n]); + if(diff <= delta) + loss += 0.5 * diff * diff; + else + loss += delta * (diff - 0.5 * delta); + } + loss /= numNeurons; + break; + } + } + // Add L2 regularization + if(g_neuralNet.weightDecay > 0) + { + double l2Loss = 0; + for(int l = 1; l < g_neuralNet.numLayers; l++) + { + for(int n = 0; n < g_neuralNet.layers[l].numNeurons; n++) + { + int numWeights = g_neuralNet.layers[l].neurons[n].numWeights; + for(int w = 0; w < numWeights; w++) + { + double weight = g_neuralNet.layers[l].neurons[n].weights[w]; + l2Loss += weight * weight; + } + } + } + loss += 0.5 * g_neuralNet.weightDecay * l2Loss; + } + return loss; +} +//+------------------------------------------------------------------+ +//| Validate Network - FIXED (No GetPointer) | +//+------------------------------------------------------------------+ +double ValidateNetwork() +{ + if(g_validationSize == 0) return DBL_MAX; + bool wasTraining = g_neuralNet.isTraining; + g_neuralNet.isTraining = false; + double totalLoss = 0; + for(int i = 0; i < g_validationSize; i++) + { + double features[]; + ArrayResize(features, NN_INPUT_FEATURES); + for(int f = 0; f < NN_INPUT_FEATURES; f++) + features[f] = g_validationInputs[i * NN_INPUT_FEATURES + f]; + double targets[]; + ArrayResize(targets, NN_OUTPUT_NODES); + for(int t = 0; t < NN_OUTPUT_NODES; t++) + targets[t] = g_validationTargets[i * NN_OUTPUT_NODES + t]; + ForwardPass(features); + totalLoss += CalculateLoss(targets); + ArrayFree(features); + ArrayFree(targets); + } + g_neuralNet.isTraining = wasTraining; + return totalLoss / g_validationSize; +} +//+------------------------------------------------------------------+ +//| Train Neural Network - FIXED | +//+------------------------------------------------------------------+ +bool TrainNeuralNetwork(const datetime &time[], const double &open[], + const double &high[], const double &low[], + const double &close[], const long &volume[]) +{ + if(!g_nnInitialized) return false; + // * Minimum samples -- need at least 30 real trades to train + if(g_tradeHistoryCount < 30) + { + if(g_verboseLog) + Print("[BOT] NN Training skipped: only ", g_tradeHistoryCount, "/30 trades recorded"); + return false; + } + Print("[BOT] NN v9.10 Training on REAL TRADE OUTCOMES (", g_tradeHistoryCount, " trades)..."); + int totalSamples = g_tradeHistoryCount; + // =============================================================== + // STEP 1: Build feature matrix and outcome labels from trade history + // Each trade -> 14 ICT features + 1 binary label (WIN=1, LOSS=0) + // =============================================================== + double allFeatures[]; + double allLabels[]; + ArrayResize(allFeatures, totalSamples * NN_INPUT_FEATURES); + ArrayResize(allLabels, totalSamples * NN_OUTPUT_NODES); + ArrayInitialize(allFeatures, 0); + ArrayInitialize(allLabels, 0); + int validSamples = 0; + for(int i = 0; i < totalSamples; i++) + { + TradeRecord tr = g_tradeHistory[i]; + // Skip incomplete records + if(tr.entryPrice <= 0 || tr.stopLoss <= 0) continue; + if(tr.result != "WIN" && tr.result != "LOSS") continue; + // == OUTCOME LABEL: did TP1 hit? == + // WIN = TP1 was reached (positive pnl means closed in profit) + bool tp1Hit = (tr.result == "WIN"); + int labelIdx = validSamples * NN_OUTPUT_NODES; + if(NN_OUTPUT_NODES == 3) + { + // 3-class: [WIN_prob, NEUTRAL_prob, LOSS_prob] + allLabels[labelIdx] = tp1Hit ? 1.0 : 0.0; // WIN + allLabels[labelIdx + 1] = 0.0; // NEUTRAL (not used for hard outcomes) + allLabels[labelIdx + 2] = tp1Hit ? 0.0 : 1.0; // LOSS + } + else + { + // Binary: WIN=1, LOSS=0 + allLabels[labelIdx] = tp1Hit ? 1.0 : 0.0; + } + // == FEATURES: ICT context at trade entry == + // These map what the EA "knew" when it entered the trade + double f[]; + ArrayResize(f, NN_INPUT_FEATURES); + ArrayInitialize(f, 0.0); + int fi = 0; + // Feature 0: Structure aligned with trade direction? + // * FIX#388: REMOVED DATA LEAKAGE — (tr.result=="WIN") was using the outcome to predict itself. + // NEW: Use marketPhase stored at entry time as a proxy for structure bias. + // MARKUP/ACCUMULATION phase = bullish structure → BUY trades are with-structure + // MARKDOWN/DISTRIBUTION = bearish structure → SELL trades are with-structure + // This is a PRE-ENTRY condition — marketPhase was recorded when the trade opened. + { + bool phaseBull = (StringFind(tr.marketPhase, "MARKUP") >= 0 || + StringFind(tr.marketPhase, "ACCUMULATION") >= 0); + bool phaseBear = (StringFind(tr.marketPhase, "MARKDOWN") >= 0 || + StringFind(tr.marketPhase, "DISTRIBUTION") >= 0); + bool structAligned = (tr.isBullish && phaseBull) || (!tr.isBullish && phaseBear); + // Unknown phase → 0.5 (neutral, not 0 or 1 — avoids biasing the network) + f[fi++] = phaseBull || phaseBear ? (structAligned ? 1.0 : 0.0) : 0.5; + } + // Feature 1: In optimal PD zone? (Discount for BUY, Premium for SELL) + // * FIX#388: tr.riskReward is the PLANNED RR at entry (not actual outcome) — valid pre-entry. + // But to better match ExtractFeatures (which uses g_currentPDZone), use phase+direction + // as a second-level proxy: trades in aligned phase are more likely to be in PD zone. + // Falls back to RR ≥ 1.5 when phase is unknown (reasonable heuristic, no leakage). + { + bool phaseBull1 = (StringFind(tr.marketPhase, "MARKUP") >= 0 || + StringFind(tr.marketPhase, "ACCUMULATION") >= 0); + bool phaseBear1 = (StringFind(tr.marketPhase, "MARKDOWN") >= 0 || + StringFind(tr.marketPhase, "DISTRIBUTION") >= 0); + bool phaseKnown = phaseBull1 || phaseBear1; + bool pdAligned = (tr.isBullish && phaseBull1) || (!tr.isBullish && phaseBear1); + if(phaseKnown) + f[fi++] = pdAligned ? 1.0 : 0.0; + else + f[fi++] = (tr.riskReward >= 1.5) ? 1.0 : 0.0; // fallback: planned RR proxy + } + // Feature 2: Killzone entry (London/NY Open) + f[fi++] = (tr.killzoneType != KZ_NONE) ? 1.0 : 0.0; + // Feature 3: Entry quality (normalized 0-1) + // * FIX#17: Old trades quality was 0-140, new is 0-85. Normalize adaptively. + double qualMax = (tr.quality > 85) ? 140.0 : 85.0; // Auto-detect old vs new scale + f[fi++] = MathMin(1.0, tr.quality / qualMax); + // Feature 4: R:R ratio (normalized -- cap at 5.0 R) + f[fi++] = MathMin(1.0, tr.riskReward / 5.0); + // Feature 5: Strategy type encoded + // FVG=0.2, OB=0.4, BOS=0.6, OTE=0.8, KZ=1.0, others=0.0 + double stratEnc = 0.0; + if(StringFind(tr.strategy, "FVG") >= 0) stratEnc = 0.20; + else if(StringFind(tr.strategy, "OB") >= 0) stratEnc = 0.40; + else if(StringFind(tr.strategy, "BOS") >= 0) stratEnc = 0.60; + else if(StringFind(tr.strategy, "OTE") >= 0) stratEnc = 0.80; + else if(StringFind(tr.strategy, "KZ") >= 0 || + StringFind(tr.strategy, "KILLZONE") >= 0) stratEnc = 1.00; + else if(StringFind(tr.strategy, "TC") >= 0) stratEnc = 0.30; + else if(StringFind(tr.strategy, "TBS") >= 0) stratEnc = 0.50; + else if(StringFind(tr.strategy, "BREAKER") >= 0) stratEnc = 0.70; + else if(StringFind(tr.strategy, "MEAN_REV") >= 0) stratEnc = 0.15; // * FIX#373 + f[fi++] = stratEnc; + // Feature 6: Market phase + // MARKUP=bullish phase, MARKDOWN=bearish phase + double phaseEnc = 0.5; + if(StringFind(tr.marketPhase, "MARKUP") >= 0) phaseEnc = 0.9; + else if(StringFind(tr.marketPhase, "ACCUMULATION") >= 0) phaseEnc = 0.7; + else if(StringFind(tr.marketPhase, "DISTRIBUTION") >= 0) phaseEnc = 0.3; + else if(StringFind(tr.marketPhase, "MARKDOWN") >= 0) phaseEnc = 0.1; + f[fi++] = phaseEnc; + // Feature 7: SL distance relative to ATR (was SL too tight or too wide?) + // Tight SL (<0.5xATR) = noise stop = often bad + // Good SL = 0.8-1.5xATR + // Wide SL (>2xATR) = low R:R setup = often bad + double slDist = MathAbs(tr.entryPrice - tr.stopLoss); + double atrEst = g_cachedATR > 0 ? g_cachedATR : slDist; + double slATRRatio = (atrEst > 0) ? slDist / atrEst : 1.0; + // Optimal band 0.8-1.5: encode as distance from optimal center (1.15) + f[fi++] = MathMax(0.0, 1.0 - MathAbs(slATRRatio - 1.15) / 2.0); + // Feature 8: Trade direction (1=BUY, 0=SELL) + f[fi++] = (tr.isBullish) ? 1.0 : 0.0; + // Feature 9: Was it a winning or losing streak before this trade? + // Use position in history as proxy: consecutive same results before this trade + int streakBefore = 0; + for(int j = i - 1; j >= MathMax(0, i - 5); j--) + { + if(g_tradeHistory[j].result == tr.result) streakBefore++; + else break; + } + f[fi++] = MathMin(1.0, streakBefore / 5.0); // 0=no streak, 1=5 in a row + // Feature 10: Day of week (0=Mon, 0.8=Fri) -- ICT knows session matters + MqlDateTime entryDt; + TimeToStruct(tr.entryTime, entryDt); + f[fi++] = (entryDt.day_of_week > 0) ? (entryDt.day_of_week - 1) / 4.0 : 0.0; + // Feature 11: Hour of day normalized (0=midnight, 0.5=noon) + f[fi++] = entryDt.hour / 24.0; + // Feature 12: Recent win rate (trailing 10 trades before this one) + int recentStart = MathMax(0, i - 10); + int recentWins = 0, recentTotal = 0; + for(int j = recentStart; j < i; j++) + { + if(g_tradeHistory[j].result == "WIN") recentWins++; + recentTotal++; + } + f[fi++] = (recentTotal > 0) ? (double)recentWins / recentTotal : 0.5; + // Feature 13: Entry quality tier + // (HIGH quality >=60/85, MEDIUM 30-59, LOW <30) + // * FIX#17: Adaptive normalization for old (140) vs new (85) scale + double qualMax2 = (tr.quality > 85) ? 140.0 : 85.0; + double qualNorm = MathMin(1.0, tr.quality / qualMax2); + f[fi++] = qualNorm; + // Pad remaining features with 0 if fi < NN_INPUT_FEATURES + // (already initialized to 0 above) + // Store in main array + for(int fi2 = 0; fi2 < NN_INPUT_FEATURES; fi2++) + allFeatures[validSamples * NN_INPUT_FEATURES + fi2] = (fi2 < fi) ? f[fi2] : 0.0; + validSamples++; + ArrayFree(f); + } + if(validSamples < 30) + { + Print("[X] NN Training: only ", validSamples, " valid samples (need 30+)"); + ArrayFree(allFeatures); + ArrayFree(allLabels); + return false; + } + Print(" Valid samples: ", validSamples); + // Count class balance + int wins = 0; + for(int i = 0; i < validSamples; i++) + if(allLabels[i * NN_OUTPUT_NODES] > 0.5) wins++; + Print(" WIN labels: ", wins, " (", DoubleToString(100.0*wins/validSamples, 1), + "%) LOSS: ", validSamples - wins); + // =============================================================== + // STEP 2: Calculate normalization parameters from this data + // =============================================================== + for(int f2 = 0; f2 < NN_INPUT_FEATURES; f2++) + { + double sum = 0, sumSq = 0; + double minVal = DBL_MAX, maxVal = -DBL_MAX; + for(int s = 0; s < validSamples; s++) + { + double val = allFeatures[s * NN_INPUT_FEATURES + f2]; + sum += val; + sumSq += val * val; + if(val < minVal) minVal = val; + if(val > maxVal) maxVal = val; + } + g_featureNormalizer.means[f2] = sum / validSamples; + double variance = sumSq / validSamples - g_featureNormalizer.means[f2] * g_featureNormalizer.means[f2]; + g_featureNormalizer.stds[f2] = MathSqrt(MathMax(0.0001, variance)); + g_featureNormalizer.mins[f2] = minVal; + g_featureNormalizer.maxs[f2] = maxVal; + } + g_featureNormalizer.initialized = true; + // =============================================================== + // STEP 3: Split train/validation (80/20) + // =============================================================== + int trainSize = (int)(validSamples * 0.8); + int valSize = validSamples - trainSize; + ArrayResize(g_trainingInputs, trainSize * NN_INPUT_FEATURES); + ArrayResize(g_trainingTargets, trainSize * NN_OUTPUT_NODES); + ArrayResize(g_validationInputs, valSize * NN_INPUT_FEATURES); + ArrayResize(g_validationTargets,valSize * NN_OUTPUT_NODES); + // Normalize and split + for(int s = 0; s < validSamples; s++) + { + // Normalize features (z-score) + double normF[]; + ArrayResize(normF, NN_INPUT_FEATURES); + for(int f2 = 0; f2 < NN_INPUT_FEATURES; f2++) + { + double raw = allFeatures[s * NN_INPUT_FEATURES + f2]; + normF[f2] = (g_featureNormalizer.stds[f2] > 0.0001) + ? (raw - g_featureNormalizer.means[f2]) / g_featureNormalizer.stds[f2] + : 0.0; + normF[f2] = MathMax(-3.0, MathMin(3.0, normF[f2])); // Clip at +/-3σ + } + if(s < trainSize) + { + for(int f2 = 0; f2 < NN_INPUT_FEATURES; f2++) + g_trainingInputs[s * NN_INPUT_FEATURES + f2] = normF[f2]; + for(int t = 0; t < NN_OUTPUT_NODES; t++) + g_trainingTargets[s * NN_OUTPUT_NODES + t] = allLabels[s * NN_OUTPUT_NODES + t]; + } + else + { + int vi = s - trainSize; + for(int f2 = 0; f2 < NN_INPUT_FEATURES; f2++) + g_validationInputs[vi * NN_INPUT_FEATURES + f2] = normF[f2]; + for(int t = 0; t < NN_OUTPUT_NODES; t++) + g_validationTargets[vi * NN_OUTPUT_NODES + t] = allLabels[s * NN_OUTPUT_NODES + t]; + } + ArrayFree(normF); + } + g_trainingSize = trainSize; + g_validationSize = valSize; + ArrayFree(allFeatures); + ArrayFree(allLabels); + // =============================================================== + // STEP 4: Train with early stopping + // =============================================================== + g_neuralNet.isTraining = true; + g_nnTrainingEpoch = 0; + g_nnBestLoss = DBL_MAX; + g_nnEpochsNoImprovement = 0; + for(int epoch = 0; epoch < NN_MaxEpochs; epoch++) + { + g_nnTrainingEpoch = epoch + 1; + double epochLoss = 0; + // Shuffle + int indices[]; + ArrayResize(indices, trainSize); + for(int i = 0; i < trainSize; i++) indices[i] = i; + for(int i = trainSize - 1; i > 0; i--) + { + int j = MathRand() % (i + 1); + int tmp = indices[i]; indices[i] = indices[j]; indices[j] = tmp; + } + // Mini-batch SGD + for(int b = 0; b < trainSize; b += NN_BatchSize) + { + int bEnd = MathMin(b + NN_BatchSize, trainSize); + for(int k = b; k < bEnd; k++) + { + int idx = indices[k]; + double features[]; + ArrayResize(features, NN_INPUT_FEATURES); + for(int f2 = 0; f2 < NN_INPUT_FEATURES; f2++) + features[f2] = g_trainingInputs[idx * NN_INPUT_FEATURES + f2]; + double targets[]; + ArrayResize(targets, NN_OUTPUT_NODES); + for(int t = 0; t < NN_OUTPUT_NODES; t++) + targets[t] = g_trainingTargets[idx * NN_OUTPUT_NODES + t]; + ForwardPass(features); + epochLoss += CalculateLoss(targets); + // Backprop via existing UpdateWeights() + { + // Compute output deltas + int outputLayerIdx = g_neuralNet.numLayers - 1; + for(int n = 0; n < g_neuralNet.layers[outputLayerIdx].numNeurons; n++) + { + double out = g_neuralNet.layers[outputLayerIdx].neurons[n].output; + double tgt = (n < NN_OUTPUT_NODES) ? targets[n] : 0; + g_neuralNet.layers[outputLayerIdx].neurons[n].delta = (out - tgt) * out * (1.0 - out); + } + // Backprop through hidden layers + for(int l = g_neuralNet.numLayers - 2; l >= 1; l--) + { + for(int n = 0; n < g_neuralNet.layers[l].numNeurons; n++) + { + double error = 0; + for(int next = 0; next < g_neuralNet.layers[l+1].numNeurons; next++) + error += g_neuralNet.layers[l+1].neurons[next].delta * + g_neuralNet.layers[l+1].neurons[next].weights[n]; + double out = g_neuralNet.layers[l].neurons[n].output; + g_neuralNet.layers[l].neurons[n].delta = error * out * (1.0 - out); + } + } + // Accumulate gradients + for(int l = 1; l < g_neuralNet.numLayers; l++) + { + int prevSize = g_neuralNet.layers[l-1].numNeurons; + for(int n = 0; n < g_neuralNet.layers[l].numNeurons; n++) + { + for(int p = 0; p < prevSize; p++) + { + double prevOut = g_neuralNet.layers[l-1].neurons[p].output; + g_neuralNet.layers[l].neurons[n].weightGradients[p] += + g_neuralNet.layers[l].neurons[n].delta * prevOut; + } + g_neuralNet.layers[l].neurons[n].biasGradient += + g_neuralNet.layers[l].neurons[n].delta; + } + } + } + ArrayFree(features); + ArrayFree(targets); + } + UpdateWeights(); + } + // Validate + double valLoss = ValidateNetwork(); + if(valLoss < g_nnBestLoss - NN_MIN_IMPROVEMENT) + { + g_nnBestLoss = valLoss; + g_nnEpochsNoImprovement = 0; + } + else + { + g_nnEpochsNoImprovement++; + if(g_nnEpochsNoImprovement >= NN_EarlyStopPatience) break; // * v9.16 FIX#48: was NN_EARLY_STOP_PATIENCE (#define 20) -- now uses input + } + } + g_neuralNet.isTraining = false; + g_nnTrained = true; + g_nnReadyForUse = true; + g_nnLastTrainCount = g_tradeHistoryCount; + g_lastNNTraining = TimeCurrent(); + Print("[OK] NN v9.10 Training Complete -- Loss: ", DoubleToString(g_nnBestLoss, 5), + " | Epochs: ", g_nnTrainingEpoch, + " | Trained on: ", trainSize, " trades"); + if(ML_SaveModel) SaveNeuralNetwork(); + return true; +} +//+------------------------------------------------------------------+ +//| * v9.10 ExtractFeatures -- ICT-specific, outcome-correlated | +//| Mirrors exactly the features used in TrainNeuralNetwork above | +//| Called at prediction time with CURRENT market state | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| Extract Features for Neural Network | +//+------------------------------------------------------------------+ +void ExtractFeatures(int barIndex, const datetime &time[], const double &open[], + const double &high[], const double &low[], const double &close[], + const long &volume[], double &features[]) +{ + if(ArraySize(features) < NN_INPUT_FEATURES) + ArrayResize(features, NN_INPUT_FEATURES); + ArrayInitialize(features, 0.0); + // These features must EXACTLY match the training features in TrainNeuralNetwork + // Any mismatch = the trained weights predict garbage + int fi = 0; + // Feature 0: Structure aligned with signal direction? + bool sigBull = g_ea_signal.isValid ? g_ea_signal.isBullish : g_isBullishStructure; + features[fi++] = ((sigBull && g_isBullishStructure) || (!sigBull && !g_isBullishStructure)) ? 1.0 : 0.0; + // Feature 1: In optimal PD zone? + bool pdAligned = (sigBull && g_currentPDZone == "DISCOUNT") || + (!sigBull && g_currentPDZone == "PREMIUM"); + features[fi++] = pdAligned ? 1.0 : 0.0; + // Feature 2: In killzone? + features[fi++] = g_isInKillzone ? 1.0 : 0.0; + // Feature 3: Entry quality normalized (from last evaluated entry score) + features[fi++] = (g_lastEntryScore.totalScore > 0) + ? MathMin(1.0, g_lastEntryScore.totalScore / 85.0) : 0.5; + // Feature 4: Current candidate R:R (from last signal) + double rr = 0; + if(g_ea_signal.isValid && g_ea_signal.entryPrice > 0 && g_ea_signal.stopLoss > 0) + { + double risk = MathAbs(g_ea_signal.entryPrice - g_ea_signal.stopLoss); + double reward = MathAbs(g_ea_signal.tp1 - g_ea_signal.entryPrice); + rr = (risk > 0) ? MathMin(1.0, reward / risk / 5.0) : 0; + } + features[fi++] = rr; + // Feature 5: Strategy type encoded (same mapping as training) + double stratEnc = 0.0; + if(g_ea_signal.isValid) + { + string stype = g_ea_signal.type; + if(StringFind(stype, "FVG") >= 0) stratEnc = 0.20; + else if(StringFind(stype, "OB") >= 0) stratEnc = 0.40; + else if(StringFind(stype, "BOS") >= 0) stratEnc = 0.60; + else if(StringFind(stype, "OTE") >= 0) stratEnc = 0.80; + else if(StringFind(stype, "KILLZONE") >= 0 || + StringFind(stype, "KZ") >= 0) stratEnc = 1.00; + else if(StringFind(stype, "TC") >= 0) stratEnc = 0.30; + else if(StringFind(stype, "TBS") >= 0) stratEnc = 0.50; + else if(StringFind(stype, "BREAKER") >= 0) stratEnc = 0.70; + else if(StringFind(stype, "MEAN_REV") >= 0) stratEnc = 0.15; // * FIX#373 + } + features[fi++] = stratEnc; + // Feature 6: Market phase encoding + double phaseEnc = 0.5; + if(g_currentPhase == PHASE_MARKUP) phaseEnc = 0.9; + else if(g_currentPhase == PHASE_ACCUMULATION) phaseEnc = 0.7; + else if(g_currentPhase == PHASE_DISTRIBUTION) phaseEnc = 0.3; + else if(g_currentPhase == PHASE_MARKDOWN) phaseEnc = 0.1; + features[fi++] = phaseEnc; + // Feature 7: SL quality -- how close is SL to optimal (0.8-1.5x ATR)? + double slDist = 0; + if(g_ea_signal.isValid && g_ea_signal.entryPrice > 0) + slDist = MathAbs(g_ea_signal.entryPrice - g_ea_signal.stopLoss); + double slATRRatio = (g_cachedATR > 0 && slDist > 0) ? slDist / g_cachedATR : 1.15; + features[fi++] = MathMax(0.0, 1.0 - MathAbs(slATRRatio - 1.15) / 2.0); + // Feature 8: Trade direction + features[fi++] = (g_ea_signal.isValid && g_ea_signal.isBullish) ? 1.0 : 0.0; + // Feature 9: Loss streak (current consecutive losses) + // g_currentStreak < 0 = losses, encode as 0-1 (0=no streak, 1=5 consecutive) + int losses = (g_currentStreak < 0) ? MathAbs(g_currentStreak) : 0; + features[fi++] = MathMin(1.0, losses / 5.0); + // Feature 10: Day of week + MqlDateTime dt; + TimeToStruct(TimeCurrent(), dt); + features[fi++] = (dt.day_of_week > 0) ? (dt.day_of_week - 1) / 4.0 : 0.0; + // Feature 11: Hour of day + features[fi++] = dt.hour / 24.0; + // Feature 12: * FIX#18b: REMOVED g_historicalWinRate (CIRCULAR DEPENDENCY) + // OLD: Fed own WR into NN -> NN produced mlScore -> Score filtered trades -> trades updated WR -> loop + // NEW: ATR volatility regime (non-circular, market-derived, predictive of trade success) + // High vol = trending = better for ICT strategies, low vol = choppy = worse + { + double atrNorm = 0.5; // Default neutral + if(g_cachedATR > 0) + { + // Compare current ATR to a longer-term average via price range + double priceRange = SymbolInfoDouble(_Symbol, SYMBOL_BID); + if(priceRange > 0) + atrNorm = MathMax(0.1, MathMin(0.9, (g_cachedATR / priceRange) * 100.0)); // Normalized ATR% + } + features[fi++] = atrNorm; + } + // Feature 13: Entry quality tier (same as feature 3 but redundant -- kept for weight consistency) + features[fi++] = (g_lastEntryScore.totalScore > 0) + ? MathMin(1.0, g_lastEntryScore.totalScore / 85.0) : 0.5; + + // ================================================================ + // * v10.29 FIX#316: NEW FEATURES 14-27 — richer ICT market context + // ================================================================ + // Feature 14: Market regime encoded (0=unknown, 0.17=trending, 0.33=ranging, + // 0.50=choppy, 0.67=weak_trend, 0.83=volatile, 1.0=breakout) + { + double regEnc = 0.0; + if(g_regimeValid) + { + ENUM_MARKET_REGIME reg = g_regimeData.regime; + if(reg==REGIME_STRONG_TREND_UP||reg==REGIME_STRONG_TREND_DOWN|| + reg==REGIME_TREND_UP||reg==REGIME_TREND_DOWN) regEnc = 0.17; + else if(reg==REGIME_RANGING||reg==REGIME_RANGING_TIGHT|| + reg==REGIME_RANGING_WIDE) regEnc = 0.33; + else if(reg==REGIME_CHOPPY) regEnc = 0.50; + else if(reg==REGIME_WEAK_TREND_UP||reg==REGIME_WEAK_TREND_DOWN) regEnc = 0.67; + else if(reg==REGIME_VOLATILE) regEnc = 0.83; + else if(reg==REGIME_BREAKOUT) regEnc = 1.00; + } + features[fi++] = regEnc; + } + // Feature 15: Best OB strength (0=no OB, 1=strongest OB at entry zone) + { + double bestOBStr = 0.0; + bool sigBull15 = g_ea_signal.isValid ? g_ea_signal.isBullish : g_isBullishStructure; + for(int i = 0; i < MathMin(g_obCount, ArraySize(OB_Array)); i++) + { + if(!g_obs[i].active || g_obs[i].mitigated) continue; + if(g_obs[i].isBullish != sigBull15) continue; + if(g_obs[i].strength > bestOBStr) bestOBStr = g_obs[i].strength; + } + features[fi++] = bestOBStr; + } + // Feature 16: Nearest FVG size relative to ATR (0=no FVG, 1=large FVG) + { + double fvgEnc = 0.0; + bool sigBull16 = g_ea_signal.isValid ? g_ea_signal.isBullish : g_isBullishStructure; + double nearestFVG = DBL_MAX; + for(int i = 0; i < ArraySize(FVG_Array); i++) + { + if(!g_fvgs[i].active || g_fvgs[i].isBullish != sigBull16) continue; + double fvgMid = (g_fvgs[i].top + g_fvgs[i].bottom) / 2.0; + double dist = (g_ea_signal.isValid && g_ea_signal.entryPrice > 0) + ? MathAbs(g_ea_signal.entryPrice - fvgMid) : 0; + if(dist < nearestFVG) + { + nearestFVG = dist; + double fvgSize = g_fvgs[i].top - g_fvgs[i].bottom; + fvgEnc = (g_cachedATR > 0) ? MathMin(1.0, fvgSize / g_cachedATR) : 0; + } + } + features[fi++] = fvgEnc; + } + // Feature 17: MTF agreement strength (0=neutral, 0.5=partial, 1=full) + { + double mtfEnc = 0.5; + if(MTF_Enabled && g_mtfInitialized) + { + bool sigBull17 = g_ea_signal.isValid ? g_ea_signal.isBullish : g_isBullishStructure; + bool mtfBull = (g_mtfAnalysis.overallDirection == MTF_BULLISH || + g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH); + bool mtfBear = (g_mtfAnalysis.overallDirection == MTF_BEARISH || + g_mtfAnalysis.overallDirection == MTF_STRONG_BEARISH); + if((sigBull17 && mtfBull) || (!sigBull17 && mtfBear)) + mtfEnc = g_mtfAnalysis.overallConfidence / 100.0; + else if((sigBull17 && mtfBear) || (!sigBull17 && mtfBull)) + mtfEnc = 0.0; + } + features[fi++] = MathMax(0.0, MathMin(1.0, mtfEnc)); + } + // Feature 18: Session quality (0=avoid, 0.5=normal/poor, 1=excellent) + { + double sessEnc = 0.5; + if(g_timeValid) + { + if(g_timeData.currentHourQuality == HOUR_EXCELLENT) sessEnc = 1.0; + else if(g_timeData.currentHourQuality == HOUR_GOOD) sessEnc = 0.75; + else if(g_timeData.currentHourQuality == HOUR_POOR) sessEnc = 0.25; + else if(g_timeData.currentHourQuality == HOUR_AVOID) sessEnc = 0.0; + else sessEnc = 0.5; // default/normal + } + features[fi++] = sessEnc; + } + // Feature 19: D1 CHoCH bias strength (0=bear, 0.5=neutral, 1=bull) + { + double d1Enc = 0.5; + if(g_d1CHoCH_Valid) + { + if(g_d1CHoCH_Bull) d1Enc = 1.0; + else if(g_d1CHoCH_Bear) d1Enc = 0.0; + } + features[fi++] = d1Enc; + } + // Feature 20: Spread relative to ATR (0=tight/good, 1=wide/bad) + { + double spreadPrice = (double)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * _Point; + double spreadEnc = (g_cachedATR > 0) ? MathMin(1.0, spreadPrice / (g_cachedATR * 0.1)) : 0.5; + features[fi++] = spreadEnc; + } + // Feature 21: Number of agreeing OBs (normalized, 0=none, 1=many=5+) + { + int obCount = 0; + bool sigBull21 = g_ea_signal.isValid ? g_ea_signal.isBullish : g_isBullishStructure; + for(int i = 0; i < MathMin(g_obCount, ArraySize(OB_Array)); i++) + if(g_obs[i].active && !g_obs[i].mitigated && g_obs[i].isBullish == sigBull21) obCount++; + features[fi++] = MathMin(1.0, obCount / 5.0); + } + // Feature 22: Per-strategy win rate in current regime (from g_strategyPerf) + { + double stratRegimeWR = 0.5; + int sIdx = GetStrategyIndex(g_ea_signal.isValid ? g_ea_signal.type : ""); + if(sIdx >= 0 && g_regimeValid) + { + ENUM_MARKET_REGIME reg = g_regimeData.regime; + int rIdx22 = -1; + if(reg==REGIME_STRONG_TREND_UP||reg==REGIME_STRONG_TREND_DOWN|| + reg==REGIME_TREND_UP||reg==REGIME_TREND_DOWN) rIdx22=0; + else if(reg==REGIME_RANGING||reg==REGIME_RANGING_TIGHT|| + reg==REGIME_RANGING_WIDE) rIdx22=1; + else if(reg==REGIME_CHOPPY) rIdx22=2; + else if(reg==REGIME_WEAK_TREND_UP||reg==REGIME_WEAK_TREND_DOWN) rIdx22=3; + else if(reg==REGIME_VOLATILE) rIdx22=4; + else if(reg==REGIME_BREAKOUT) rIdx22=5; + if(rIdx22>=0 && g_strategyPerf[sIdx].total_regime[rIdx22] >= 5) + stratRegimeWR = g_strategyPerf[sIdx].wr_regime[rIdx22] / 100.0; + } + features[fi++] = stratRegimeWR; + } + // Feature 23: Volume relative to 20-bar average (0=low, 0.5=normal, 1=high) + { + long curVol = iVolume(_Symbol, _Period, 0); + long avgVol = 0; + for(int i = 1; i <= 20; i++) avgVol += iVolume(_Symbol, _Period, i); + avgVol /= 20; + double volEnc = (avgVol > 0) ? MathMin(1.0, (double)curVol / (double)(avgVol * 2)) : 0.5; + features[fi++] = volEnc; + } + // Feature 24: Distance to nearest unmitigated OB (0=at OB, 1=far) + { + double minDist = 1.0; + bool sigBull24 = g_ea_signal.isValid ? g_ea_signal.isBullish : g_isBullishStructure; + double ep = (g_ea_signal.isValid && g_ea_signal.entryPrice > 0) + ? g_ea_signal.entryPrice : SymbolInfoDouble(_Symbol, SYMBOL_BID); + for(int i = 0; i < MathMin(g_obCount, ArraySize(OB_Array)); i++) + { + if(!g_obs[i].active || g_obs[i].mitigated || g_obs[i].isBullish != sigBull24) continue; + double mid = (g_obs[i].top + g_obs[i].bottom) / 2.0; + double dist = (g_cachedATR > 0) ? MathAbs(ep - mid) / (g_cachedATR * 5.0) : 1.0; + if(dist < minDist) minDist = dist; + } + features[fi++] = minDist; + } + // Feature 25: Structure age — how recent is the last swing break (0=just broke, 1=old) + { + double structAge = 1.0; + if(g_lastCHoCHTime > 0) + { + int barsSince = (int)((TimeCurrent() - g_lastCHoCHTime) / PeriodSeconds(_Period)); + structAge = MathMin(1.0, barsSince / 20.0); + } + features[fi++] = structAge; + } + // Feature 26: RSI momentum direction (0=bearish momentum, 0.5=neutral, 1=bullish) + { + double rsiEnc = (g_cachedRSI > 0) ? g_cachedRSI / 100.0 : 0.5; + features[fi++] = rsiEnc; + } + // Feature 27: Active liquidity targets in direction (0=none, 1=many) + { + int liqCount = 0; + bool sigBull27 = g_ea_signal.isValid ? g_ea_signal.isBullish : g_isBullishStructure; + for(int i = 0; i < ArraySize(LIQ_Array); i++) + { + if(!LIQ_Array[i].isValid || LIQ_Array[i].swept) continue; + if(sigBull27 == LIQ_Array[i].isBSL) liqCount++; + } + features[fi++] = MathMin(1.0, liqCount / 5.0); + } + // All remaining features (fi to NN_INPUT_FEATURES-1): already 0 from ArrayInitialize + // Normalize (z-score, same as training) + if(g_featureNormalizer.initialized) + { + for(int i = 0; i < NN_INPUT_FEATURES && i < fi; i++) + { + if(g_featureNormalizer.stds[i] > 0.0001) + features[i] = (features[i] - g_featureNormalizer.means[i]) / g_featureNormalizer.stds[i]; + features[i] = MathMax(-3.0, MathMin(3.0, features[i])); + } + } +} +//+------------------------------------------------------------------+ +//| * v9.10 NormalizeFeatures -- kept for API compatibility | +//| Actual normalization is done inline in ExtractFeatures | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| Normalize Features | +//+------------------------------------------------------------------+ +void NormalizeFeatures(double &features[]) +{ + // * v9.10: Normalization is done inline in ExtractFeatures to ensure + // train/predict consistency. This function is kept for compatibility + // with any call sites that use it directly, but does nothing extra. + if(!g_featureNormalizer.initialized) return; + int size = ArraySize(features); + for(int i = 0; i < size && i < NN_INPUT_FEATURES; i++) + { + if(g_featureNormalizer.useMeanStd) + { + if(g_featureNormalizer.stds[i] > 0.0001) + features[i] = (features[i] - g_featureNormalizer.means[i]) / g_featureNormalizer.stds[i]; + } + else + { + double rng = g_featureNormalizer.maxs[i] - g_featureNormalizer.mins[i]; + if(rng > 0) + features[i] = (features[i] - g_featureNormalizer.mins[i]) / rng; + } + features[i] = MathMax(-3.0, MathMin(3.0, features[i])); + } +} +//+------------------------------------------------------------------+ +//| * v9.10 GenerateNNPrediction -- updates g_nnTP1WinProb | +//| This is the REAL output: probability that TP1 hits before SL | +//| Injected into CalculateEntryScore and CalculateWinProbability | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| Save Neural Network | +//+------------------------------------------------------------------+ +bool SaveNeuralNetwork() +{ + string filepath = GetDataFilePath(NN_MODEL_FILE); + int handle = FileOpen(filepath, FILE_WRITE|FILE_BIN|FILE_COMMON); + if(handle == INVALID_HANDLE) { + Print("[X] Failed to open file for saving: ", filepath); + return false; + } + // Write header + string header = "ICT_NN_v5.0"; + FileWriteString(handle, header, 12); + // Write network structure + FileWriteInteger(handle, g_neuralNet.numLayers); + FileWriteInteger(handle, (int)g_neuralNet.optimizer); + FileWriteInteger(handle, (int)g_neuralNet.lossFunction); + FileWriteDouble(handle, g_neuralNet.learningRate); + FileWriteDouble(handle, g_neuralNet.bestLoss); + FileWriteInteger(handle, g_neuralNet.timestep); + // Write layers + for(int l = 0; l < g_neuralNet.numLayers; l++) + { + FileWriteInteger(handle, g_neuralNet.layers[l].numNeurons); + FileWriteInteger(handle, (int)g_neuralNet.layers[l].activation); + FileWriteDouble(handle, g_neuralNet.layers[l].dropoutRate); + // Write neurons + for(int n = 0; n < g_neuralNet.layers[l].numNeurons; n++) + { + FileWriteDouble(handle, g_neuralNet.layers[l].neurons[n].bias); + if(l > 0) + { + int numWeights = g_neuralNet.layers[l].neurons[n].numWeights; + FileWriteInteger(handle, numWeights); + for(int w = 0; w < numWeights; w++) + { + FileWriteDouble(handle, g_neuralNet.layers[l].neurons[n].weights[w]); + } + // Save Adam states + for(int w = 0; w < numWeights; w++) + { + FileWriteDouble(handle, g_neuralNet.layers[l].neurons[n].m_weights[w]); + FileWriteDouble(handle, g_neuralNet.layers[l].neurons[n].v_weights[w]); + } + FileWriteDouble(handle, g_neuralNet.layers[l].neurons[n].m_bias); + FileWriteDouble(handle, g_neuralNet.layers[l].neurons[n].v_bias); + } + } + } + // Write normalizer + FileWriteInteger(handle, g_featureNormalizer.initialized ? 1 : 0); + FileWriteInteger(handle, g_featureNormalizer.useMeanStd ? 1 : 0); + for(int f = 0; f < NN_INPUT_FEATURES; f++) + { + FileWriteDouble(handle, g_featureNormalizer.means[f]); + FileWriteDouble(handle, g_featureNormalizer.stds[f]); + FileWriteDouble(handle, g_featureNormalizer.mins[f]); + FileWriteDouble(handle, g_featureNormalizer.maxs[f]); + } + FileClose(handle); + if(g_verboseLog) Print("[OK] Neural Network saved to: ", filepath); + return true; +} +//+------------------------------------------------------------------+ +//| Load Neural Network | +//+------------------------------------------------------------------+ +bool LoadNeuralNetwork() +{ + string filepath = GetDataFilePath(NN_MODEL_FILE); + if(!FileIsExist(filepath, FILE_COMMON)) { + if(g_verboseLog) Print("[i] No saved model found: ", filepath); + return false; + } + int handle = FileOpen(filepath, FILE_READ|FILE_BIN|FILE_COMMON); + if(handle == INVALID_HANDLE) { + Print("[X] Failed to open file for loading: ", filepath); + return false; + } + // Read and verify header + string header = FileReadString(handle, 12); + if(StringFind(header, "ICT_NN_v5") < 0) { + Print("[X] Invalid model file format"); + FileClose(handle); + return false; + } + // Read network structure + int numLayers = FileReadInteger(handle); + if(numLayers != g_neuralNet.numLayers) { + Print("[X] Model architecture mismatch"); + FileClose(handle); + return false; + } + g_neuralNet.optimizer = (ENUM_NN_OPTIMIZER)FileReadInteger(handle); + g_neuralNet.lossFunction = (ENUM_NN_LOSS)FileReadInteger(handle); + g_neuralNet.learningRate = FileReadDouble(handle); + g_neuralNet.bestLoss = FileReadDouble(handle); + g_neuralNet.timestep = FileReadInteger(handle); + // Read layers + for(int l = 0; l < g_neuralNet.numLayers; l++) + { + int numNeurons = FileReadInteger(handle); + if(numNeurons != g_neuralNet.layers[l].numNeurons) { + Print("[X] Layer size mismatch at layer ", l); + FileClose(handle); + return false; + } + g_neuralNet.layers[l].activation = (ENUM_NN_ACTIVATION)FileReadInteger(handle); + g_neuralNet.layers[l].dropoutRate = FileReadDouble(handle); + // Read neurons + for(int n = 0; n < numNeurons; n++) + { + g_neuralNet.layers[l].neurons[n].bias = FileReadDouble(handle); + if(l > 0) + { + int numWeights = FileReadInteger(handle); + g_neuralNet.layers[l].neurons[n].numWeights = numWeights; + for(int w = 0; w < numWeights; w++) + { + g_neuralNet.layers[l].neurons[n].weights[w] = FileReadDouble(handle); + } + // Load Adam states + for(int w = 0; w < numWeights; w++) + { + g_neuralNet.layers[l].neurons[n].m_weights[w] = FileReadDouble(handle); + g_neuralNet.layers[l].neurons[n].v_weights[w] = FileReadDouble(handle); + } + g_neuralNet.layers[l].neurons[n].m_bias = FileReadDouble(handle); + g_neuralNet.layers[l].neurons[n].v_bias = FileReadDouble(handle); + } + } + } + // Read normalizer + g_featureNormalizer.initialized = (FileReadInteger(handle) == 1); + g_featureNormalizer.useMeanStd = (FileReadInteger(handle) == 1); + for(int f = 0; f < NN_INPUT_FEATURES; f++) + { + g_featureNormalizer.means[f] = FileReadDouble(handle); + g_featureNormalizer.stds[f] = FileReadDouble(handle); + g_featureNormalizer.mins[f] = FileReadDouble(handle); + g_featureNormalizer.maxs[f] = FileReadDouble(handle); + } + g_featureNormalizer.numFeatures = NN_INPUT_FEATURES; + g_lastNNTraining = TimeCurrent(); + FileClose(handle); + g_nnTrained = true; + if(g_verboseLog) Print("[OK] Neural Network loaded from: ", filepath); + return true; +} +//+------------------------------------------------------------------+ +//| [NEW] ICT KILLZONES IMPLEMENTATION v5.0 | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| Initialize Killzone Definitions | +//+------------------------------------------------------------------+ +void InitializeKillzones() +{ + g_numKillzones = 0; + // =============================================================== + // ASIAN SESSION (23:00 - 08:00 UTC) + // =============================================================== + if(KZ_EnableAsian) + { + g_killzoneDefinitions[g_numKillzones].type = KZ_ASIAN; + g_killzoneDefinitions[g_numKillzones].name = "Asian"; + g_killzoneDefinitions[g_numKillzones].startHourUTC = KZ_ASIAN_START_HOUR; + g_killzoneDefinitions[g_numKillzones].startMinuteUTC = KZ_ASIAN_START_MIN; + g_killzoneDefinitions[g_numKillzones].endHourUTC = KZ_ASIAN_END_HOUR; + g_killzoneDefinitions[g_numKillzones].endMinuteUTC = KZ_ASIAN_END_MIN; + g_killzoneDefinitions[g_numKillzones].dstOffsetUS = 0; + g_killzoneDefinitions[g_numKillzones].dstOffsetEU = 0; + g_killzoneDefinitions[g_numKillzones].zoneColor = KZ_AsianColor; + g_killzoneDefinitions[g_numKillzones].enabled = true; + g_killzoneDefinitions[g_numKillzones].historicalVolatility = 0; + g_killzoneDefinitions[g_numKillzones].historicalWinRate = 0; + g_numKillzones++; + } + // =============================================================== + // LONDON OPEN (07:00 - 10:00 UTC, adjusted for DST) + // =============================================================== + if(KZ_EnableLondonOpen) + { + g_killzoneDefinitions[g_numKillzones].type = KZ_LONDON_OPEN; + g_killzoneDefinitions[g_numKillzones].name = "London Open"; + g_killzoneDefinitions[g_numKillzones].startHourUTC = KZ_LONDON_OPEN_START_HOUR; + g_killzoneDefinitions[g_numKillzones].startMinuteUTC = KZ_LONDON_OPEN_START_MIN; + g_killzoneDefinitions[g_numKillzones].endHourUTC = KZ_LONDON_OPEN_END_HOUR; + g_killzoneDefinitions[g_numKillzones].endMinuteUTC = KZ_LONDON_OPEN_END_MIN; + g_killzoneDefinitions[g_numKillzones].dstOffsetUS = 0; + g_killzoneDefinitions[g_numKillzones].dstOffsetEU = -1; // EU DST shifts 1 hour earlier + g_killzoneDefinitions[g_numKillzones].zoneColor = KZ_LondonColor; + g_killzoneDefinitions[g_numKillzones].enabled = true; + g_killzoneDefinitions[g_numKillzones].historicalVolatility = 0; + g_killzoneDefinitions[g_numKillzones].historicalWinRate = 0; + g_numKillzones++; + } + // =============================================================== + // LONDON CLOSE (15:00 - 17:00 UTC) + // =============================================================== + if(KZ_EnableLondonClose) + { + g_killzoneDefinitions[g_numKillzones].type = KZ_LONDON_CLOSE; + g_killzoneDefinitions[g_numKillzones].name = "London Close"; + g_killzoneDefinitions[g_numKillzones].startHourUTC = KZ_LONDON_CLOSE_START_HOUR; + g_killzoneDefinitions[g_numKillzones].startMinuteUTC = KZ_LONDON_CLOSE_START_MIN; + g_killzoneDefinitions[g_numKillzones].endHourUTC = KZ_LONDON_CLOSE_END_HOUR; + g_killzoneDefinitions[g_numKillzones].endMinuteUTC = KZ_LONDON_CLOSE_END_MIN; + g_killzoneDefinitions[g_numKillzones].dstOffsetUS = -1; + g_killzoneDefinitions[g_numKillzones].dstOffsetEU = -1; + g_killzoneDefinitions[g_numKillzones].zoneColor = KZ_LondonColor; + g_killzoneDefinitions[g_numKillzones].enabled = true; + g_killzoneDefinitions[g_numKillzones].historicalVolatility = 0; + g_killzoneDefinitions[g_numKillzones].historicalWinRate = 0; + g_numKillzones++; + } + // =============================================================== + // NEW YORK OPEN (12:00 - 15:00 UTC, adjusted for US DST) + // =============================================================== + if(KZ_EnableNYOpen) + { + g_killzoneDefinitions[g_numKillzones].type = KZ_NY_OPEN; + g_killzoneDefinitions[g_numKillzones].name = "NY Open"; + g_killzoneDefinitions[g_numKillzones].startHourUTC = KZ_NY_OPEN_START_HOUR; + g_killzoneDefinitions[g_numKillzones].startMinuteUTC = KZ_NY_OPEN_START_MIN; + g_killzoneDefinitions[g_numKillzones].endHourUTC = KZ_NY_OPEN_END_HOUR; + g_killzoneDefinitions[g_numKillzones].endMinuteUTC = KZ_NY_OPEN_END_MIN; + g_killzoneDefinitions[g_numKillzones].dstOffsetUS = -1; // US DST shifts 1 hour earlier + g_killzoneDefinitions[g_numKillzones].dstOffsetEU = 0; + g_killzoneDefinitions[g_numKillzones].zoneColor = KZ_NYColor; + g_killzoneDefinitions[g_numKillzones].enabled = true; + g_killzoneDefinitions[g_numKillzones].historicalVolatility = 0; + g_killzoneDefinitions[g_numKillzones].historicalWinRate = 0; + g_numKillzones++; + } + // =============================================================== + // NEW YORK LUNCH (16:00 - 18:00 UTC) + // =============================================================== + if(KZ_EnableNYLunch) + { + g_killzoneDefinitions[g_numKillzones].type = KZ_NY_LUNCH; + g_killzoneDefinitions[g_numKillzones].name = "NY Lunch"; + g_killzoneDefinitions[g_numKillzones].startHourUTC = KZ_NY_LUNCH_START_HOUR; + g_killzoneDefinitions[g_numKillzones].startMinuteUTC = KZ_NY_LUNCH_START_MIN; + g_killzoneDefinitions[g_numKillzones].endHourUTC = KZ_NY_LUNCH_END_HOUR; + g_killzoneDefinitions[g_numKillzones].endMinuteUTC = KZ_NY_LUNCH_END_MIN; + g_killzoneDefinitions[g_numKillzones].dstOffsetUS = -1; + g_killzoneDefinitions[g_numKillzones].dstOffsetEU = 0; + g_killzoneDefinitions[g_numKillzones].zoneColor = KZ_NYColor; + g_killzoneDefinitions[g_numKillzones].enabled = true; + g_killzoneDefinitions[g_numKillzones].historicalVolatility = 0; + g_killzoneDefinitions[g_numKillzones].historicalWinRate = 0; + g_numKillzones++; + } + // =============================================================== + // NEW YORK CLOSE (19:00 - 21:00 UTC) + // =============================================================== + if(KZ_EnableNYClose) + { + g_killzoneDefinitions[g_numKillzones].type = KZ_NY_CLOSE; + g_killzoneDefinitions[g_numKillzones].name = "NY Close"; + g_killzoneDefinitions[g_numKillzones].startHourUTC = KZ_NY_CLOSE_START_HOUR; + g_killzoneDefinitions[g_numKillzones].startMinuteUTC = KZ_NY_CLOSE_START_MIN; + g_killzoneDefinitions[g_numKillzones].endHourUTC = KZ_NY_CLOSE_END_HOUR; + g_killzoneDefinitions[g_numKillzones].endMinuteUTC = KZ_NY_CLOSE_END_MIN; + g_killzoneDefinitions[g_numKillzones].dstOffsetUS = -1; + g_killzoneDefinitions[g_numKillzones].dstOffsetEU = 0; + g_killzoneDefinitions[g_numKillzones].zoneColor = KZ_NYColor; + g_killzoneDefinitions[g_numKillzones].enabled = true; + g_killzoneDefinitions[g_numKillzones].historicalVolatility = 0; + g_killzoneDefinitions[g_numKillzones].historicalWinRate = 0; + g_numKillzones++; + } + // =============================================================== + // SILVER BULLET LONDON (10:00 - 11:00 UTC) + // =============================================================== + if(KZ_EnableSilverBullet) + { + g_killzoneDefinitions[g_numKillzones].type = KZ_SILVER_BULLET_LDN; + g_killzoneDefinitions[g_numKillzones].name = "Silver Bullet LDN"; + g_killzoneDefinitions[g_numKillzones].startHourUTC = KZ_SILVER_BULLET_LDN_START_HOUR; + g_killzoneDefinitions[g_numKillzones].startMinuteUTC = KZ_SILVER_BULLET_LDN_START_MIN; + g_killzoneDefinitions[g_numKillzones].endHourUTC = KZ_SILVER_BULLET_LDN_END_HOUR; + g_killzoneDefinitions[g_numKillzones].endMinuteUTC = KZ_SILVER_BULLET_LDN_END_MIN; + g_killzoneDefinitions[g_numKillzones].dstOffsetUS = 0; + g_killzoneDefinitions[g_numKillzones].dstOffsetEU = -1; + g_killzoneDefinitions[g_numKillzones].zoneColor = KZ_SilverBulletColor; + g_killzoneDefinitions[g_numKillzones].enabled = true; + g_killzoneDefinitions[g_numKillzones].historicalVolatility = 0; + g_killzoneDefinitions[g_numKillzones].historicalWinRate = 0; + g_numKillzones++; + // SILVER BULLET NY AM (14:00 - 15:00 UTC) + g_killzoneDefinitions[g_numKillzones].type = KZ_SILVER_BULLET_NY_AM; + g_killzoneDefinitions[g_numKillzones].name = "Silver Bullet NY AM"; + g_killzoneDefinitions[g_numKillzones].startHourUTC = KZ_SILVER_BULLET_NY_AM_START_HOUR; + g_killzoneDefinitions[g_numKillzones].startMinuteUTC = KZ_SILVER_BULLET_NY_AM_START_MIN; + g_killzoneDefinitions[g_numKillzones].endHourUTC = KZ_SILVER_BULLET_NY_AM_END_HOUR; + g_killzoneDefinitions[g_numKillzones].endMinuteUTC = KZ_SILVER_BULLET_NY_AM_END_MIN; + g_killzoneDefinitions[g_numKillzones].dstOffsetUS = -1; + g_killzoneDefinitions[g_numKillzones].dstOffsetEU = 0; + g_killzoneDefinitions[g_numKillzones].zoneColor = KZ_SilverBulletColor; + g_killzoneDefinitions[g_numKillzones].enabled = true; + g_killzoneDefinitions[g_numKillzones].historicalVolatility = 0; + g_killzoneDefinitions[g_numKillzones].historicalWinRate = 0; + g_numKillzones++; + // SILVER BULLET NY PM (19:00 - 20:00 UTC) + g_killzoneDefinitions[g_numKillzones].type = KZ_SILVER_BULLET_NY_PM; + g_killzoneDefinitions[g_numKillzones].name = "Silver Bullet NY PM"; + g_killzoneDefinitions[g_numKillzones].startHourUTC = KZ_SILVER_BULLET_NY_PM_START_HOUR; + g_killzoneDefinitions[g_numKillzones].startMinuteUTC = KZ_SILVER_BULLET_NY_PM_START_MIN; + g_killzoneDefinitions[g_numKillzones].endHourUTC = KZ_SILVER_BULLET_NY_PM_END_HOUR; + g_killzoneDefinitions[g_numKillzones].endMinuteUTC = KZ_SILVER_BULLET_NY_PM_END_MIN; + g_killzoneDefinitions[g_numKillzones].dstOffsetUS = -1; + g_killzoneDefinitions[g_numKillzones].dstOffsetEU = 0; + g_killzoneDefinitions[g_numKillzones].zoneColor = KZ_SilverBulletColor; + g_killzoneDefinitions[g_numKillzones].enabled = true; + g_killzoneDefinitions[g_numKillzones].historicalVolatility = 0; + g_killzoneDefinitions[g_numKillzones].historicalWinRate = 0; + g_numKillzones++; + } + // Initialize active killzone tracking + for(int i = 0; i < 9; i++) + { + g_activeKillzones[i].type = KZ_NONE; + g_activeKillzones[i].isActive = false; + g_activeKillzones[i].startTime = 0; + g_activeKillzones[i].endTime = 0; + g_activeKillzones[i].high = 0; + g_activeKillzones[i].low = DBL_MAX; + g_activeKillzones[i].open = 0; + g_activeKillzones[i].close = 0; + g_activeKillzones[i].range = 0; + g_activeKillzones[i].direction = 0; + g_activeKillzones[i].breakoutOccurred = false; + g_activeKillzones[i].objectName = ""; + } + g_killzonesInitialized = true; + Print("[PIN] Killzones Initialized: ", g_numKillzones, " zones active"); +} +//+------------------------------------------------------------------+ +//| Initialize DST Information | +//+------------------------------------------------------------------+ +void InitializeDSTInfo() +{ + datetime currentTime = TimeCurrent(); + // Detect broker timezone offset + if(BrokerTimezone == TZ_AUTO) + { + g_dstInfo.brokerUTCOffset = DetectBrokerUTCOffset(); + } + else + { + g_dstInfo.brokerUTCOffset = GetTimezoneOffset(BrokerTimezone); + } + // Detect DST status + g_dstInfo.isUSDST = IsUSDST(currentTime); + g_dstInfo.isEUDST = IsEUDST(currentTime); + g_dstInfo.isUKDST = IsUKDST(currentTime); + // Calculate next DST change + g_dstInfo.nextDSTChange = GetNextDSTChange(currentTime); + // Get local offset + g_dstInfo.localUTCOffset = GetLocalUTCOffset(); + // Determine current timezone string + g_dstInfo.currentTimezone = GetTimezoneString(BrokerTimezone); + if(ShowTimezoneInfo) + { + Print("-----------------------------------------------------------"); + Print("[WORLD] DST/TIMEZONE INFORMATION:"); + Print("-----------------------------------------------------------"); + PrintFormat(" Broker UTC Offset: %+d hours", g_dstInfo.brokerUTCOffset); + PrintFormat(" US DST Active: %s", g_dstInfo.isUSDST ? "Yes" : "No"); + PrintFormat(" EU DST Active: %s", g_dstInfo.isEUDST ? "Yes" : "No"); + PrintFormat(" UK DST Active: %s", g_dstInfo.isUKDST ? "Yes" : "No"); + PrintFormat(" Next DST Change: %s", TimeToString(g_dstInfo.nextDSTChange)); + Print("-----------------------------------------------------------"); + } +} +//+------------------------------------------------------------------+ +//| Detect Broker UTC Offset | +//+------------------------------------------------------------------+ +int DetectBrokerUTCOffset() +{ + datetime brokerTime = TimeCurrent(); + datetime gmtTime = TimeGMT(); + int offsetSeconds = (int)(brokerTime - gmtTime); + int offsetHours = offsetSeconds / 3600; + // Round to nearest hour + if(MathAbs(offsetSeconds % 3600) > 1800) + { + offsetHours += (offsetSeconds > 0) ? 1 : -1; + } + return offsetHours; +} +//+------------------------------------------------------------------+ +//| Get Timezone Offset | +//+------------------------------------------------------------------+ +int GetTimezoneOffset(ENUM_TIMEZONE tz) +{ + switch(tz) + { + case TZ_UTC: + case TZ_GMT: + return 0; + case TZ_EST: + return g_dstInfo.isUSDST ? -4 : -5; + case TZ_CET: + return g_dstInfo.isEUDST ? 2 : 1; + case TZ_JST: + return 9; + case TZ_AEST: + return 10; // Simplified, doesn't account for AEDT + case TZ_BROKER: + case TZ_AUTO: + default: + return g_dstInfo.brokerUTCOffset; + } +} +//+------------------------------------------------------------------+ +//| Get Local UTC Offset | +//+------------------------------------------------------------------+ +int GetLocalUTCOffset() +{ + datetime local = TimeLocal(); + datetime gmt = TimeGMT(); + int offsetSeconds = (int)(local - gmt); + return offsetSeconds / 3600; +} +//+------------------------------------------------------------------+ +//| Get Timezone String | +//+------------------------------------------------------------------+ +string GetTimezoneString(ENUM_TIMEZONE tz) +{ + switch(tz) + { + case TZ_UTC: return "UTC"; + case TZ_GMT: return "GMT"; + case TZ_EST: return g_dstInfo.isUSDST ? "EDT" : "EST"; + case TZ_CET: return g_dstInfo.isEUDST ? "CEST" : "CET"; + case TZ_JST: return "JST"; + case TZ_AEST: return "AEST"; + case TZ_BROKER: return "Broker"; + case TZ_AUTO: return "Auto"; + default: return "Unknown"; + } +} +//+------------------------------------------------------------------+ +//| Check if US DST is Active | +//+------------------------------------------------------------------+ +bool IsUSDST(datetime time) +{ + MqlDateTime dt; + TimeToStruct(time, dt); + int year = dt.year; + int month = dt.mon; + int day = dt.day; + int hour = dt.hour; + // US DST: Second Sunday in March to First Sunday in November + // Starts at 2:00 AM local time + // Before March or after November - no DST + if(month < 3 || month > 11) return false; + // April to October - DST active + if(month > 3 && month < 11) return true; + // March - check if we're past second Sunday + if(month == 3) + { + int secondSunday = GetNthSunday(year, 3, 2); + if(day > secondSunday) return true; + if(day == secondSunday && hour >= 2) return true; + return false; + } + // November - check if we're before first Sunday + if(month == 11) + { + int firstSunday = GetNthSunday(year, 11, 1); + if(day < firstSunday) return true; + if(day == firstSunday && hour < 2) return true; + return false; + } + return false; +} +//+------------------------------------------------------------------+ +//| Check if EU DST is Active | +//+------------------------------------------------------------------+ +bool IsEUDST(datetime time) +{ + MqlDateTime dt; + TimeToStruct(time, dt); + int year = dt.year; + int month = dt.mon; + int day = dt.day; + int hour = dt.hour; + // EU DST: Last Sunday in March to Last Sunday in October + // Starts/ends at 1:00 AM UTC + // Before March or after October - no DST + if(month < 3 || month > 10) return false; + // April to September - DST active + if(month > 3 && month < 10) return true; + // March - check if we're past last Sunday + if(month == 3) + { + int lastSunday = GetLastSunday(year, 3); + if(day > lastSunday) return true; + if(day == lastSunday && hour >= 1) return true; + return false; + } + // October - check if we're before last Sunday + if(month == 10) + { + int lastSunday = GetLastSunday(year, 10); + if(day < lastSunday) return true; + if(day == lastSunday && hour < 1) return true; + return false; + } + return false; +} +//+------------------------------------------------------------------+ +//| Check if UK DST (BST) is Active | +//+------------------------------------------------------------------+ +bool IsUKDST(datetime time) +{ + // UK follows EU DST rules + return IsEUDST(time); +} +//+------------------------------------------------------------------+ +//| Get Nth Sunday of Month | +//+------------------------------------------------------------------+ +int GetNthSunday(int year, int month, int n) +{ + MqlDateTime dt; + dt.year = year; + dt.mon = month; + dt.day = 1; + dt.hour = 12; + dt.min = 0; + dt.sec = 0; + datetime firstDay = StructToTime(dt); + TimeToStruct(firstDay, dt); + int dayOfWeek = dt.day_of_week; // 0 = Sunday + int daysToFirstSunday = (7 - dayOfWeek) % 7; + int nthSunday = 1 + daysToFirstSunday + (n - 1) * 7; + return nthSunday; +} +//+------------------------------------------------------------------+ +//| Get Last Sunday of Month | +//+------------------------------------------------------------------+ +int GetLastSunday(int year, int month) +{ + // Get last day of month + int lastDay; + if(month == 2) + { + // Leap year check + if((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) + lastDay = 29; + else + lastDay = 28; + } + else if(month == 4 || month == 6 || month == 9 || month == 11) + { + lastDay = 30; + } + else + { + lastDay = 31; + } + MqlDateTime dt; + dt.year = year; + dt.mon = month; + dt.day = lastDay; + dt.hour = 12; + dt.min = 0; + dt.sec = 0; + datetime lastDayTime = StructToTime(dt); + TimeToStruct(lastDayTime, dt); + int dayOfWeek = dt.day_of_week; + int lastSunday = lastDay - dayOfWeek; + return lastSunday; +} +//+------------------------------------------------------------------+ +//| Get Next DST Change Date | +//+------------------------------------------------------------------+ +datetime GetNextDSTChange(datetime currentTime) +{ + MqlDateTime dt; + TimeToStruct(currentTime, dt); + int year = dt.year; + int month = dt.mon; + datetime nextChange = 0; + if(DST_Mode == DST_US_RULES || DST_Mode == DST_AUTO_DETECT) + { + // US DST transitions + if(month < 3) + { + // Next is second Sunday in March + int day = GetNthSunday(year, 3, 2); + MqlDateTime change; + change.year = year; change.mon = 3; change.day = day; + change.hour = 2; change.min = 0; change.sec = 0; + nextChange = StructToTime(change); + } + else if(month < 11) + { + // Next is first Sunday in November + int day = GetNthSunday(year, 11, 1); + MqlDateTime change; + change.year = year; change.mon = 11; change.day = day; + change.hour = 2; change.min = 0; change.sec = 0; + nextChange = StructToTime(change); + } + else + { + // Next is second Sunday in March next year + int day = GetNthSunday(year + 1, 3, 2); + MqlDateTime change; + change.year = year + 1; change.mon = 3; change.day = day; + change.hour = 2; change.min = 0; change.sec = 0; + nextChange = StructToTime(change); + } + } + return nextChange; +} +//+------------------------------------------------------------------+ +//| Convert Broker Time to UTC | +//+------------------------------------------------------------------+ +datetime ConvertToUTC(datetime brokerTime) +{ + return brokerTime - g_dstInfo.brokerUTCOffset * 3600; +} +//+------------------------------------------------------------------+ +//| Convert UTC to Broker Time | +//+------------------------------------------------------------------+ +datetime ConvertFromUTC(datetime utcTime) +{ + return utcTime + g_dstInfo.brokerUTCOffset * 3600; +} +//+------------------------------------------------------------------+ +//| Get Adjusted Killzone Times | +//+------------------------------------------------------------------+ +void GetAdjustedKillzoneTimes(int kzIndex, int &startHour, int &startMin, + int &endHour, int &endMin) +{ + startHour = g_killzoneDefinitions[kzIndex].startHourUTC; + startMin = g_killzoneDefinitions[kzIndex].startMinuteUTC; + endHour = g_killzoneDefinitions[kzIndex].endHourUTC; + endMin = g_killzoneDefinitions[kzIndex].endMinuteUTC; + // Apply DST adjustments + int dstAdjustment = 0; + if(DST_Mode == DST_AUTO_DETECT) + { + if(g_dstInfo.isUSDST) + { + dstAdjustment += g_killzoneDefinitions[kzIndex].dstOffsetUS; + } + } + if(DST_Mode == DST_EU_RULES || DST_Mode == DST_UK_RULES || DST_Mode == DST_AUTO_DETECT) + { + if(g_dstInfo.isEUDST) + { + dstAdjustment += g_killzoneDefinitions[kzIndex].dstOffsetEU; + } + } + // Apply adjustment + startHour += dstAdjustment; + endHour += dstAdjustment; + // Handle hour wraparound + if(startHour < 0) startHour += 24; + if(startHour >= 24) startHour -= 24; + if(endHour < 0) endHour += 24; + if(endHour >= 24) endHour -= 24; + // Convert from UTC to broker time + startHour += g_dstInfo.brokerUTCOffset; + endHour += g_dstInfo.brokerUTCOffset; + // Handle hour wraparound again + if(startHour < 0) startHour += 24; + if(startHour >= 24) startHour -= 24; + if(endHour < 0) endHour += 24; + if(endHour >= 24) endHour -= 24; +} +//+------------------------------------------------------------------+ +//| Check if Time is in Killzone | +//+------------------------------------------------------------------+ +bool IsInKillzone(datetime time, ENUM_KILLZONE_TYPE &kzType) +{ + kzType = KZ_NONE; + if(!g_killzonesInitialized || g_numKillzones == 0) return false; + MqlDateTime dt; + TimeToStruct(time, dt); + int currentMinutes = dt.hour * 60 + dt.min; + for(int i = 0; i < g_numKillzones; i++) + { + if(!g_killzoneDefinitions[i].enabled) continue; + int startHour, startMin, endHour, endMin; + GetAdjustedKillzoneTimes(i, startHour, startMin, endHour, endMin); + int startMinutes = startHour * 60 + startMin; + int endMinutes = endHour * 60 + endMin; + bool isInKZ = false; + // Handle overnight sessions (e.g., Asian) + if(startMinutes > endMinutes) + { + // Killzone spans midnight + isInKZ = (currentMinutes >= startMinutes) || (currentMinutes < endMinutes); + } + else + { + isInKZ = (currentMinutes >= startMinutes) && (currentMinutes < endMinutes); + } + if(isInKZ) + { + kzType = g_killzoneDefinitions[i].type; + return true; + } + } + return false; +} +//+------------------------------------------------------------------+ +//| Check if in Any Killzone | +//+------------------------------------------------------------------+ +bool IsInAnyKillzone(datetime time) +{ + ENUM_KILLZONE_TYPE kzType; + return IsInKillzone(time, kzType); +} +//+------------------------------------------------------------------+ +//| Update Killzones | +//+------------------------------------------------------------------+ +void UpdateKillzones(datetime currentTime) +{ + if(!EnableKillzones || !g_killzonesInitialized) return; + // Check for DST changes + if(currentTime >= g_dstInfo.nextDSTChange) + { + InitializeDSTInfo(); + } + // Update current killzone status + ENUM_KILLZONE_TYPE currentKZ; + bool wasInKillzone = g_isInKillzone; + g_isInKillzone = IsInKillzone(currentTime, currentKZ); + if(g_isInKillzone) + { + // Find killzone index + for(int i = 0; i < g_numKillzones; i++) + { + if(g_killzoneDefinitions[i].type == currentKZ) + { + g_currentKillzoneIndex = i; + g_currentKillzoneName = g_killzoneDefinitions[i].name; + // Update active killzone data + if(!g_activeKillzones[i].isActive) + { + // Killzone just started + g_activeKillzones[i].isActive = true; + g_activeKillzones[i].type = currentKZ; + g_activeKillzones[i].startTime = currentTime; + double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); + g_activeKillzones[i].open = currentPrice; + g_activeKillzones[i].high = currentPrice; + g_activeKillzones[i].low = currentPrice; + g_activeKillzones[i].close = currentPrice; + g_activeKillzones[i].breakoutOccurred = false; + if(KZ_AlertOnEntry) + { + string msg = StringFormat("[TARGET] Entering %s Killzone", g_currentKillzoneName); + Alert(msg); + if(g_verboseLog) Print(msg); + } + } + else + { + // Update killzone high/low + double currentBid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + if(currentBid > g_activeKillzones[i].high) + g_activeKillzones[i].high = currentBid; + if(currentBid < g_activeKillzones[i].low) + g_activeKillzones[i].low = currentBid; + g_activeKillzones[i].close = currentBid; + g_activeKillzones[i].range = g_activeKillzones[i].high - g_activeKillzones[i].low; + // Check for breakout + if(!g_activeKillzones[i].breakoutOccurred) + { + double openPrice = g_activeKillzones[i].open; + double breakoutThreshold = g_cachedATR * 0.5; + if(currentBid > openPrice + breakoutThreshold) + { + g_activeKillzones[i].breakoutOccurred = true; + g_activeKillzones[i].direction = 1; // Bullish + if(KZ_AlertOnBreakout) + { + string msg = StringFormat("[UP] Bullish breakout in %s Killzone!", g_currentKillzoneName); + Alert(msg); + } + } + else if(currentBid < openPrice - breakoutThreshold) + { + g_activeKillzones[i].breakoutOccurred = true; + g_activeKillzones[i].direction = -1; // Bearish + if(KZ_AlertOnBreakout) + { + string msg = StringFormat("[DOWN] Bearish breakout in %s Killzone!", g_currentKillzoneName); + Alert(msg); + } + } + } + } + break; + } + } + } + else + { + // Not in any killzone + if(wasInKillzone && g_currentKillzoneIndex >= 0) + { + // Just exited a killzone + g_activeKillzones[g_currentKillzoneIndex].isActive = false; + g_activeKillzones[g_currentKillzoneIndex].endTime = currentTime; + // Log killzone statistics + if(g_verboseLog) + { + double range = g_activeKillzones[g_currentKillzoneIndex].range / g_pipValue; + string direction = (g_activeKillzones[g_currentKillzoneIndex].direction > 0) ? "Bullish" : + (g_activeKillzones[g_currentKillzoneIndex].direction < 0) ? "Bearish" : "Neutral"; + PrintFormat("[CHART] %s Killzone ended - Range: %.1f pips, Direction: %s", + g_currentKillzoneName, range, direction); + } + } + g_currentKillzoneIndex = -1; + g_currentKillzoneName = "NONE"; + } + g_lastKillzoneUpdate = currentTime; +} +//+------------------------------------------------------------------+ +//| Draw Killzone Boxes on Chart | +//+------------------------------------------------------------------+ +void DrawKillzones(const datetime &time[], const double &high[], const double &low[]) +{ + if(!EnableKillzones || !g_workingKZ_ShowBoxes) return; + // Get visible chart range + datetime visibleStart = (datetime)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR); + int visibleBars = (int)ChartGetInteger(0, CHART_VISIBLE_BARS); + MqlDateTime currentDt; + TimeToStruct(time[0], currentDt); + // Draw killzone boxes for current day + for(int kz = 0; kz < g_numKillzones; kz++) + { + if(!g_killzoneDefinitions[kz].enabled) continue; + // [v6.42] Session-specific toggle from inputs + string sessName = g_killzoneDefinitions[kz].name; + if(StringFind(sessName, "Asian") >= 0 && !KZ_ShowAsianSession) continue; + if(StringFind(sessName, "London Open") >= 0 && !KZ_ShowLondonOpen) continue; + if(StringFind(sessName, "London Close") >= 0 && !KZ_ShowLondonClose) continue; + if(StringFind(sessName, "NY Open") >= 0 && !KZ_ShowNYOpen) continue; + if(StringFind(sessName, "NY Lunch") >= 0 && !KZ_ShowNYLunch) continue; + if(StringFind(sessName, "NY Close") >= 0 && !KZ_ShowNYClose) continue; + if(StringFind(sessName, "Silver") >= 0 && !KZ_ShowSilverBullet) continue; + int startHour, startMin, endHour, endMin; + GetAdjustedKillzoneTimes(kz, startHour, startMin, endHour, endMin); + // Calculate killzone start/end times for today + MqlDateTime kzStart, kzEnd; + kzStart = currentDt; + kzStart.hour = startHour; + kzStart.min = startMin; + kzStart.sec = 0; + kzEnd = currentDt; + kzEnd.hour = endHour; + kzEnd.min = endMin; + kzEnd.sec = 0; + // Handle overnight sessions + if(startHour > endHour) + { + // If we're after midnight, adjust start to previous day + if(currentDt.hour < endHour) + { + kzStart.day--; + } + else + { + kzEnd.day++; + } + } + datetime kzStartTime = StructToTime(kzStart); + datetime kzEndTime = StructToTime(kzEnd); + // Find price range within killzone + double kzHigh = 0; + double kzLow = DBL_MAX; + bool hasData = false; + for(int i = 0; i < ArraySize(time) && i < 500; i++) + { + if(time[i] >= kzStartTime && time[i] <= kzEndTime) + { + if(high[i] > kzHigh) kzHigh = high[i]; + if(low[i] < kzLow) kzLow = low[i]; + hasData = true; + } + if(time[i] < kzStartTime) break; + } + if(!hasData) continue; + // Create/update killzone rectangle + string objName = "ICT_KZ_" + g_killzoneDefinitions[kz].name + "_" + + IntegerToString(currentDt.year) + + IntegerToString(currentDt.mon) + + IntegerToString(currentDt.day); + if(ObjectFind(0, objName) < 0) + { + ObjectCreate(0, objName, OBJ_RECTANGLE, 0, kzStartTime, kzHigh, kzEndTime, kzLow); + } + else + { + ObjectSetInteger(0, objName, OBJPROP_TIME, 0, kzStartTime); + ObjectSetDouble(0, objName, OBJPROP_PRICE, 0, kzHigh); + ObjectSetInteger(0, objName, OBJPROP_TIME, 1, kzEndTime); + ObjectSetDouble(0, objName, OBJPROP_PRICE, 1, kzLow); + } + // Set appearance + color kzColor = g_killzoneDefinitions[kz].zoneColor; + // [v6.42] Override with KZ color inputs based on session name + string kzName = g_killzoneDefinitions[kz].name; + if(StringFind(kzName, "Asian") >= 0) kzColor = KZ_AsianColor; + else if(StringFind(kzName, "London") >= 0) kzColor = KZ_LondonColor; + else if(StringFind(kzName, "NY") >= 0 || StringFind(kzName, "New York") >= 0) kzColor = KZ_NewYorkColor; + else if(StringFind(kzName, "Silver") >= 0) kzColor = KZ_SilverBulletColor; + ObjectSetInteger(0, objName, OBJPROP_COLOR, kzColor); + ObjectSetInteger(0, objName, OBJPROP_FILL, true); + ObjectSetInteger(0, objName, OBJPROP_BACK, true); + ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false); + ObjectSetInteger(0, objName, OBJPROP_HIDDEN, true); + // [v6.42] KZ_Transparency + if(KZ_Transparency > 0 && KZ_Transparency < 100) + { + ObjectSetInteger(0, objName, OBJPROP_FILL, true); + } + // Store object name + g_activeKillzones[kz].objectName = objName; + // Add label if enabled + if(KZ_ShowLabels) + { + string labelName = objName + "_Label"; + if(ObjectFind(0, labelName) < 0) + { + ObjectCreate(0, labelName, OBJ_TEXT, 0, kzStartTime, kzHigh); + } + ObjectSetInteger(0, labelName, OBJPROP_TIME, kzStartTime); + ObjectSetDouble(0, labelName, OBJPROP_PRICE, kzHigh + 5 * g_pipValue); + ObjectSetString(0, labelName, OBJPROP_TEXT, g_killzoneDefinitions[kz].name); + ObjectSetInteger(0, labelName, OBJPROP_COLOR, kzColor); + ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 8); + ObjectSetString(0, labelName, OBJPROP_FONT, "Arial"); + } + } +} +//+------------------------------------------------------------------+ +//| Delete Killzone Objects | +//+------------------------------------------------------------------+ +void DeleteKillzoneObjects() +{ + int totalObjects = ObjectsTotal(0, 0, -1); + for(int i = totalObjects - 1; i >= 0; i--) + { + string objName = ObjectName(0, i, 0, -1); + if(StringFind(objName, "ICT_KZ_") >= 0) + { + ObjectDelete(0, objName); + } + } +} +//+------------------------------------------------------------------+ +//| Get Killzone Quality Bonus | +//+------------------------------------------------------------------+ +double GetKillzoneQualityBonus(ENUM_KILLZONE_TYPE kzType) +{ + // Higher quality bonus for high-probability killzones + switch(kzType) + { + case KZ_LONDON_OPEN: + return 0.15; + case KZ_NY_OPEN: + return 0.15; + case KZ_SILVER_BULLET_LDN: + case KZ_SILVER_BULLET_NY_AM: + case KZ_SILVER_BULLET_NY_PM: + return 0.20; // Silver Bullet is highest probability + case KZ_LONDON_CLOSE: + case KZ_NY_CLOSE: + return 0.10; + case KZ_NY_LUNCH: + return -0.05; // Lower probability during lunch + case KZ_ASIAN: + return 0.05; + default: + return 0.0; + } +} +//+------------------------------------------------------------------+ +//| Check Killzone Trade Filter | +//+------------------------------------------------------------------+ +bool PassesKillzoneFilter() +{ + if(!EnableKillzones) return true; + if(!KZ_OnlyTradeInKillzones) return true; + return g_isInKillzone; +} +//+------------------------------------------------------------------+ +//| Get Current Killzone Name | +//+------------------------------------------------------------------+ +string GetCurrentKillzoneName() +{ + if(!g_isInKillzone) return "NONE"; + return g_currentKillzoneName; +} +//+------------------------------------------------------------------+ +//| Get Killzone Historical Win Rate | +//+------------------------------------------------------------------+ +double GetKillzoneWinRate(int kzIndex) +{ + if(kzIndex < 0 || kzIndex >= g_numKillzones) return 0; + int total = g_kzWins[kzIndex] + g_kzLosses[kzIndex]; + if(total == 0) return 0; + return (double)g_kzWins[kzIndex] / total * 100.0; +} +//+------------------------------------------------------------------+ +//| Update Killzone Statistics | +//+------------------------------------------------------------------+ +void UpdateKillzoneStats(int kzIndex, bool isWin, double pips) +{ + if(kzIndex < 0 || kzIndex >= g_numKillzones) return; + g_kzTradesCount[kzIndex]++; + if(isWin) + g_kzWins[kzIndex]++; + else + g_kzLosses[kzIndex]++; + // Update average pips + double totalPips = g_kzAvgPips[kzIndex] * (g_kzTradesCount[kzIndex] - 1); + totalPips += pips; + g_kzAvgPips[kzIndex] = totalPips / g_kzTradesCount[kzIndex]; +} +//+------------------------------------------------------------------+ +//| [NEW] PERFORMANCE PERSISTENCE IMPLEMENTATION v5.0 | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| Initialize Persistence System | +//+------------------------------------------------------------------+ +bool InitializePersistence() +{ + // Create data folder path + g_dataFolderPath = DataFolder; + if(!CreateDataFolder()) + { + Print("[X] Failed to create data folder: ", g_dataFolderPath); + return false; + } + // Initialize performance data structure + ZeroMemory(g_perfData); + g_perfData.version = "5.0"; + g_perfData.totalTrades = 0; + g_perfData.totalWins = 0; + g_perfData.totalLosses = 0; + g_perfData.totalBreakeven = 0; + g_perfData.overallWinRate = 0; + g_perfData.avgWinPips = 0; + g_perfData.avgLossPips = 0; + g_perfData.profitFactor = 0; + g_perfData.expectancy = 0; + g_perfData.maxDrawdown = 0; + g_perfData.maxConsecutiveLosses = 0; + g_perfData.totalProfitPips = 0; + g_perfData.totalLossPips = 0; + g_perfData.sharpeRatio = 0; + g_perfData.sortinoRatio = 0; + g_perfData.mlAccuracy = 0; + g_perfData.firstTradeDate = 0; + g_perfData.lastTradeDate = 0; + g_perfData.lastSaveTime = 0; + // Initialize trade history array + if(ArrayResize(g_tradeHistory, 0) < 0) + { + Print("[X] Failed to initialize trade history array"); + return false; + } + g_tradeHistoryCount = 0; + // Initialize daily performance array (static array - no resize needed) + for(int i = 0; i < MAX_DAILY_STATS; i++) + { + ZeroMemory(g_perfData.dailyStats[i]); + } + // Initialize strategy stats + for(int i = 0; i < MAX_STRATEGY_STATS; i++) + { + ZeroMemory(g_perfData.strategyStats[i]); + g_perfData.strategyStats[i].name = g_strategyNames[i]; + } + g_perfDataLoaded = false; + g_lastPerfSave = TimeCurrent(); + Print("[OK] Persistence system initialized"); + Print(" Data folder: ", g_dataFolderPath); + return true; +} +//+------------------------------------------------------------------+ +//| Create Data Folder | +//+------------------------------------------------------------------+ +bool CreateDataFolder() +{ + string fullPath = TerminalInfoString(TERMINAL_DATA_PATH) + "\\MQL5\\Files\\" + g_dataFolderPath; + // Check if folder exists + if(FolderCreate(g_dataFolderPath, FILE_COMMON)) + { + return true; + } + // Folder might already exist + return true; +} +//+------------------------------------------------------------------+ +//| Get Full Data File Path | +//+------------------------------------------------------------------+ +string GetDataFilePath(string filename) +{ + return g_dataFolderPath + "\\" + _Symbol + "_" + filename; +} +//+------------------------------------------------------------------+ +//| Save Performance Data | +//+------------------------------------------------------------------+ +bool SavePerformanceData() +{ + if(!EnableDataPersistence) return false; + // Backup existing file if enabled + if(BackupBeforeSave) + { + BackupPerformanceFile(); + } + string filepath = GetDataFilePath(PERF_DATA_FILE); + int handle = FileOpen(filepath, FILE_WRITE|FILE_BIN|FILE_COMMON); + if(handle == INVALID_HANDLE) + { + Print("[X] Failed to open file for saving: ", filepath); + Print(" Error: ", GetLastError()); + return false; + } + // =============================================================== + // WRITE HEADER + // =============================================================== + string header = "ICT_PERF_v5.0"; + FileWriteString(handle, header, 16); + FileWriteInteger(handle, PERSISTENCE_VERSION); + // =============================================================== + // WRITE CUMULATIVE STATS + // =============================================================== + FileWriteInteger(handle, g_perfData.totalTrades); + FileWriteInteger(handle, g_perfData.totalWins); + FileWriteInteger(handle, g_perfData.totalLosses); + FileWriteInteger(handle, g_perfData.totalBreakeven); + FileWriteDouble(handle, g_perfData.overallWinRate); + FileWriteDouble(handle, g_perfData.avgWinPips); + FileWriteDouble(handle, g_perfData.avgLossPips); + FileWriteDouble(handle, g_perfData.profitFactor); + FileWriteDouble(handle, g_perfData.expectancy); + FileWriteDouble(handle, g_perfData.maxDrawdown); + FileWriteInteger(handle, g_perfData.maxConsecutiveLosses); + FileWriteDouble(handle, g_perfData.totalProfitPips); + FileWriteDouble(handle, g_perfData.totalLossPips); + FileWriteDouble(handle, g_perfData.sharpeRatio); + FileWriteDouble(handle, g_perfData.sortinoRatio); + FileWriteDouble(handle, g_perfData.mlAccuracy); + // =============================================================== + // WRITE QUALITY CORRELATION + // =============================================================== + FileWriteInteger(handle, g_perfData.highQualityWins); + FileWriteInteger(handle, g_perfData.highQualityLosses); + FileWriteInteger(handle, g_perfData.mediumQualityWins); + FileWriteInteger(handle, g_perfData.mediumQualityLosses); + FileWriteInteger(handle, g_perfData.lowQualityWins); + FileWriteInteger(handle, g_perfData.lowQualityLosses); + // =============================================================== + // WRITE STRATEGY STATS + // =============================================================== + FileWriteInteger(handle, MAX_STRATEGY_STATS); + for(int i = 0; i < MAX_STRATEGY_STATS; i++) + { + FileWriteString(handle, g_perfData.strategyStats[i].name, 32); + FileWriteInteger(handle, g_perfData.strategyStats[i].totalTrades); + FileWriteInteger(handle, g_perfData.strategyStats[i].wins); + FileWriteInteger(handle, g_perfData.strategyStats[i].losses); + FileWriteInteger(handle, g_perfData.strategyStats[i].breakeven); + FileWriteDouble(handle, g_perfData.strategyStats[i].winRate); + FileWriteDouble(handle, g_perfData.strategyStats[i].avgWin); + FileWriteDouble(handle, g_perfData.strategyStats[i].avgLoss); + FileWriteDouble(handle, g_perfData.strategyStats[i].profitFactor); + FileWriteDouble(handle, g_perfData.strategyStats[i].expectancy); + FileWriteDouble(handle, g_perfData.strategyStats[i].maxDrawdown); + FileWriteInteger(handle, g_perfData.strategyStats[i].maxConsecutiveLosses); + FileWriteDouble(handle, g_perfData.strategyStats[i].totalProfitPips); + FileWriteDouble(handle, g_perfData.strategyStats[i].totalLossPips); + FileWriteDouble(handle, g_perfData.strategyStats[i].sharpeRatio); + FileWriteDouble(handle, g_perfData.strategyStats[i].sortinoRatio); + FileWriteLong(handle, g_perfData.strategyStats[i].lastTradeTime); + } + // =============================================================== + // WRITE DAILY STATS + // =============================================================== + int dailyCount = GetDailyStatsCount(); + FileWriteInteger(handle, dailyCount); + for(int i = 0; i < dailyCount && i < MAX_DAILY_STATS; i++) + { + FileWriteLong(handle, g_perfData.dailyStats[i].date); + FileWriteInteger(handle, g_perfData.dailyStats[i].trades); + FileWriteInteger(handle, g_perfData.dailyStats[i].wins); + FileWriteInteger(handle, g_perfData.dailyStats[i].losses); + FileWriteDouble(handle, g_perfData.dailyStats[i].profitPips); // [OK] ΣΩΣΤΟ + FileWriteDouble(handle, g_perfData.dailyStats[i].profitMoney); // [OK] ΣΩΣΤΟ + FileWriteDouble(handle, g_perfData.dailyStats[i].maxDrawdown); + FileWriteDouble(handle, g_perfData.dailyStats[i].peakEquity); + FileWriteString(handle, g_perfData.dailyStats[i].bestStrategy, 32); + FileWriteString(handle, g_perfData.dailyStats[i].bestKillzone, 32); + } + // =============================================================== + // WRITE METADATA + // =============================================================== + FileWriteLong(handle, g_perfData.firstTradeDate); + FileWriteLong(handle, g_perfData.lastTradeDate); + FileWriteLong(handle, TimeCurrent()); + FileWriteString(handle, g_perfData.version, 8); + // =============================================================== + // WRITE KILLZONE STATS + // =============================================================== + FileWriteInteger(handle, 9); // Number of killzones + for(int i = 0; i < 9; i++) + { + FileWriteInteger(handle, g_kzTradesCount[i]); + FileWriteInteger(handle, g_kzWins[i]); + FileWriteInteger(handle, g_kzLosses[i]); + FileWriteDouble(handle, g_kzAvgPips[i]); + } + FileClose(handle); + g_lastPerfSave = TimeCurrent(); + g_perfData.lastSaveTime = g_lastPerfSave; + if(g_verboseLog) + { + Print("[OK] Performance data saved to: ", filepath); + } + // Also export trade history to CSV + if(EnableTradeJournal) + { + ExportTradeHistory(); + } + return true; +} +//+------------------------------------------------------------------+ +//| Load Performance Data | +//+------------------------------------------------------------------+ +bool LoadPerformanceData() +{ + if(!EnableDataPersistence) return false; + string filepath = GetDataFilePath(PERF_DATA_FILE); + if(!FileIsExist(filepath, FILE_COMMON)) + { + if(g_verboseLog) Print("[i] No saved performance data found"); + return false; + } + int handle = FileOpen(filepath, FILE_READ|FILE_BIN|FILE_COMMON); + if(handle == INVALID_HANDLE) + { + Print("[X] Failed to open file for loading: ", filepath); + return false; + } + // =============================================================== + // READ AND VERIFY HEADER + // =============================================================== + string header = FileReadString(handle, 16); + if(StringFind(header, "ICT_PERF_v5") < 0) + { + Print("[X] Invalid or incompatible performance file format"); + Print(" Expected: ICT_PERF_v5, Found: ", header); + FileClose(handle); + return false; + } + int version = FileReadInteger(handle); + if(version != PERSISTENCE_VERSION) + { + Print("[WARN] Performance file version mismatch. Attempting migration..."); + FileClose(handle); + return MigratePerformanceData(filepath, version); + } + // =============================================================== + // READ CUMULATIVE STATS + // =============================================================== + g_perfData.totalTrades = FileReadInteger(handle); + g_perfData.totalWins = FileReadInteger(handle); + g_perfData.totalLosses = FileReadInteger(handle); + g_perfData.totalBreakeven = FileReadInteger(handle); + g_perfData.overallWinRate = FileReadDouble(handle); + g_perfData.avgWinPips = FileReadDouble(handle); + g_perfData.avgLossPips = FileReadDouble(handle); + g_perfData.profitFactor = FileReadDouble(handle); + g_perfData.expectancy = FileReadDouble(handle); + g_perfData.maxDrawdown = FileReadDouble(handle); + g_perfData.maxConsecutiveLosses = FileReadInteger(handle); + g_perfData.totalProfitPips = FileReadDouble(handle); + g_perfData.totalLossPips = FileReadDouble(handle); + g_perfData.sharpeRatio = FileReadDouble(handle); + g_perfData.sortinoRatio = FileReadDouble(handle); + g_perfData.mlAccuracy = FileReadDouble(handle); + // =============================================================== + // READ QUALITY CORRELATION + // =============================================================== + g_perfData.highQualityWins = FileReadInteger(handle); + g_perfData.highQualityLosses = FileReadInteger(handle); + g_perfData.mediumQualityWins = FileReadInteger(handle); + g_perfData.mediumQualityLosses = FileReadInteger(handle); + g_perfData.lowQualityWins = FileReadInteger(handle); + g_perfData.lowQualityLosses = FileReadInteger(handle); + // Update global tracking variables + g_highQualityWins = g_perfData.highQualityWins; + g_highQualityLosses = g_perfData.highQualityLosses; + g_mediumQualityWins = g_perfData.mediumQualityWins; + g_mediumQualityLosses = g_perfData.mediumQualityLosses; + g_lowQualityWins = g_perfData.lowQualityWins; + g_lowQualityLosses = g_perfData.lowQualityLosses; + g_totalWins = g_perfData.totalWins; + g_totalLosses = g_perfData.totalLosses; + g_totalProfitPips = g_perfData.totalProfitPips; + g_totalLossPips = g_perfData.totalLossPips; + // =============================================================== + // READ STRATEGY STATS + // =============================================================== + int numStrategies = FileReadInteger(handle); + for(int i = 0; i < numStrategies && i < MAX_STRATEGY_STATS; i++) + { + g_perfData.strategyStats[i].name = FileReadString(handle, 32); + g_perfData.strategyStats[i].totalTrades = FileReadInteger(handle); + g_perfData.strategyStats[i].wins = FileReadInteger(handle); + g_perfData.strategyStats[i].losses = FileReadInteger(handle); + g_perfData.strategyStats[i].breakeven = FileReadInteger(handle); + g_perfData.strategyStats[i].winRate = FileReadDouble(handle); + g_perfData.strategyStats[i].avgWin = FileReadDouble(handle); + g_perfData.strategyStats[i].avgLoss = FileReadDouble(handle); + g_perfData.strategyStats[i].profitFactor = FileReadDouble(handle); + g_perfData.strategyStats[i].expectancy = FileReadDouble(handle); + g_perfData.strategyStats[i].maxDrawdown = FileReadDouble(handle); + g_perfData.strategyStats[i].maxConsecutiveLosses = FileReadInteger(handle); + g_perfData.strategyStats[i].totalProfitPips = FileReadDouble(handle); + g_perfData.strategyStats[i].totalLossPips = FileReadDouble(handle); + g_perfData.strategyStats[i].sharpeRatio = FileReadDouble(handle); + g_perfData.strategyStats[i].sortinoRatio = FileReadDouble(handle); + g_perfData.strategyStats[i].lastTradeTime = (datetime)FileReadLong(handle); + // Copy to global strategy performance + g_strategyPerf[i] = g_perfData.strategyStats[i]; + } + // =============================================================== + // READ DAILY STATS + // =============================================================== + int dailyCount = FileReadInteger(handle); + for(int i = 0; i < dailyCount && i < MAX_DAILY_STATS; i++) + { + g_perfData.dailyStats[i].date = (datetime)FileReadLong(handle); + g_perfData.dailyStats[i].trades = FileReadInteger(handle); + g_perfData.dailyStats[i].wins = FileReadInteger(handle); + g_perfData.dailyStats[i].losses = FileReadInteger(handle); + g_perfData.dailyStats[i].profitPips = FileReadDouble(handle); // [OK] ΣΩΣΤΟ + g_perfData.dailyStats[i].profitMoney = FileReadDouble(handle); // [OK] ΣΩΣΤΟ + g_perfData.dailyStats[i].maxDrawdown = FileReadDouble(handle); + g_perfData.dailyStats[i].peakEquity = FileReadDouble(handle); + g_perfData.dailyStats[i].bestStrategy = FileReadString(handle, 32); + g_perfData.dailyStats[i].bestKillzone = FileReadString(handle, 32); + } + // =============================================================== + // READ METADATA + // =============================================================== + g_perfData.firstTradeDate = (datetime)FileReadLong(handle); + g_perfData.lastTradeDate = (datetime)FileReadLong(handle); + g_perfData.lastSaveTime = (datetime)FileReadLong(handle); + g_perfData.version = FileReadString(handle, 8); + // =============================================================== + // READ KILLZONE STATS + // =============================================================== + int numKZ = FileReadInteger(handle); + for(int i = 0; i < numKZ && i < 9; i++) + { + g_kzTradesCount[i] = FileReadInteger(handle); + g_kzWins[i] = FileReadInteger(handle); + g_kzLosses[i] = FileReadInteger(handle); + g_kzAvgPips[i] = FileReadDouble(handle); + } + FileClose(handle); + g_perfDataLoaded = true; + if(g_verboseLog) + { + Print("[OK] Performance data loaded from: ", filepath); + } + // Load trade history from CSV + LoadTradeHistory(); + return true; +} +//+------------------------------------------------------------------+ +//| Migrate Performance Data from Older Version | +//+------------------------------------------------------------------+ +bool MigratePerformanceData(string filepath, int oldVersion) +{ + Print("[PKG] Migrating performance data from version ", oldVersion, " to ", PERSISTENCE_VERSION); + // For now, just reinitialize + // In production, you would read the old format and convert + Print("[WARN] Data migration not yet implemented. Starting fresh."); + return false; +} +//+------------------------------------------------------------------+ +//| Backup Performance File | +//+------------------------------------------------------------------+ +void BackupPerformanceFile() +{ + string filepath = GetDataFilePath(PERF_DATA_FILE); + if(!FileIsExist(filepath, FILE_COMMON)) return; + MqlDateTime dt; + TimeToStruct(TimeCurrent(), dt); + string backupName = StringFormat("%s_backup_%04d%02d%02d_%02d%02d.dat", + PERF_DATA_FILE, + dt.year, dt.mon, dt.day, + dt.hour, dt.min); + string backupPath = GetDataFilePath(backupName); + if(FileCopy(filepath, FILE_COMMON, backupPath, FILE_COMMON)) + { + if(g_verboseLog) Print("[OK] Backup created: ", backupPath); + } +} +//+------------------------------------------------------------------+ +//| Save Trade to History | +//+------------------------------------------------------------------+ +bool SaveTradeToHistory(TradeRecord &trade) +{ + // Add to memory array + int newSize = ArrayResize(g_tradeHistory, g_tradeHistoryCount + 1); + if(newSize < 0) + { + Print("[X] Failed to resize trade history array"); + return false; + } + g_tradeHistory[g_tradeHistoryCount] = trade; + g_tradeHistoryCount++; + // Limit array size + if(g_tradeHistoryCount > MAX_TRADE_HISTORY) + { + // Remove oldest trades + int removeCount = g_tradeHistoryCount - MAX_TRADE_HISTORY; + for(int i = 0; i < MAX_TRADE_HISTORY; i++) + { + g_tradeHistory[i] = g_tradeHistory[i + removeCount]; + } + ArrayResize(g_tradeHistory, MAX_TRADE_HISTORY); + g_tradeHistoryCount = MAX_TRADE_HISTORY; + } + // Update performance statistics + UpdatePerformanceFromTrade(trade); + // Update daily stats + UpdateDailyStats(trade); + // Update strategy stats + UpdateStrategyStats(trade); + // Update killzone stats + if(trade.killzoneType != KZ_NONE) + { + for(int i = 0; i < g_numKillzones; i++) + { + if(g_killzoneDefinitions[i].type == trade.killzoneType) + { + UpdateKillzoneStats(i, trade.result == "WIN", trade.pnlPips); + break; + } + } + } + // Auto-save if interval reached + datetime currentTime = TimeCurrent(); + if(AutoSaveInterval > 0 && (currentTime - g_lastPerfSave) >= AutoSaveInterval * 60) + { + SavePerformanceData(); + } + return true; +} +//+------------------------------------------------------------------+ +//| Update Performance from Trade | +//+------------------------------------------------------------------+ +void UpdatePerformanceFromTrade(TradeRecord &trade) +{ + g_perfData.totalTrades++; + if(trade.result == "WIN") + { + g_perfData.totalWins++; + g_perfData.totalProfitPips += trade.pnlPips; + // Update quality correlation + if(trade.quality >= 80) + g_perfData.highQualityWins++; + else if(trade.quality >= 60) + g_perfData.mediumQualityWins++; + else + g_perfData.lowQualityWins++; + } + else if(trade.result == "LOSS") + { + g_perfData.totalLosses++; + g_perfData.totalLossPips += MathAbs(trade.pnlPips); + // Update quality correlation + if(trade.quality >= 80) + g_perfData.highQualityLosses++; + else if(trade.quality >= 60) + g_perfData.mediumQualityLosses++; + else + g_perfData.lowQualityLosses++; + } + else + { + g_perfData.totalBreakeven++; + } + // * v9.13 FIX#25: DD calculation was using pips-based ratio, producing absurd values. + // Old code: runningPnl = totalProfitPips - totalLossPips (in PIPS), + // ddPercent = (currentDD / peakPnl) * 100 -> if peakPnl=5p and DD=5.2p -> 104%! + // The "Max Drawdown: 108.19%" seen in backtest was caused by this pip/pip division. + // Fix: use balance-relative DD. Convert pip DD to money using avgPipValue, then + // express as % of account balance at the time of the drawdown peak. + { + double runningPnl = g_perfData.totalProfitPips - g_perfData.totalLossPips; + static double peakPnlDD25 = 0; + static double peakBalanceDD25 = 0; + double balance = AccountInfoDouble(ACCOUNT_BALANCE); + if(balance <= 0) balance = 10000.0; // fallback for backtest/tester + if(runningPnl > peakPnlDD25) + { + peakPnlDD25 = runningPnl; + peakBalanceDD25 = balance; + } + double currentDD25 = peakPnlDD25 - runningPnl; + // Convert pip DD to % of balance using a pip value estimate + // 1 pip on standard lot EURUSD ~= $10. Use riskPercent/riskPips ratio instead. + // Better: use the ratio of (total trades * avgRisk%) to express DD as % balance. + if(peakPnlDD25 > 0 && currentDD25 > 0) + { + // Estimate: each pip lost represents EA_RiskPercent of balance per trade. + // Use totalLossPips / (totalLosses * 1/risk%) as DD denominator. + double riskPerTrade = EA_RiskPercent / 100.0; // e.g. 0.01 for 1% + double avgSLPips = (g_perfData.totalLosses > 0 && riskPerTrade > 0) + ? (g_perfData.totalLossPips / g_perfData.totalLosses) : 50.0; + // Each SL hit = riskPerTrade of balance. DD in % = (DD_pips / avgSLPips) * riskPerTrade * 100 + if(avgSLPips > 0) + { + double ddPercent = (currentDD25 / avgSLPips) * riskPerTrade * 100.0; + if(ddPercent > g_perfData.maxDrawdown) + g_perfData.maxDrawdown = ddPercent; + } + } + } + // Update timestamps + if(g_perfData.firstTradeDate == 0) + g_perfData.firstTradeDate = trade.entryTime; + g_perfData.lastTradeDate = trade.exitTime; + // Recalculate derived statistics + RecalculatePerformanceStats(); +} +//+------------------------------------------------------------------+ +//| Recalculate Performance Statistics | +//+------------------------------------------------------------------+ +void RecalculatePerformanceStats() +{ + // Win Rate + if(g_perfData.totalTrades > 0) + { + g_perfData.overallWinRate = (double)g_perfData.totalWins / + (g_perfData.totalWins + g_perfData.totalLosses) * 100.0; + } + // Average Win/Loss + if(g_perfData.totalWins > 0) + { + g_perfData.avgWinPips = g_perfData.totalProfitPips / g_perfData.totalWins; + } + if(g_perfData.totalLosses > 0) + { + g_perfData.avgLossPips = g_perfData.totalLossPips / g_perfData.totalLosses; + } + // Profit Factor + if(g_perfData.totalLossPips > 0) + { + g_perfData.profitFactor = g_perfData.totalProfitPips / g_perfData.totalLossPips; + } + // Expectancy + if(g_perfData.totalTrades > 0) + { + double winRate = g_perfData.overallWinRate / 100.0; + double lossRate = 1.0 - winRate; + g_perfData.expectancy = (winRate * g_perfData.avgWinPips) - (lossRate * g_perfData.avgLossPips); + } + // * v9.59 FIX#230: Ensure maxDrawdown reflects real equity-based DD from the DD protection + // system. The per-trade pips-based calculation (line ~12627) missed floating losses on open + // positions. g_currentTotalDD is updated every tick by CheckWeeklyTotalDrawdownLimit and + // already uses MathMin(balance,equity) → always reflects true worst-case DD. + if(g_currentTotalDD > g_perfData.maxDrawdown) + g_perfData.maxDrawdown = g_currentTotalDD; + // Sharpe and Sortino ratios (simplified) + CalculateRiskAdjustedReturns(); +} +//+------------------------------------------------------------------+ +//| Calculate Risk-Adjusted Returns | +//+------------------------------------------------------------------+ +void CalculateRiskAdjustedReturns() +{ + if(g_tradeHistoryCount < 10) return; + // Calculate returns array + double returns[]; + ArrayResize(returns, g_tradeHistoryCount); + double sumReturns = 0; + double sumSquaredReturns = 0; + double sumNegativeSquaredReturns = 0; + int negativeCount = 0; + for(int i = 0; i < g_tradeHistoryCount; i++) + { + returns[i] = g_tradeHistory[i].pnlPips; + sumReturns += returns[i]; + sumSquaredReturns += returns[i] * returns[i]; + if(returns[i] < 0) + { + sumNegativeSquaredReturns += returns[i] * returns[i]; + negativeCount++; + } + } + double avgReturn = sumReturns / g_tradeHistoryCount; + double variance = (sumSquaredReturns / g_tradeHistoryCount) - (avgReturn * avgReturn); + double stdDev = MathSqrt(variance); + // Sharpe Ratio (assuming 0 risk-free rate for simplicity) + if(stdDev > 0) + { + g_perfData.sharpeRatio = avgReturn / stdDev; + } + // Sortino Ratio (using downside deviation) + double downsideVariance = (negativeCount > 0) ? sumNegativeSquaredReturns / negativeCount : 0; + double downsideDeviation = MathSqrt(downsideVariance); + if(downsideDeviation > 0) + { + g_perfData.sortinoRatio = avgReturn / downsideDeviation; + } + ArrayFree(returns); +} +//+------------------------------------------------------------------+ +//| Update Daily Statistics | +//+------------------------------------------------------------------+ +void UpdateDailyStats(TradeRecord &trade) +{ + // Get trade date (without time) + MqlDateTime dt; + TimeToStruct(trade.exitTime, dt); + dt.hour = 0; + dt.min = 0; + dt.sec = 0; + datetime tradeDate = StructToTime(dt); + // Find or create daily entry + int dayIndex = -1; + for(int i = 0; i < MAX_DAILY_STATS; i++) + { + if(g_perfData.dailyStats[i].date == tradeDate) + { + dayIndex = i; + break; + } + else if(g_perfData.dailyStats[i].date == 0) + { + dayIndex = i; + g_perfData.dailyStats[i].date = tradeDate; + break; + } + } + if(dayIndex < 0) + { + // Array is full, shift entries + for(int i = 0; i < MAX_DAILY_STATS - 1; i++) + { + g_perfData.dailyStats[i] = g_perfData.dailyStats[i + 1]; + } + dayIndex = MAX_DAILY_STATS - 1; + ZeroMemory(g_perfData.dailyStats[dayIndex]); + g_perfData.dailyStats[dayIndex].date = tradeDate; + } + // [OK] Update daily stats - ΣΩΣΤΑ ΟΝΟΜΑΤΑ + g_perfData.dailyStats[dayIndex].trades++; + g_perfData.dailyStats[dayIndex].profitPips += trade.pnlPips; // [OK] ΣΩΣΤΟ + g_perfData.dailyStats[dayIndex].profitMoney += trade.pnlMoney; // [OK] ΣΩΣΤΟ + if(trade.result == "WIN") + { + g_perfData.dailyStats[dayIndex].wins++; + } + else if(trade.result == "LOSS") + { + g_perfData.dailyStats[dayIndex].losses++; + } + // Update max drawdown + if(trade.maxDrawdown > g_perfData.dailyStats[dayIndex].maxDrawdown) + { + g_perfData.dailyStats[dayIndex].maxDrawdown = trade.maxDrawdown; + } +} +//+------------------------------------------------------------------+ +//| Update Strategy Statistics | +//+------------------------------------------------------------------+ +void UpdateStrategyStats(TradeRecord &trade) +{ + int stratIdx = GetStrategyIndex(trade.strategy); + if(stratIdx < 0 || stratIdx >= MAX_STRATEGY_STATS) return; + // Direct access without GetPointer + g_perfData.strategyStats[stratIdx].totalTrades++; + g_perfData.strategyStats[stratIdx].lastTradeTime = trade.exitTime; + if(trade.result == "WIN") + { + g_perfData.strategyStats[stratIdx].wins++; + g_perfData.strategyStats[stratIdx].totalProfitPips += trade.pnlPips; + // Update average win + if(g_perfData.strategyStats[stratIdx].wins > 0) + { + g_perfData.strategyStats[stratIdx].avgWin = + g_perfData.strategyStats[stratIdx].totalProfitPips / + g_perfData.strategyStats[stratIdx].wins; + } + } + else if(trade.result == "LOSS") + { + g_perfData.strategyStats[stratIdx].losses++; + g_perfData.strategyStats[stratIdx].totalLossPips += MathAbs(trade.pnlPips); + // Update average loss + if(g_perfData.strategyStats[stratIdx].losses > 0) + { + g_perfData.strategyStats[stratIdx].avgLoss = + g_perfData.strategyStats[stratIdx].totalLossPips / + g_perfData.strategyStats[stratIdx].losses; + } + } + else + { + g_perfData.strategyStats[stratIdx].breakeven++; + } + // Recalculate derived stats + int totalDecided = g_perfData.strategyStats[stratIdx].wins + + g_perfData.strategyStats[stratIdx].losses; + if(totalDecided > 0) + { + g_perfData.strategyStats[stratIdx].winRate = + (double)g_perfData.strategyStats[stratIdx].wins / totalDecided * 100.0; + } + if(g_perfData.strategyStats[stratIdx].totalLossPips > 0) + { + g_perfData.strategyStats[stratIdx].profitFactor = + g_perfData.strategyStats[stratIdx].totalProfitPips / + g_perfData.strategyStats[stratIdx].totalLossPips; + } + double winRate = g_perfData.strategyStats[stratIdx].winRate / 100.0; + double lossRate = 1.0 - winRate; + g_perfData.strategyStats[stratIdx].expectancy = + (winRate * g_perfData.strategyStats[stratIdx].avgWin) - + (lossRate * g_perfData.strategyStats[stratIdx].avgLoss); + // Update max drawdown + if(trade.maxDrawdown > g_perfData.strategyStats[stratIdx].maxDrawdown) + { + g_perfData.strategyStats[stratIdx].maxDrawdown = trade.maxDrawdown; + } + // Copy to global array + g_strategyPerf[stratIdx] = g_perfData.strategyStats[stratIdx]; +} +//+------------------------------------------------------------------+ +//| Export Trade History to CSV | +//+------------------------------------------------------------------+ +bool ExportTradeHistory() +{ + if(g_tradeHistoryCount == 0) return false; + string filepath = GetDataFilePath(TRADE_HISTORY_FILE); + int handle = FileOpen(filepath, FILE_WRITE|FILE_CSV|FILE_COMMON, ","); + if(handle == INVALID_HANDLE) + { + Print("[X] Failed to open CSV file for export: ", filepath); + return false; + } + // Write header + FileWrite(handle, + "ID", + "Entry Time", + "Exit Time", + "Entry Price", + "Exit Price", + "Stop Loss", + "Take Profit", + "Lot Size", + "Direction", + "Strategy", + "Quality", + "Result", + "PnL Pips", + "PnL Money", + "Risk Reward", + "Max Drawdown", + "Max Profit", + "Killzone", + "Market Phase"); + // Write trades + for(int i = 0; i < g_tradeHistoryCount; i++) + { + // Get killzone name from killzoneType (enum) + string kzName = "NONE"; + if(g_tradeHistory[i].killzoneType != KZ_NONE) + { + for(int k = 0; k < g_numKillzones; k++) + { + if(g_killzoneDefinitions[k].type == g_tradeHistory[i].killzoneType) + { + kzName = g_killzoneDefinitions[k].name; + break; + } + } + } + FileWrite(handle, + IntegerToString(g_tradeHistory[i].id), + TimeToString(g_tradeHistory[i].entryTime, TIME_DATE|TIME_MINUTES), + TimeToString(g_tradeHistory[i].exitTime, TIME_DATE|TIME_MINUTES), + DoubleToString(g_tradeHistory[i].entryPrice, g_digits), + DoubleToString(g_tradeHistory[i].exitPrice, g_digits), + DoubleToString(g_tradeHistory[i].stopLoss, g_digits), + DoubleToString(g_tradeHistory[i].takeProfit, g_digits), + DoubleToString(g_tradeHistory[i].lotSize, 2), + g_tradeHistory[i].direction, + g_tradeHistory[i].strategy, + DoubleToString(g_tradeHistory[i].quality, 1), + g_tradeHistory[i].result, + DoubleToString(g_tradeHistory[i].pnlPips, 1), + DoubleToString(g_tradeHistory[i].pnlMoney, 2), + DoubleToString(g_tradeHistory[i].riskReward, 2), + DoubleToString(g_tradeHistory[i].maxDrawdown, 2), + DoubleToString(g_tradeHistory[i].maxProfit, 2), + kzName, + g_tradeHistory[i].marketPhase); + } + FileClose(handle); + if(g_verboseLog) + { + Print("[OK] Trade history exported to: ", filepath); + Print(" Total trades: ", g_tradeHistoryCount); + } + return true; +} +//+------------------------------------------------------------------+ +//| Load Trade History from CSV | +//+------------------------------------------------------------------+ +bool LoadTradeHistory() +{ + string filepath = GetDataFilePath(TRADE_HISTORY_FILE); + if(!FileIsExist(filepath, FILE_COMMON)) return false; + int handle = FileOpen(filepath, FILE_READ|FILE_CSV|FILE_COMMON, ","); + if(handle == INVALID_HANDLE) return false; + // Skip header + while(!FileIsLineEnding(handle) && !FileIsEnding(handle)) + { + FileReadString(handle); + } + // Read trades + g_tradeHistoryCount = 0; + ArrayResize(g_tradeHistory, 0); + while(!FileIsEnding(handle)) + { + TradeRecord trade; + trade.id = (long)StringToInteger(FileReadString(handle)); + trade.entryTime = StringToTime(FileReadString(handle)); + trade.exitTime = StringToTime(FileReadString(handle)); + trade.entryPrice = StringToDouble(FileReadString(handle)); + trade.exitPrice = StringToDouble(FileReadString(handle)); + trade.stopLoss = StringToDouble(FileReadString(handle)); + trade.takeProfit = StringToDouble(FileReadString(handle)); + trade.lotSize = StringToDouble(FileReadString(handle)); + trade.direction = FileReadString(handle); + trade.strategy = FileReadString(handle); + trade.quality = StringToDouble(FileReadString(handle)); + trade.result = FileReadString(handle); + trade.pnlPips = StringToDouble(FileReadString(handle)); + trade.pnlMoney = StringToDouble(FileReadString(handle)); + trade.riskReward = StringToDouble(FileReadString(handle)); + trade.maxDrawdown = StringToDouble(FileReadString(handle)); + trade.maxProfit = StringToDouble(FileReadString(handle)); + string kzName = FileReadString(handle); + trade.killzoneType = GetKillzoneTypeFromName(kzName); + trade.marketPhase = FileReadString(handle); + if(trade.id > 0) + { + ArrayResize(g_tradeHistory, g_tradeHistoryCount + 1); + g_tradeHistory[g_tradeHistoryCount] = trade; + g_tradeHistoryCount++; + } + } + FileClose(handle); + if(g_verboseLog) + { + Print("[OK] Trade history loaded: ", g_tradeHistoryCount, " trades"); + } + return true; +} +//+------------------------------------------------------------------+ +//| Get Killzone Type from Name | +//+------------------------------------------------------------------+ +ENUM_KILLZONE_TYPE GetKillzoneTypeFromName(string name) +{ + if(name == "Asian") return KZ_ASIAN; + if(name == "London Open") return KZ_LONDON_OPEN; + if(name == "London Close") return KZ_LONDON_CLOSE; + if(name == "NY Open") return KZ_NY_OPEN; + if(name == "NY Lunch") return KZ_NY_LUNCH; + if(name == "NY Close") return KZ_NY_CLOSE; + if(name == "Silver Bullet LDN") return KZ_SILVER_BULLET_LDN; + if(name == "Silver Bullet NY AM") return KZ_SILVER_BULLET_NY_AM; + if(name == "Silver Bullet NY PM") return KZ_SILVER_BULLET_NY_PM; + return KZ_NONE; +} +//+------------------------------------------------------------------+ +//| Get Daily Stats Count | +//+------------------------------------------------------------------+ +int GetDailyStatsCount() +{ + int count = 0; + for(int i = 0; i < MAX_DAILY_STATS; i++) + { + if(g_perfData.dailyStats[i].date > 0) + count++; + else + break; + } + return count; +} +//+------------------------------------------------------------------+ +//| Get Performance Summary String | +//+------------------------------------------------------------------+ +string GetPerformanceSummary() +{ + string summary = ""; + summary += "===============================================\n"; + summary += "[CHART] PERFORMANCE SUMMARY\n"; + summary += "===============================================\n"; + summary += StringFormat("Total Trades: %d\n", g_perfData.totalTrades); + summary += StringFormat("Win Rate: %.1f%%\n", g_perfData.overallWinRate); + summary += StringFormat("Profit Factor: %.2f\n", g_perfData.profitFactor); + summary += StringFormat("Expectancy: %.1f pips\n", g_perfData.expectancy); + summary += StringFormat("Net Pips: %.1f\n", g_perfData.totalProfitPips - g_perfData.totalLossPips); + summary += StringFormat("Max Drawdown: %.2f%%\n", g_perfData.maxDrawdown); + summary += StringFormat("Sharpe Ratio: %.2f\n", g_perfData.sharpeRatio); + summary += StringFormat("Sortino Ratio: %.2f\n", g_perfData.sortinoRatio); + if(g_perfData.totalWins + g_perfData.totalLosses > 0) + { + summary += "\n[UP] QUALITY CORRELATION:\n"; + int totalHigh = g_perfData.highQualityWins + g_perfData.highQualityLosses; + int totalMed = g_perfData.mediumQualityWins + g_perfData.mediumQualityLosses; + int totalLow = g_perfData.lowQualityWins + g_perfData.lowQualityLosses; + if(totalHigh > 0) + summary += StringFormat(" High Quality: %.1f%% win rate\n", + (double)g_perfData.highQualityWins / totalHigh * 100); + if(totalMed > 0) + summary += StringFormat(" Medium Quality: %.1f%% win rate\n", + (double)g_perfData.mediumQualityWins / totalMed * 100); + if(totalLow > 0) + summary += StringFormat(" Low Quality: %.1f%% win rate\n", + (double)g_perfData.lowQualityWins / totalLow * 100); + } + summary += "===============================================\n"; + return summary; +} +//+------------------------------------------------------------------+ +//| Print Best and Worst Strategies | +//+------------------------------------------------------------------+ +void PrintStrategyRanking() +{ + Print("-----------------------------------------------------------"); + Print("[CHART] STRATEGY RANKING BY EXPECTANCY:"); + Print("-----------------------------------------------------------"); + // Simple bubble sort by expectancy + int indices[]; + ArrayResize(indices, MAX_STRATEGY_STATS); + for(int i = 0; i < MAX_STRATEGY_STATS; i++) indices[i] = i; + for(int i = 0; i < MAX_STRATEGY_STATS - 1; i++) + { + for(int j = 0; j < MAX_STRATEGY_STATS - i - 1; j++) + { + if(g_strategyPerf[indices[j]].expectancy < g_strategyPerf[indices[j+1]].expectancy) + { + int temp = indices[j]; + indices[j] = indices[j+1]; + indices[j+1] = temp; + } + } + } + int rank = 1; + for(int i = 0; i < MAX_STRATEGY_STATS; i++) + { + int idx = indices[i]; + if(g_strategyPerf[idx].totalTrades > 0) + { + PrintFormat(" %d. %s: %.1f pips exp, %.1f%% WR, %d trades", + rank, + g_strategyPerf[idx].name, + g_strategyPerf[idx].expectancy, + g_strategyPerf[idx].winRate, + g_strategyPerf[idx].totalTrades); + rank++; + } + } + Print("-----------------------------------------------------------"); +} +//+------------------------------------------------------------------+ +//| Export ML Prediction History | +//+------------------------------------------------------------------+ +bool ExportMLHistory() +{ + if(ArraySize(ML_Predictions) == 0) return false; + string filepath = GetDataFilePath(ML_HISTORY_FILE); + int handle = FileOpen(filepath, FILE_WRITE|FILE_CSV|FILE_COMMON, ","); + if(handle == INVALID_HANDLE) return false; + // Write header + FileWrite(handle, + "Timestamp", + "Category", + "Confidence", + "Bullish Prob", + "Bearish Prob", + "Neutral Prob", + "Target Price", + "Expected Move", + "Actual Direction", + "Correct"); + // Write predictions + for(int i = 0; i < ArraySize(ML_Predictions); i++) + { + // Direct access without GetPointer + FileWrite(handle, + TimeToString(ML_Predictions[i].timestamp, TIME_DATE|TIME_MINUTES), + ML_Predictions[i].category, + DoubleToString(ML_Predictions[i].confidence, 3), + DoubleToString(ML_Predictions[i].bullishProb, 3), + DoubleToString(ML_Predictions[i].bearishProb, 3), + DoubleToString(ML_Predictions[i].neutralProb, 3), + DoubleToString(ML_Predictions[i].targetPrice, g_digits), + DoubleToString(ML_Predictions[i].expectedMove, 1), + ML_Predictions[i].actualDirection, + ML_Predictions[i].wasCorrect ? "YES" : "NO"); + } + FileClose(handle); + return true; +} +//+------------------------------------------------------------------+ +//| Initialize Trade Journal | +//+------------------------------------------------------------------+ +void InitializeTradeJournal() +{ + if(!EnableTradeJournal) return; + string filepath = GetDataFilePath(JournalFilename); + // Check if file exists + if(!FileIsExist(filepath, FILE_COMMON)) + { + // Create new file with header + int handle = FileOpen(filepath, FILE_WRITE|FILE_CSV|FILE_COMMON, ","); + if(handle != INVALID_HANDLE) + { + FileWrite(handle, + "Timestamp", + "Type", + "Signal ID", + "Strategy", + "Direction", + "Entry Price", + "Stop Loss", + "Take Profit", + "Quality", + "Confluence", + "Killzone", + "Market Phase", + "ML Confidence", + "Structure Aligned", + "Notes"); + FileClose(handle); + } + } + g_journalInitialized = true; +} +//+------------------------------------------------------------------+ +//| Write to Trade Journal | +//+------------------------------------------------------------------+ +void WriteToJournal(string type, SIGNAL_Struct &signal, string notes = "") +{ + if(!EnableTradeJournal || !g_journalInitialized) return; + string filepath = GetDataFilePath(JournalFilename); + int handle = FileOpen(filepath, FILE_READ|FILE_WRITE|FILE_CSV|FILE_COMMON, ","); + if(handle == INVALID_HANDLE) return; + // Move to end of file + FileSeek(handle, 0, SEEK_END); + string kzName = "NONE"; + if(signal.killzone != KZ_NONE) + { + for(int k = 0; k < g_numKillzones; k++) + { + if(g_killzoneDefinitions[k].type == signal.killzone) + { + kzName = g_killzoneDefinitions[k].name; + break; + } + } + } + FileWrite(handle, + TimeToString(signal.time, TIME_DATE|TIME_MINUTES), + type, + signal.id, + signal.strategy, + signal.isBullish ? "BUY" : "SELL", + DoubleToString(signal.entryPrice, g_digits), + DoubleToString(signal.stopLoss, g_digits), + DoubleToString(signal.takeProfit, g_digits), + DoubleToString(signal.entryQuality, 1), + DoubleToString(signal.confluence, 2), + kzName, + signal.marketPhase, + DoubleToString(signal.mlConfidence, 3), + signal.structureAligned ? "YES" : "NO", + notes); + FileClose(handle); + g_lastJournalWrite = TimeCurrent(); +} +//+------------------------------------------------------------------+ +//| [NEW] BACKTESTING FRAMEWORK IMPLEMENTATION v5.0 | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| Initialize Backtesting System | +//+------------------------------------------------------------------+ +void InitializeBacktest() +{ + if(BacktestMode == BACKTEST_DISABLED) return; + // Initialize results structure + ZeroMemory(g_backtestResults); + g_backtestResults.startDate = 0; + g_backtestResults.endDate = 0; + g_backtestResults.totalBars = 0; + g_backtestResults.totalTrades = 0; + g_backtestResults.winningTrades = 0; + g_backtestResults.losingTrades = 0; + g_backtestResults.breakevenTrades = 0; + g_backtestResults.netProfit = 0; + g_backtestResults.grossProfit = 0; + g_backtestResults.grossLoss = 0; + g_backtestResults.profitFactor = 0; + g_backtestResults.returnPercent = 0; + g_backtestResults.maxDrawdown = 0; + g_backtestResults.maxDrawdownPercent = 0; + g_backtestResults.avgDrawdown = 0; + g_backtestResults.sharpeRatio = 0; + g_backtestResults.sortinoRatio = 0; + g_backtestResults.calmarRatio = 0; + g_backtestResults.winRate = 0; + g_backtestResults.avgWin = 0; + g_backtestResults.avgLoss = 0; + g_backtestResults.avgTrade = 0; + g_backtestResults.expectancy = 0; + g_backtestResults.payoffRatio = 0; + g_backtestResults.maxConsecutiveWins = 0; + g_backtestResults.maxConsecutiveLosses = 0; + g_backtestResults.avgTradesPerDay = 0; + // Initialize equity curve arrays + ArrayResize(g_backtestResults.equityCurve, 0); + ArrayResize(g_backtestResults.drawdownCurve, 0); + ArrayResize(g_backtestResults.equityDates, 0); + // Initialize strategy breakdown (static array - no resize needed) + for(int i = 0; i < MAX_STRATEGY_STATS; i++) + { + ZeroMemory(g_backtestResults.strategyBreakdown[i]); + g_backtestResults.strategyBreakdown[i].name = g_strategyNames[i]; + } + // Initialize trade array + ArrayResize(g_backtestTrades, 0); + g_backtestTradeCount = 0; + // Set initial equity + g_backtestEquity = g_effectiveBIB; + g_backtestPeakEquity = g_effectiveBIB; + g_backtestDrawdown = 0; + // Initialize Monte Carlo if needed + if(BacktestMode == BACKTEST_MONTE_CARLO) + { + ZeroMemory(g_monteCarloResults); + g_monteCarloResults.numSimulations = MonteCarloSimulations; + } + // Initialize Walk-Forward if needed + if(BacktestMode == BACKTEST_WALK_FORWARD) + { + g_wfCurrentWindow = 0; + g_wfTotalWindows = 0; + ArrayResize(g_wfOptimalParams, 10); // Store optimal params + } + g_isBacktesting = true; + g_backtestCurrentBar = 0; + Print("[CHART] Backtesting Framework Initialized"); + PrintFormat(" Mode: %s", EnumToString(BacktestMode)); + PrintFormat(" Initial Balance: $%.2f", g_effectiveBIB); + PrintFormat(" Optimization Target: %s", EnumToString(OptimizationTarget)); +} +//+------------------------------------------------------------------+ +//| Process Backtest Bar | +//+------------------------------------------------------------------+ +void ProcessBacktestBar(int barIndex, const datetime &time[], const double &open[], + const double &high[], const double &low[], const double &close[]) +{ + if(!g_isBacktesting || BacktestMode == BACKTEST_DISABLED) return; + g_backtestCurrentBar++; + // Update start/end dates + if(g_backtestResults.startDate == 0) + g_backtestResults.startDate = time[barIndex]; + g_backtestResults.endDate = time[barIndex]; + g_backtestResults.totalBars++; + // Check existing open positions for exit + CheckBacktestExits(barIndex, time, high, low, close); + // Record equity point periodically + if(g_backtestCurrentBar % 10 == 0) + { + RecordEquityPoint(time[barIndex]); + } +} +//+------------------------------------------------------------------+ +//| Add Backtest Trade | +//+------------------------------------------------------------------+ +void AddBacktestTrade(SIGNAL_Struct &signal, datetime entryTime, double entryPrice) +{ + if(!g_isBacktesting) return; + BacktestTrade trade; + trade.id = g_backtestTradeCount + 1; + trade.entryTime = entryTime; + trade.entryPrice = entryPrice; + trade.stopLoss = signal.stopLoss; + trade.takeProfit = signal.takeProfit; + trade.direction = signal.isBullish ? "BUY" : "SELL"; + trade.strategy = signal.strategy; + trade.quality = signal.entryQuality; + trade.exitTime = 0; + trade.exitPrice = 0; + trade.exitReason = ""; + trade.pnlPips = 0; + trade.pnlMoney = 0; + trade.runningEquity = g_backtestEquity; + trade.drawdown = g_backtestDrawdown; + // Calculate lot size based on risk + trade.lotSize = CalculateBacktestLotSize(entryPrice, signal.stopLoss); + // Add to array + int newSize = ArrayResize(g_backtestTrades, g_backtestTradeCount + 1); + if(newSize > 0) + { + g_backtestTrades[g_backtestTradeCount] = trade; + g_backtestTradeCount++; + } +} +//+------------------------------------------------------------------+ +//| Calculate Backtest Lot Size | +//+------------------------------------------------------------------+ +double CalculateBacktestLotSize(double entryPrice, double stopLoss) +{ + double riskAmount = g_backtestEquity * (AccountRiskPercent / 100.0); + double slPips = MathAbs(entryPrice - stopLoss) / g_pipValue; + if(slPips <= 0) slPips = 20; // Default 20 pips SL + double pipValuePerLot = g_tickValue * (g_pipValue / g_tickSize); + if(pipValuePerLot <= 0) pipValuePerLot = 10; + double lotSize = riskAmount / (slPips * pipValuePerLot); + // Apply lot constraints + lotSize = MathMax(g_minLot, MathMin(g_maxLot, lotSize)); + lotSize = NormalizeDouble(MathFloor(lotSize / g_lotStep) * g_lotStep, 2); + return lotSize; +} +//+------------------------------------------------------------------+ +//| Check Backtest Exits | +//+------------------------------------------------------------------+ +void CheckBacktestExits(int barIndex, const datetime &time[], + const double &high[], const double &low[], const double &close[]) +{ + for(int i = 0; i < g_backtestTradeCount; i++) + { + // Skip already closed trades + if(g_backtestTrades[i].exitTime > 0) continue; + bool isBuy = (g_backtestTrades[i].direction == "BUY"); + double sl = g_backtestTrades[i].stopLoss; + double tp = g_backtestTrades[i].takeProfit; + double entryPrice = g_backtestTrades[i].entryPrice; + bool hitSL = false; + bool hitTP = false; + double exitPrice = 0; + string exitReason = ""; + if(isBuy) + { + // Check SL hit + if(low[barIndex] <= sl) + { + hitSL = true; + exitPrice = sl; + exitReason = "STOP_LOSS"; + } + // Check TP hit + else if(high[barIndex] >= tp) + { + hitTP = true; + exitPrice = tp; + exitReason = "TAKE_PROFIT"; + } + } + else // SELL + { + // Check SL hit + if(high[barIndex] >= sl) + { + hitSL = true; + exitPrice = sl; + exitReason = "STOP_LOSS"; + } + // Check TP hit + else if(low[barIndex] <= tp) + { + hitTP = true; + exitPrice = tp; + exitReason = "TAKE_PROFIT"; + } + } + // Close trade if hit + if(hitSL || hitTP) + { + CloseBacktestTrade(i, time[barIndex], exitPrice, exitReason); + } + // Check for time-based exit (e.g., end of day) + // Could be implemented here + } +} +//+------------------------------------------------------------------+ +//| Close Backtest Trade | +//+------------------------------------------------------------------+ +void CloseBacktestTrade(int tradeIndex, datetime exitTime, double exitPrice, string exitReason) +{ + if(tradeIndex < 0 || tradeIndex >= g_backtestTradeCount) return; + // Direct access without GetPointer + g_backtestTrades[tradeIndex].exitTime = exitTime; + g_backtestTrades[tradeIndex].exitPrice = exitPrice; + g_backtestTrades[tradeIndex].exitReason = exitReason; + // Calculate P&L + bool isBuy = (g_backtestTrades[tradeIndex].direction == "BUY"); + double priceDiff = isBuy ? + (exitPrice - g_backtestTrades[tradeIndex].entryPrice) : + (g_backtestTrades[tradeIndex].entryPrice - exitPrice); + g_backtestTrades[tradeIndex].pnlPips = priceDiff / g_pipValue; + // Calculate money P&L + double pipValuePerLot = g_tickValue * (g_pipValue / g_tickSize); + g_backtestTrades[tradeIndex].pnlMoney = + g_backtestTrades[tradeIndex].pnlPips * pipValuePerLot * + g_backtestTrades[tradeIndex].lotSize; + // Update equity + g_backtestEquity += g_backtestTrades[tradeIndex].pnlMoney; + g_backtestTrades[tradeIndex].runningEquity = g_backtestEquity; + // Update peak and drawdown + if(g_backtestEquity > g_backtestPeakEquity) + { + g_backtestPeakEquity = g_backtestEquity; + } + double currentDD = (g_backtestPeakEquity > 0) ? (g_backtestPeakEquity - g_backtestEquity) / g_backtestPeakEquity * 100 : 0.0; + g_backtestTrades[tradeIndex].drawdown = currentDD; + if(currentDD > g_backtestDrawdown) + { + g_backtestDrawdown = currentDD; + } + // Update results + g_backtestResults.totalTrades++; + if(g_backtestTrades[tradeIndex].pnlMoney > 0) + { + g_backtestResults.winningTrades++; + g_backtestResults.grossProfit += g_backtestTrades[tradeIndex].pnlMoney; + } + else if(g_backtestTrades[tradeIndex].pnlMoney < 0) + { + g_backtestResults.losingTrades++; + g_backtestResults.grossLoss += MathAbs(g_backtestTrades[tradeIndex].pnlMoney); + } + else + { + g_backtestResults.breakevenTrades++; + } + g_backtestResults.netProfit = g_backtestResults.grossProfit - g_backtestResults.grossLoss; + // Update strategy breakdown + int stratIdx = GetStrategyIndex(g_backtestTrades[tradeIndex].strategy); + if(stratIdx >= 0 && stratIdx < MAX_STRATEGY_STATS) + { + g_backtestResults.strategyBreakdown[stratIdx].totalTrades++; + if(g_backtestTrades[tradeIndex].pnlMoney > 0) + { + g_backtestResults.strategyBreakdown[stratIdx].wins++; + g_backtestResults.strategyBreakdown[stratIdx].totalProfitPips += + g_backtestTrades[tradeIndex].pnlPips; + } + else if(g_backtestTrades[tradeIndex].pnlMoney < 0) + { + g_backtestResults.strategyBreakdown[stratIdx].losses++; + g_backtestResults.strategyBreakdown[stratIdx].totalLossPips += + MathAbs(g_backtestTrades[tradeIndex].pnlPips); + } + } +} +//+------------------------------------------------------------------+ +//| Record Equity Point | +//+------------------------------------------------------------------+ +void RecordEquityPoint(datetime time) +{ + int size = ArraySize(g_backtestResults.equityCurve); + ArrayResize(g_backtestResults.equityCurve, size + 1); + ArrayResize(g_backtestResults.drawdownCurve, size + 1); + ArrayResize(g_backtestResults.equityDates, size + 1); + g_backtestResults.equityCurve[size] = g_backtestEquity; + g_backtestResults.equityDates[size] = time; + // Calculate current drawdown + double dd = 0; + if(g_backtestPeakEquity > 0) + { + dd = (g_backtestPeakEquity - g_backtestEquity) / g_backtestPeakEquity * 100; + } + g_backtestResults.drawdownCurve[size] = dd; +} +//+------------------------------------------------------------------+ +//| Finalize Backtest | +//+------------------------------------------------------------------+ +void FinalizeBacktest() +{ + if(!g_isBacktesting) return; + Print("==========================================================="); + Print("[CHART] FINALIZING BACKTEST..."); + Print("==========================================================="); + // Calculate all metrics + CalculateBacktestMetrics(); + // Calculate strategy breakdown metrics + CalculateStrategyBreakdownMetrics(); + // Run Monte Carlo if needed + if(BacktestMode == BACKTEST_MONTE_CARLO) + { + RunMonteCarloSimulation(); + } + // Print results + PrintBacktestResults(); + // Draw equity curve if enabled + if(ShowEquityCurve) + { + DrawEquityCurve(); + } + // Export results if enabled + if(ExportBacktestResults_Enabled) + { + ExportBacktestResults(); + } + g_isBacktesting = false; + Print("==========================================================="); + Print("[OK] BACKTEST COMPLETE"); + Print("==========================================================="); +} +//+------------------------------------------------------------------+ +//| Calculate Backtest Metrics | +//+------------------------------------------------------------------+ +void CalculateBacktestMetrics() +{ + if(g_backtestResults.totalTrades == 0) return; + // =============================================================== + // BASIC METRICS + // =============================================================== + // Win Rate + int totalDecided = g_backtestResults.winningTrades + g_backtestResults.losingTrades; + if(totalDecided > 0) + { + g_backtestResults.winRate = (double)g_backtestResults.winningTrades / totalDecided * 100.0; + } + // Profit Factor + if(g_backtestResults.grossLoss > 0) + { + g_backtestResults.profitFactor = g_backtestResults.grossProfit / g_backtestResults.grossLoss; + } + // Return Percent + if(g_effectiveBIB > 0) + { + g_backtestResults.returnPercent = (g_backtestEquity - g_effectiveBIB) / + g_effectiveBIB * 100.0; + } + // Average Win/Loss + if(g_backtestResults.winningTrades > 0) + { + g_backtestResults.avgWin = g_backtestResults.grossProfit / g_backtestResults.winningTrades; + } + if(g_backtestResults.losingTrades > 0) + { + g_backtestResults.avgLoss = g_backtestResults.grossLoss / g_backtestResults.losingTrades; + } + // Average Trade + g_backtestResults.avgTrade = g_backtestResults.netProfit / g_backtestResults.totalTrades; + // Payoff Ratio + if(g_backtestResults.avgLoss > 0) + { + g_backtestResults.payoffRatio = g_backtestResults.avgWin / g_backtestResults.avgLoss; + } + // Expectancy + double winRate = g_backtestResults.winRate / 100.0; + double lossRate = 1.0 - winRate; + g_backtestResults.expectancy = (winRate * g_backtestResults.avgWin) - + (lossRate * g_backtestResults.avgLoss); + // =============================================================== + // DRAWDOWN METRICS + // =============================================================== + // Max Drawdown (already tracked) + g_backtestResults.maxDrawdownPercent = g_backtestDrawdown; + g_backtestResults.maxDrawdown = g_backtestPeakEquity * (g_backtestDrawdown / 100.0); + // Average Drawdown + int ddCount = ArraySize(g_backtestResults.drawdownCurve); + if(ddCount > 0) + { + double sumDD = 0; + for(int i = 0; i < ddCount; i++) + { + sumDD += g_backtestResults.drawdownCurve[i]; + } + g_backtestResults.avgDrawdown = sumDD / ddCount; + } + // =============================================================== + // CONSECUTIVE WINS/LOSSES + // =============================================================== + int currentWinStreak = 0; + int currentLossStreak = 0; + int maxWinStreak = 0; + int maxLossStreak = 0; + for(int i = 0; i < g_backtestTradeCount; i++) + { + if(g_backtestTrades[i].pnlMoney > 0) + { + currentWinStreak++; + currentLossStreak = 0; + if(currentWinStreak > maxWinStreak) maxWinStreak = currentWinStreak; + } + else if(g_backtestTrades[i].pnlMoney < 0) + { + currentLossStreak++; + currentWinStreak = 0; + if(currentLossStreak > maxLossStreak) maxLossStreak = currentLossStreak; + } + } + g_backtestResults.maxConsecutiveWins = maxWinStreak; + g_backtestResults.maxConsecutiveLosses = maxLossStreak; + // =============================================================== + // RISK-ADJUSTED RETURNS + // =============================================================== + CalculateBacktestRiskMetrics(); + // =============================================================== + // TRADING FREQUENCY + // =============================================================== + if(g_backtestResults.startDate > 0 && g_backtestResults.endDate > g_backtestResults.startDate) + { + int totalDays = (int)((g_backtestResults.endDate - g_backtestResults.startDate) / 86400); + if(totalDays > 0) + { + g_backtestResults.avgTradesPerDay = (double)g_backtestResults.totalTrades / totalDays; + } + } +} +//+------------------------------------------------------------------+ +//| Calculate Risk-Adjusted Return Metrics | +//+------------------------------------------------------------------+ +void CalculateBacktestRiskMetrics() +{ + if(g_backtestTradeCount < 10) return; + // Collect returns + double returns[]; + ArrayResize(returns, g_backtestTradeCount); + double sumReturns = 0; + double sumSquaredReturns = 0; + double sumNegativeSquaredReturns = 0; + int negativeCount = 0; + for(int i = 0; i < g_backtestTradeCount; i++) + { + // Calculate return as percentage of equity at trade + double tradeReturn = 0; + if(g_backtestTrades[i].runningEquity > 0) + { + tradeReturn = g_backtestTrades[i].pnlMoney / g_backtestTrades[i].runningEquity * 100; + } + returns[i] = tradeReturn; + sumReturns += tradeReturn; + sumSquaredReturns += tradeReturn * tradeReturn; + if(tradeReturn < 0) + { + sumNegativeSquaredReturns += tradeReturn * tradeReturn; + negativeCount++; + } + } + double avgReturn = sumReturns / g_backtestTradeCount; + double variance = (sumSquaredReturns / g_backtestTradeCount) - (avgReturn * avgReturn); + double stdDev = MathSqrt(MathMax(0, variance)); + // Sharpe Ratio (annualized, assuming 252 trading days) + if(stdDev > 0) + { + // Simple Sharpe without risk-free rate adjustment + g_backtestResults.sharpeRatio = (avgReturn / stdDev) * MathSqrt(252.0 / + MathMax(1, g_backtestResults.avgTradesPerDay)); + } + // Sortino Ratio + double downsideVariance = (negativeCount > 0) ? sumNegativeSquaredReturns / negativeCount : 0; + double downsideDeviation = MathSqrt(MathMax(0, downsideVariance)); + if(downsideDeviation > 0) + { + g_backtestResults.sortinoRatio = (avgReturn / downsideDeviation) * MathSqrt(252.0 / + MathMax(1, g_backtestResults.avgTradesPerDay)); + } + // Calmar Ratio (Annual Return / Max Drawdown) + if(g_backtestResults.maxDrawdownPercent > 0) + { + // Annualize the return + int totalDays = (int)((g_backtestResults.endDate - g_backtestResults.startDate) / 86400); + double annualizedReturn = (totalDays > 0) ? + (g_backtestResults.returnPercent * 365.0 / totalDays) : g_backtestResults.returnPercent; + g_backtestResults.calmarRatio = annualizedReturn / g_backtestResults.maxDrawdownPercent; + } + ArrayFree(returns); +} +//+------------------------------------------------------------------+ +//| Calculate Strategy Breakdown Metrics | +//+------------------------------------------------------------------+ +void CalculateStrategyBreakdownMetrics() +{ + for(int i = 0; i < MAX_STRATEGY_STATS; i++) + { + // Direct access without GetPointer + if(g_backtestResults.strategyBreakdown[i].totalTrades == 0) continue; + int totalDecided = g_backtestResults.strategyBreakdown[i].wins + + g_backtestResults.strategyBreakdown[i].losses; + // Win Rate + if(totalDecided > 0) + { + g_backtestResults.strategyBreakdown[i].winRate = + (double)g_backtestResults.strategyBreakdown[i].wins / totalDecided * 100.0; + } + // Average Win/Loss + if(g_backtestResults.strategyBreakdown[i].wins > 0) + { + g_backtestResults.strategyBreakdown[i].avgWin = + g_backtestResults.strategyBreakdown[i].totalProfitPips / + g_backtestResults.strategyBreakdown[i].wins; + } + if(g_backtestResults.strategyBreakdown[i].losses > 0) + { + g_backtestResults.strategyBreakdown[i].avgLoss = + g_backtestResults.strategyBreakdown[i].totalLossPips / + g_backtestResults.strategyBreakdown[i].losses; + } + // Profit Factor + if(g_backtestResults.strategyBreakdown[i].totalLossPips > 0) + { + g_backtestResults.strategyBreakdown[i].profitFactor = + g_backtestResults.strategyBreakdown[i].totalProfitPips / + g_backtestResults.strategyBreakdown[i].totalLossPips; + } + // Expectancy + double winRate = g_backtestResults.strategyBreakdown[i].winRate / 100.0; + double lossRate = 1.0 - winRate; + g_backtestResults.strategyBreakdown[i].expectancy = + (winRate * g_backtestResults.strategyBreakdown[i].avgWin) - + (lossRate * g_backtestResults.strategyBreakdown[i].avgLoss); + } +} +//+------------------------------------------------------------------+ +//| Run Monte Carlo Simulation | +//+------------------------------------------------------------------+ +void RunMonteCarloSimulation() +{ + if(g_backtestTradeCount < 30) + { + Print("[WARN] Not enough trades for Monte Carlo simulation (need at least 30)"); + return; + } + Print("[DICE] Running Monte Carlo Simulation with ", MonteCarloSimulations, " iterations..."); + // Collect trade returns + double tradeReturns[]; + ArrayResize(tradeReturns, g_backtestTradeCount); + for(int i = 0; i < g_backtestTradeCount; i++) + { + tradeReturns[i] = g_backtestTrades[i].pnlMoney; + } + // Storage for simulation results + double simReturns[]; + ArrayResize(simReturns, MonteCarloSimulations); + double simMaxDD[]; + ArrayResize(simMaxDD, MonteCarloSimulations); + // Run simulations + for(int sim = 0; sim < MonteCarloSimulations; sim++) + { + double equity = g_effectiveBIB; + double peakEquity = g_effectiveBIB; + double maxDD = 0; + // Randomly resample trades with replacement + for(int t = 0; t < g_backtestTradeCount; t++) + { + int randomIdx = MathRand() % g_backtestTradeCount; + equity += tradeReturns[randomIdx]; + if(equity > peakEquity) + { + peakEquity = equity; + } + double dd = (peakEquity - equity) / peakEquity * 100; + if(dd > maxDD) + { + maxDD = dd; + } + } + simReturns[sim] = (equity - g_effectiveBIB) / g_effectiveBIB * 100; + simMaxDD[sim] = maxDD; + } + // Sort results + ArraySort(simReturns); + ArraySort(simMaxDD); + // Calculate statistics + double sumReturns = 0; + double sumSquared = 0; + int profitCount = 0; + int ruinCount = 0; // Ruin defined as >50% drawdown + for(int i = 0; i < MonteCarloSimulations; i++) + { + sumReturns += simReturns[i]; + sumSquared += simReturns[i] * simReturns[i]; + if(simReturns[i] > 0) profitCount++; + if(simMaxDD[i] > 50) ruinCount++; + } + // Store results + g_monteCarloResults.numSimulations = MonteCarloSimulations; + g_monteCarloResults.meanReturn = sumReturns / MonteCarloSimulations; + g_monteCarloResults.medianReturn = simReturns[MonteCarloSimulations / 2]; + g_monteCarloResults.stdReturn = MathSqrt((sumSquared / MonteCarloSimulations) - + (g_monteCarloResults.meanReturn * g_monteCarloResults.meanReturn)); + // Percentiles + g_monteCarloResults.percentile5 = simReturns[(int)(MonteCarloSimulations * 0.05)]; + g_monteCarloResults.percentile25 = simReturns[(int)(MonteCarloSimulations * 0.25)]; + g_monteCarloResults.percentile75 = simReturns[(int)(MonteCarloSimulations * 0.75)]; + g_monteCarloResults.percentile95 = simReturns[(int)(MonteCarloSimulations * 0.95)]; + g_monteCarloResults.worstCase = simReturns[0]; + g_monteCarloResults.bestCase = simReturns[MonteCarloSimulations - 1]; + g_monteCarloResults.probabilityOfProfit = (double)profitCount / MonteCarloSimulations * 100; + g_monteCarloResults.probabilityOfRuin = (double)ruinCount / MonteCarloSimulations * 100; + g_monteCarloResults.confidenceLevel = 95.0; + // Print Monte Carlo results + Print("-----------------------------------------------------------"); + Print("[DICE] MONTE CARLO RESULTS:"); + Print("-----------------------------------------------------------"); + PrintFormat(" Mean Return: %.2f%%", g_monteCarloResults.meanReturn); + PrintFormat(" Median Return: %.2f%%", g_monteCarloResults.medianReturn); + PrintFormat(" Std Deviation: %.2f%%", g_monteCarloResults.stdReturn); + PrintFormat(" 5th Percentile: %.2f%%", g_monteCarloResults.percentile5); + PrintFormat(" 95th Percentile: %.2f%%", g_monteCarloResults.percentile95); + PrintFormat(" Worst Case: %.2f%%", g_monteCarloResults.worstCase); + PrintFormat(" Best Case: %.2f%%", g_monteCarloResults.bestCase); + PrintFormat(" Probability of Profit: %.1f%%", g_monteCarloResults.probabilityOfProfit); + PrintFormat(" Probability of Ruin: %.1f%%", g_monteCarloResults.probabilityOfRuin); + Print("-----------------------------------------------------------"); + ArrayFree(tradeReturns); + ArrayFree(simReturns); + ArrayFree(simMaxDD); +} +//+------------------------------------------------------------------+ +//| Walk-Forward Optimization | +//+------------------------------------------------------------------+ +void RunWalkForwardOptimization() +{ + if(BacktestMode != BACKTEST_WALK_FORWARD) return; + Print("[WALK] Running Walk-Forward Optimization..."); + int totalBars = g_totalRates; + int windowSize = WalkForwardWindow; + int stepSize = WalkForwardStep; + g_wfTotalWindows = (totalBars - windowSize) / stepSize + 1; + Print(" Total Windows: ", g_wfTotalWindows); + Print(" Window Size: ", windowSize, " bars"); + Print(" Step Size: ", stepSize, " bars"); + // Storage for out-of-sample results + double oosReturns[]; + ArrayResize(oosReturns, g_wfTotalWindows); + for(int w = 0; w < g_wfTotalWindows; w++) + { + g_wfCurrentWindow = w; + int isStart = w * stepSize; + int isEnd = isStart + windowSize; + int oosStart = isEnd; + int oosEnd = MathMin(oosStart + stepSize, totalBars); + // In-sample optimization would happen here + // For now, we just use current parameters + // Out-of-sample testing + double oosProfit = 0; + // Count trades in OOS period + for(int t = 0; t < g_backtestTradeCount; t++) + { + // Check if trade is in OOS period + // This is simplified - real implementation would use bar indices + oosProfit += g_backtestTrades[t].pnlMoney; + } + oosReturns[w] = oosProfit; + if(g_verboseLog) + { + PrintFormat(" Window %d: IS[%d-%d] OOS[%d-%d] Return: $%.2f", + w, isStart, isEnd, oosStart, oosEnd, oosProfit); + } + } + // Calculate Walk-Forward Efficiency + double totalIS = g_backtestResults.netProfit; // Simplified + double totalOOS = 0; + for(int w = 0; w < g_wfTotalWindows; w++) + { + totalOOS += oosReturns[w]; + } + double wfEfficiency = (totalIS != 0) ? (totalOOS / totalIS) * 100 : 0; + Print("-----------------------------------------------------------"); + Print("[WALK] WALK-FORWARD RESULTS:"); + Print("-----------------------------------------------------------"); + PrintFormat(" In-Sample Return: $%.2f", totalIS); + PrintFormat(" Out-of-Sample Return: $%.2f", totalOOS); + PrintFormat(" Walk-Forward Efficiency: %.1f%%", wfEfficiency); + Print("-----------------------------------------------------------"); + ArrayFree(oosReturns); +} +//+------------------------------------------------------------------+ +//| Draw Equity Curve on Chart | +//+------------------------------------------------------------------+ +void DrawEquityCurve() +{ + int points = ArraySize(g_backtestResults.equityCurve); + if(points < 2) return; + // Delete existing equity curve objects + DeleteEquityCurveObjects(); + // Create equity curve line segments + for(int i = 1; i < points; i++) + { + string lineName = "ICT_BT_Equity_" + IntegerToString(i); + // Create trend line between points + if(ObjectCreate(0, lineName, OBJ_TREND, 0, + g_backtestResults.equityDates[i-1], g_backtestResults.equityCurve[i-1], + g_backtestResults.equityDates[i], g_backtestResults.equityCurve[i])) + { + // Color based on direction + color lineColor = (g_backtestResults.equityCurve[i] >= g_backtestResults.equityCurve[i-1]) ? + clrLime : clrRed; + ObjectSetInteger(0, lineName, OBJPROP_COLOR, lineColor); + ObjectSetInteger(0, lineName, OBJPROP_WIDTH, 2); + ObjectSetInteger(0, lineName, OBJPROP_RAY_RIGHT, false); + ObjectSetInteger(0, lineName, OBJPROP_BACK, false); + } + } + // Add labels for peak and current + string peakLabel = "ICT_BT_Peak"; + if(ObjectCreate(0, peakLabel, OBJ_TEXT, 0, + g_backtestResults.equityDates[points-1], g_backtestPeakEquity)) + { + ObjectSetString(0, peakLabel, OBJPROP_TEXT, + StringFormat("Peak: $%.2f", g_backtestPeakEquity)); + ObjectSetInteger(0, peakLabel, OBJPROP_COLOR, clrGold); + ObjectSetInteger(0, peakLabel, OBJPROP_FONTSIZE, 10); + } + ChartRedraw(0); +} +//+------------------------------------------------------------------+ +//| Delete Equity Curve Objects | +//+------------------------------------------------------------------+ +void DeleteEquityCurveObjects() +{ + int totalObjects = ObjectsTotal(0, 0, -1); + for(int i = totalObjects - 1; i >= 0; i--) + { + string objName = ObjectName(0, i, 0, -1); + if(StringFind(objName, "ICT_BT_") >= 0) + { + ObjectDelete(0, objName); + } + } +} +//+------------------------------------------------------------------+ +//| Print Backtest Results | +//+------------------------------------------------------------------+ +void PrintBacktestResults() +{ + Print("==========================================================="); + Print("[CHART] BACKTEST RESULTS"); + Print("==========================================================="); + Print("[DATE] PERIOD:"); + PrintFormat(" Start: %s", TimeToString(g_backtestResults.startDate, TIME_DATE)); + PrintFormat(" End: %s", TimeToString(g_backtestResults.endDate, TIME_DATE)); + PrintFormat(" Bars: %d", g_backtestResults.totalBars); + Print("\n[UP] PERFORMANCE:"); + PrintFormat(" Initial Balance: $%.2f", g_effectiveBIB); + PrintFormat(" Final Balance: $%.2f", g_backtestEquity); + PrintFormat(" Net Profit: $%.2f (%.2f%%)", g_backtestResults.netProfit, g_backtestResults.returnPercent); + PrintFormat(" Gross Profit: $%.2f", g_backtestResults.grossProfit); + PrintFormat(" Gross Loss: $%.2f", g_backtestResults.grossLoss); + Print("\n[CHART] STATISTICS:"); + PrintFormat(" Total Trades: %d", g_backtestResults.totalTrades); + // * v9.59 FIX#229: Show grouped Win Rate from multiTP stats alongside MT5 trade count WR + PrintFormat(" Winning: %d (%.1f%%)", g_backtestResults.winningTrades, g_backtestResults.winRate); + if(InpEnableMultiTP && g_multiTPStats.totalEntries > 0) + { + CalculateMultiTPStats(); + PrintFormat(" Win Rate (grouped entries): %.1f%% [FIX#229 — tranche-corrected]", g_multiTPStats.winRate); + PrintFormat(" Entry Groups: %d TP1 hits: %d TP2 hits: %d TP3 hits: %d", + g_multiTPStats.totalEntries, g_multiTPStats.tp1HitCount, + g_multiTPStats.tp2HitCount, g_multiTPStats.tp3HitCount); + } + PrintFormat(" Losing: %d", g_backtestResults.losingTrades); + PrintFormat(" Breakeven: %d", g_backtestResults.breakevenTrades); + PrintFormat(" Avg Win: $%.2f", g_backtestResults.avgWin); + PrintFormat(" Avg Loss: $%.2f", g_backtestResults.avgLoss); + PrintFormat(" Avg Trade: $%.2f", g_backtestResults.avgTrade); + PrintFormat(" Payoff Ratio: %.2f", g_backtestResults.payoffRatio); + PrintFormat(" Profit Factor: %.2f", g_backtestResults.profitFactor); + PrintFormat(" Expectancy: $%.2f", g_backtestResults.expectancy); + Print("\n[DOWN] RISK METRICS:"); + PrintFormat(" Max Drawdown: $%.2f (%.2f%%)", g_backtestResults.maxDrawdown, g_backtestResults.maxDrawdownPercent); + // * v9.59 FIX#230: Also print equity-based Total DD from DD protection system + // This catches floating-loss drawdowns not reflected in per-trade closed P&L tracking + PrintFormat(" Max Drawdown (equity-based): %.2f%% [FIX#230 — peak=%.2f current=%.2f]", + g_currentTotalDD, g_peakBalance, MathMin(AccountInfoDouble(ACCOUNT_BALANCE), AccountInfoDouble(ACCOUNT_EQUITY))); + PrintFormat(" Avg Drawdown: %.2f%%", g_backtestResults.avgDrawdown); + PrintFormat(" Max Consecutive Wins: %d", g_backtestResults.maxConsecutiveWins); + PrintFormat(" Max Consecutive Losses: %d", g_backtestResults.maxConsecutiveLosses); + Print("\n[CHART] RISK-ADJUSTED RETURNS:"); + PrintFormat(" Sharpe Ratio: %.2f", g_backtestResults.sharpeRatio); + PrintFormat(" Sortino Ratio: %.2f", g_backtestResults.sortinoRatio); + PrintFormat(" Calmar Ratio: %.2f", g_backtestResults.calmarRatio); + Print("\n[LIST] STRATEGY BREAKDOWN:"); + for(int i = 0; i < MAX_STRATEGY_STATS; i++) + { + if(g_backtestResults.strategyBreakdown[i].totalTrades > 0) + { + PrintFormat(" %s: %d trades, %.1f%% WR, %.1f exp", + g_backtestResults.strategyBreakdown[i].name, + g_backtestResults.strategyBreakdown[i].totalTrades, + g_backtestResults.strategyBreakdown[i].winRate, + g_backtestResults.strategyBreakdown[i].expectancy); + } + } + Print("==========================================================="); +} +//+------------------------------------------------------------------+ +//| Export Backtest Results to CSV | +//+------------------------------------------------------------------+ +void ExportBacktestResults() +{ + string filepath = GetDataFilePath(BACKTEST_RESULTS_FILE); + int handle = FileOpen(filepath, FILE_WRITE|FILE_CSV|FILE_COMMON, ","); + if(handle == INVALID_HANDLE) + { + Print("[X] Failed to open file for backtest export: ", filepath); + return; + } + // Write summary + FileWrite(handle, "EA ANGEL " + EA_VERSION + " - BACKTEST RESULTS | " + EA_LAST_DATE); + FileWrite(handle, "Generated", TimeToString(TimeCurrent(), TIME_DATE|TIME_MINUTES)); + FileWrite(handle, ""); + FileWrite(handle, "PERIOD"); + FileWrite(handle, "Start Date", TimeToString(g_backtestResults.startDate, TIME_DATE)); + FileWrite(handle, "End Date", TimeToString(g_backtestResults.endDate, TIME_DATE)); + FileWrite(handle, "Total Bars", g_backtestResults.totalBars); + FileWrite(handle, ""); + FileWrite(handle, "PERFORMANCE"); + FileWrite(handle, "Initial Balance", g_effectiveBIB); + FileWrite(handle, "Final Balance", g_backtestEquity); + FileWrite(handle, "Net Profit", g_backtestResults.netProfit); + FileWrite(handle, "Return %", g_backtestResults.returnPercent); + FileWrite(handle, "Profit Factor", g_backtestResults.profitFactor); + FileWrite(handle, ""); + FileWrite(handle, "STATISTICS"); + FileWrite(handle, "Total Trades", g_backtestResults.totalTrades); + FileWrite(handle, "Win Rate %", g_backtestResults.winRate); + FileWrite(handle, "Expectancy", g_backtestResults.expectancy); + FileWrite(handle, "Sharpe Ratio", g_backtestResults.sharpeRatio); + FileWrite(handle, "Max Drawdown %", g_backtestResults.maxDrawdownPercent); + FileWrite(handle, ""); + // Write trade list + FileWrite(handle, "TRADE LIST"); + FileWrite(handle, "ID", "Entry Time", "Exit Time", "Direction", "Strategy", + "Entry", "Exit", "P&L", "Equity", "Drawdown"); + for(int i = 0; i < g_backtestTradeCount; i++) + { + FileWrite(handle, + g_backtestTrades[i].id, + TimeToString(g_backtestTrades[i].entryTime, TIME_DATE|TIME_MINUTES), + TimeToString(g_backtestTrades[i].exitTime, TIME_DATE|TIME_MINUTES), + g_backtestTrades[i].direction, + g_backtestTrades[i].strategy, + DoubleToString(g_backtestTrades[i].entryPrice, g_digits), + DoubleToString(g_backtestTrades[i].exitPrice, g_digits), + DoubleToString(g_backtestTrades[i].pnlMoney, 2), + DoubleToString(g_backtestTrades[i].runningEquity, 2), + DoubleToString(g_backtestTrades[i].drawdown, 2)); + } + FileClose(handle); + Print("[OK] Backtest results exported to: ", filepath); +} +//+------------------------------------------------------------------+ +//| Get Optimization Score | +//+------------------------------------------------------------------+ +double GetOptimizationScore() +{ + switch(OptimizationTarget) + { + case OPT_NET_PROFIT: + return g_backtestResults.netProfit; + case OPT_PROFIT_FACTOR: + return g_backtestResults.profitFactor; + case OPT_SHARPE_RATIO: + return g_backtestResults.sharpeRatio; + case OPT_SORTINO_RATIO: + return g_backtestResults.sortinoRatio; + case OPT_MAX_DRAWDOWN: + return -g_backtestResults.maxDrawdownPercent; // Negative because we want to minimize + case OPT_WIN_RATE: + return g_backtestResults.winRate; + case OPT_EXPECTANCY: + return g_backtestResults.expectancy; + default: + return g_backtestResults.netProfit; + } +} +//+------------------------------------------------------------------+ +//| HELPER FUNCTIONS | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| Check if Price is in FVG - OPTIMIZED | +//+------------------------------------------------------------------+ +bool IsPriceInFVG(double price, int &fvgIndex, bool bullishOnly = false) +{ + fvgIndex = -1; + int size = ArraySize(FVG_Array); + if(size == 0) return false; + // OPTIMIZATION: Check from END (recent FVGs more relevant), limit to 50 + int checkLimit = MathMin(size, 50); + for(int i = size - 1; i >= size - checkLimit && i >= 0; i--) + { + if(FVG_Array[i].status != FVG_STATUS_ACTIVE) continue; + if(bullishOnly && !FVG_Array[i].isBullish) continue; + if(price >= FVG_Array[i].bottom && price <= FVG_Array[i].top) + { + fvgIndex = i; + return true; + } + } + return false; +} +//+------------------------------------------------------------------+ +//| Check if Price is Near Order Block - OPTIMIZED | +//+------------------------------------------------------------------+ +bool IsPriceNearOB(double price, int &obIndex, bool bullishOnly = false) +{ + obIndex = -1; + int size = ArraySize(OB_Array); + if(size == 0) return false; + double tolerance = g_cachedATR * 0.3; + // OPTIMIZATION: Check from END, limit to 30 + int checkLimit = MathMin(size, 30); + for(int i = size - 1; i >= size - checkLimit && i >= 0; i--) + { + if(OB_Array[i].mitigated) continue; + if(bullishOnly && !OB_Array[i].isBullish) continue; + if(price >= OB_Array[i].bottom - tolerance && + price <= OB_Array[i].top + tolerance) + { + obIndex = i; + return true; + } + } + return false; +} +//+------------------------------------------------------------------+ +//| Check if Price is in OTE Zone - OPTIMIZED | +//+------------------------------------------------------------------+ +bool IsPriceInOTE(double price, int &oteIndex) +{ + oteIndex = -1; + int size = ArraySize(OTE_Array); + if(size == 0) return false; + // OPTIMIZATION: Check from END, limit to 20 + int checkLimit = MathMin(size, 20); + for(int i = size - 1; i >= size - checkLimit && i >= 0; i--) + { + if(!OTE_Array[i].isValid) continue; + double level618 = OTE_Array[i].level618; + double level786 = OTE_Array[i].level786; + // Handle both directions (bullish/bearish OTE) + double top = MathMax(level618, level786); + double bottom = MathMin(level618, level786); + if(price >= bottom && price <= top) + { + oteIndex = i; + return true; + } + } + return false; +} +//+------------------------------------------------------------------+ +//| Get Strategy Index from Name | +//+------------------------------------------------------------------+ +int GetStrategyIndex(string strategyName) +{ + for(int i = 0; i < g_numStrategies; i++) + { + if(g_strategyNames[i] == strategyName) + return i; + } + return -1; +} +//+------------------------------------------------------------------+ +//| Check if in Trading Session | +//+------------------------------------------------------------------+ +bool IsInTradingSession() +{ + if(!g_workingUseSessionFilter) return true; + MqlDateTime dt; + TimeToStruct(TimeGMT(), dt); // * FIX#413b: TimeGMT() — session hours (London 7-16, NY 12-21) are UTC + int hour = dt.hour; + if(g_workingSessionLondon && hour >= 7 && hour < 16) + return true; + // New York Session: 12:00 - 21:00 + if(g_workingSessionNewYork && hour >= 12 && hour < 21) + return true; + // Asian Session: 23:00 - 08:00 + if(g_workingSessionAsian && (hour >= 23 || hour < 8)) + return true; + return false; +} +//+------------------------------------------------------------------+ +//| Get Higher Timeframe | +//+------------------------------------------------------------------+ +ENUM_TIMEFRAMES GetHigherTimeframe(ENUM_TIMEFRAMES currentTF) +{ + switch(currentTF) + { + case PERIOD_M1: return PERIOD_M5; + case PERIOD_M5: return PERIOD_M15; + case PERIOD_M15: return PERIOD_H1; + case PERIOD_M30: return PERIOD_H1; + case PERIOD_H1: return PERIOD_H4; + case PERIOD_H4: return PERIOD_D1; + case PERIOD_D1: return PERIOD_W1; + case PERIOD_W1: return PERIOD_MN1; + default: return PERIOD_H1; + } +} +//+------------------------------------------------------------------+ +//| Get Timeframe Bias | +//+------------------------------------------------------------------+ +bool GetTimeframeBias(ENUM_TIMEFRAMES tf) +{ + double close1 = iClose(_Symbol, tf, 1); + double close2 = iClose(_Symbol, tf, 2); + double ma = 0; + // Simple check: compare recent closes + for(int i = 1; i <= 10; i++) + { + ma += iClose(_Symbol, tf, i); + } + ma /= 10; + return (close1 > ma); // Bullish if price above MA +} +//+------------------------------------------------------------------+ +//| Calculate Position Size | +//+------------------------------------------------------------------+ +double CalculatePositionSize(double entryPrice, double stopLoss) +{ + // [UNIFIED] Now uses EA_* inputs instead of old indicator inputs + // Check if using fixed lots (EA_FixedLotSize > 0) + if(EA_FixedLotSize > 0) { + return EA_FixedLotSize; + } + // Dynamic calculation using EA_RiskPercent + double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE); + double riskPct = EA_RiskPercent; + // [v6.42] Apply correlation reduction + if(g_corrReductionFactor < 1.0 && g_corrReductionFactor > 0.1) + riskPct *= g_corrReductionFactor; + // [v6.42] Kelly criterion position sizing + // * FIX#370: Use R-multiples (g_historicalAvgWin/Loss) NOT pip-based perfData. + // * FIX#441: D1+ bypasses Kelly entirely. + // ROOT: Kelly uses ALL trade history (mostly H4). H4 WR=70% → Kelly=~4.69% on D1. + // D1 has <10 own trades — Kelly is statistically invalid and inflates risk massively. + // Jan 19 D1: 1.45 lots (4.69% risk). Feb 16 SL hit: $948 (expected $375 at 1.50%). + // Fix: _Period>=PERIOD_D1 skips Kelly block, uses flat cfg.risk[4]=1.50% instead. + if(PosSize_UseKelly && _Period < PERIOD_D1 && g_perfData.totalTrades >= PosSize_StreakLookback && g_perfData.overallWinRate > 0) + { + double wr = g_historicalWinRate; // R-based win rate + double avgW = (g_historicalAvgWin > 0) ? g_historicalAvgWin : EV_DefaultAvgWin; // R-multiples + double avgL = (g_historicalAvgLoss > 0) ? g_historicalAvgLoss : EV_DefaultAvgLoss; // R-multiples + double bR = (avgL > 0) ? avgW / avgL : 1; + double kelly = MathMax(0.0, wr - (1.0 - wr) / bR); + kelly *= PosSize_KellyFraction; // Fractional Kelly + if(EV_UseKellyCriterion) kelly = MathMax(kelly, 0.01); // [v6.42] EV Kelly override + kelly = MathMin(kelly, 0.25); // Cap at 25% + if(kelly > 0.01) riskPct = kelly * 100.0; // Use Kelly % + } + double riskAmount = accountBalance * (riskPct / 100.0); + // [UNIFIED] FIX: Calculate SL distance (was using raw SL price = wrong lots!) + double slDistance = MathAbs(entryPrice - stopLoss); + if(slDistance <= 0) return SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); + double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); + double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); + if(tickValue <= 0 || tickSize <= 0) return SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); + double slInTicks = slDistance / tickSize; + double lots = riskAmount / (slInTicks * tickValue); + // Normalize lots + double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); + double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); + double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); + lots = MathMax(minLot, MathMin(maxLot, lots)); + lots = MathFloor(lots / lotStep) * lotStep; + return lots; +} +//+------------------------------------------------------------------+ +//| Validate Price | +//+------------------------------------------------------------------+ +bool ValidatePrice(double price) +{ + return (price > 0 && price < DBL_MAX && MathIsValidNumber(price)); +} +//+------------------------------------------------------------------+ +//| 1. DETECT MARKET STRUCTURE - ΠΛΗΡΗΣ ΥΛΟΠΟΙΗΣΗ | +//+------------------------------------------------------------------+ +void DetectMarketStructure(const datetime &time[], const double &high[], + const double &low[], const double &close[], int limit) +{ + if(limit < g_workingSTRUCT_SwingStrength * 2 + 1) return; + int maxBars = MathMin(limit, MaxBarsToCalculate); + // Detect swing highs and lows + for(int i = g_workingSTRUCT_SwingStrength; i < maxBars - g_workingSTRUCT_SwingStrength; i++) + { + bool isSwingHigh = true; + bool isSwingLow = true; + // Check if it's a swing high + for(int j = 1; j <= g_workingSTRUCT_SwingStrength; j++) + { + if(high[i] <= high[i - j] || high[i] <= high[i + j]) + { + isSwingHigh = false; + break; + } + } + // Check if it's a swing low + for(int j = 1; j <= g_workingSTRUCT_SwingStrength; j++) + { + if(low[i] >= low[i - j] || low[i] >= low[i + j]) + { + isSwingLow = false; + break; + } + } + // Add swing point to array + if(isSwingHigh || isSwingLow) + { + AddSwingPoint(time[i], isSwingHigh ? high[i] : low[i], isSwingHigh, i); + } + } + // Analyze structure (BOS/CHoCH) + AnalyzeStructureBreaks(time, high, low, close); + // Update global structure bias + UpdateStructureBias(); +} +//+------------------------------------------------------------------+ +//| Add Swing Point to Array | +//+------------------------------------------------------------------+ +void AddSwingPoint(datetime time, double price, bool isHigh, int barIndex) +{ + // Check for duplicate + for(int i = 0; i < ArraySize(STRUCT_Array); i++) + { + if(STRUCT_Array[i].time == time) return; + } + // Add new swing point + int size = ArraySize(STRUCT_Array); + if(size >= MAX_STRUCT_ARRAY) + { + // Remove oldest + for(int i = 0; i < size - 1; i++) + { + STRUCT_Array[i] = STRUCT_Array[i + 1]; + } + size--; + } + ArrayResize(STRUCT_Array, size + 1); + STRUCT_Array[size].time = time; + STRUCT_Array[size].price = price; + STRUCT_Array[size].isHigh = isHigh; + STRUCT_Array[size].broken = false; + STRUCT_Array[size].type = isHigh ? "SWING_HIGH" : "SWING_LOW"; + STRUCT_Array[size].age = 0; + STRUCT_Array[size].swingStrength = CalculateSwingStrength(barIndex, isHigh); + STRUCT_Array[size].isValid = true; +} +//+------------------------------------------------------------------+ +//| Calculate Swing Strength | +//+------------------------------------------------------------------+ +double CalculateSwingStrength(int barIndex, bool isHigh) +{ + double strength = 1.0; + // Strength based on how many bars it dominates + int dominance = 0; + for(int i = 1; i <= 10 && barIndex + i < g_totalRates; i++) + { + double currentHigh = iHigh(_Symbol, _Period, barIndex); + double currentLow = iLow(_Symbol, _Period, barIndex); + double checkHigh = iHigh(_Symbol, _Period, barIndex + i); + double checkLow = iLow(_Symbol, _Period, barIndex + i); + if(isHigh && currentHigh > checkHigh) dominance++; + if(!isHigh && currentLow < checkLow) dominance++; + } + strength = dominance / 10.0; + // Bonus for volume + long vol = iVolume(_Symbol, _Period, barIndex); + long avgVol = 0; + for(int i = 1; i <= 20 && barIndex + i < g_totalRates; i++) + { + avgVol += iVolume(_Symbol, _Period, barIndex + i); + } + avgVol /= 20; + if(avgVol > 0 && vol > avgVol * 1.5) + { + strength += 0.3; + } + return MathMin(1.0, strength); +} +//+------------------------------------------------------------------+ +//| Analyze Structure Breaks (BOS/CHoCH) | +//+------------------------------------------------------------------+ +void AnalyzeStructureBreaks(const datetime &time[], const double &high[], + const double &low[], const double &close[]) +{ + int size = ArraySize(STRUCT_Array); + if(size < 4) return; + double currentPrice = close[0]; + // Find last significant swing highs and lows + double lastSwingHigh = 0, lastSwingLow = DBL_MAX; + double prevSwingHigh = 0, prevSwingLow = DBL_MAX; + datetime lastHighTime = 0, lastLowTime = 0; + int highCount = 0, lowCount = 0; + for(int i = size - 1; i >= 0 && (highCount < 2 || lowCount < 2); i--) + { + if(STRUCT_Array[i].isHigh && !STRUCT_Array[i].broken) + { + if(highCount == 0) + { + lastSwingHigh = STRUCT_Array[i].price; + lastHighTime = STRUCT_Array[i].time; + } + else if(highCount == 1) + { + prevSwingHigh = STRUCT_Array[i].price; + } + highCount++; + } + else if(!STRUCT_Array[i].isHigh && !STRUCT_Array[i].broken) + { + if(lowCount == 0) + { + lastSwingLow = STRUCT_Array[i].price; + lastLowTime = STRUCT_Array[i].time; + } + else if(lowCount == 1) + { + prevSwingLow = STRUCT_Array[i].price; + } + lowCount++; + } + } + // Check for Break of Structure (BOS) + if(g_isBullishStructure) + { + // In bullish trend, look for BOS above swing high + if(currentPrice > lastSwingHigh && lastSwingHigh > 0) + { + // Bullish BOS - continuation + MarkSwingAsBroken(lastHighTime); + // * FIX#457b: ShowBOS && STRUCT_ShowBOS was redundant double-gate. + // User had to enable BOTH inputs to see BOS lines. STRUCT_ShowBOS alone is sufficient. + if(STRUCT_ShowBOS) + { + DrawBOSLine(lastHighTime, lastSwingHigh, true, "BOS"); + } + } + // Look for CHoCH (trend change) + if(currentPrice < lastSwingLow && lastSwingLow < DBL_MAX) + { + // Bearish CHoCH - potential trend change + g_isBullishStructure = false; + g_lastCHoCHTime = TimeCurrent(); // * v7.4 FIX: Cooldown prevents UpdateStructureBias override + MarkSwingAsBroken(lastLowTime); + if(STRUCT_ShowCHoCH) + { + DrawBOSLine(lastLowTime, lastSwingLow, false, "CHoCH"); + } + if(BOS_Alert) + { + Alert(_Symbol, " ", EnumToString(_Period), ": CHoCH - Bearish Structure Shift!"); + } + } + } + else // Bearish structure + { + // In bearish trend, look for BOS below swing low + if(currentPrice < lastSwingLow && lastSwingLow < DBL_MAX) + { + // Bearish BOS - continuation + MarkSwingAsBroken(lastLowTime); + if(STRUCT_ShowBOS) + { + DrawBOSLine(lastLowTime, lastSwingLow, false, "BOS"); + } + } + // Look for CHoCH + if(currentPrice > lastSwingHigh && lastSwingHigh > 0) + { + // Bullish CHoCH + g_isBullishStructure = true; + g_lastCHoCHTime = TimeCurrent(); // * v7.4 FIX: Cooldown prevents UpdateStructureBias override + MarkSwingAsBroken(lastHighTime); + if(STRUCT_ShowCHoCH) + { + DrawBOSLine(lastHighTime, lastSwingHigh, true, "CHoCH"); + } + if(BOS_Alert) + { + Alert(_Symbol, " ", EnumToString(_Period), ": CHoCH - Bullish Structure Shift!"); + } + } + } +} +//+------------------------------------------------------------------+ +//| Mark Swing as Broken | +//+------------------------------------------------------------------+ +void MarkSwingAsBroken(datetime swingTime) +{ + for(int i = 0; i < ArraySize(STRUCT_Array); i++) + { + if(STRUCT_Array[i].time == swingTime) + { + STRUCT_Array[i].broken = true; + break; + } + } +} +//+------------------------------------------------------------------+ +//| Draw BOS/CHoCH Line | +//+------------------------------------------------------------------+ +void DrawBOSLine(datetime time, double price, bool isBullish, string type) +{ + string objName = "ICT_" + type + "_" + IntegerToString((long)time); + color lineColor = (type == "BOS") ? STRUCT_BOSColor : STRUCT_CHoCHColor; + if(ObjectFind(0, objName) < 0) + { + ObjectCreate(0, objName, OBJ_HLINE, 0, time, price); + } + ObjectSetDouble(0, objName, OBJPROP_PRICE, price); + ObjectSetInteger(0, objName, OBJPROP_COLOR, lineColor); + ObjectSetInteger(0, objName, OBJPROP_STYLE, STYLE_DASH); + ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1); + ObjectSetInteger(0, objName, OBJPROP_BACK, true); + // Add label + string labelName = objName + "_Label"; + if(ObjectFind(0, labelName) < 0) + { + ObjectCreate(0, labelName, OBJ_TEXT, 0, time, price); + } + ObjectSetString(0, labelName, OBJPROP_TEXT, type); + ObjectSetInteger(0, labelName, OBJPROP_COLOR, lineColor); + ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 8); +} +//+------------------------------------------------------------------+ +//| Update Structure Bias | +//+------------------------------------------------------------------+ +// +------------------------------------------------------------------+ +// | FIX#196 (v9.48): UpdateD1CHoChBias | +// | Calculates D1 CHoCH direction independently from current TF. | +// | Called once per new D1 bar (cheap — no indicator handles). | +// | Logic: swing high/low detection on D1 closes. CHoCH confirmed | +// | when price closes beyond the last swing extreme. | +// | Result stored in g_d1CHoCH_Bull / _Bear / _Valid globals. | +// +------------------------------------------------------------------+ +void UpdateD1CHoChBias() +{ + // Only recalculate on new D1 bar + datetime d1BarTime = iTime(_Symbol, PERIOD_D1, 0); + if(d1BarTime == g_d1LastBarTime) return; + g_d1LastBarTime = d1BarTime; + + int lookback = 5; // FIX#505e: 2→5. lookback=2 found 2-day local highs as structural pivots. + // ROOT: Feb 21 2024 — SwingHigh=1.08056 was a 2-day local high. Close=1.08083 (+3 pips) + // triggered D1 CHoCH BULL while real D1 structure was still BEARISH (LH from Jan 1.09985). + // FIX: lookback=5 requires 5 bars on each side → true D1 structural swing only. + // Lag: ~5 days to detect new swing (was 2 days). Acceptable for D1 macro filter. + int totalBars = lookback * 2 + 5 + 10; // need extra for detection window + + // --- Collect D1 OHLC --- + double d1High[], d1Low[], d1Close[]; + ArraySetAsSeries(d1High, true); + ArraySetAsSeries(d1Low, true); + ArraySetAsSeries(d1Close, true); + if(CopyHigh (_Symbol, PERIOD_D1, 1, totalBars, d1High) < totalBars) return; + if(CopyLow (_Symbol, PERIOD_D1, 1, totalBars, d1Low) < totalBars) return; + if(CopyClose(_Symbol, PERIOD_D1, 1, totalBars, d1Close) < totalBars) return; + + // --- Detect last D1 swing high and swing low --- + // Swing High: bar[i] high > all bars in [i-lookback .. i+lookback] (excluding itself) + double lastSwingHigh = 0, lastSwingLow = DBL_MAX; + int lastSwingHighIdx = -1, lastSwingLowIdx = -1; + + int scanEnd = totalBars - lookback - 1; + for(int i = lookback; i < scanEnd; i++) + { + // Check swing high + bool isSwingHigh = true; + for(int j = i - lookback; j <= i + lookback; j++) + { + if(j == i) continue; + if(d1High[j] >= d1High[i]) { isSwingHigh = false; break; } + } + if(isSwingHigh && lastSwingHighIdx < 0) + { + lastSwingHigh = d1High[i]; + lastSwingHighIdx = i; + } + + // Check swing low + bool isSwingLow = true; + for(int j = i - lookback; j <= i + lookback; j++) + { + if(j == i) continue; + if(d1Low[j] <= d1Low[i]) { isSwingLow = false; break; } + } + if(isSwingLow && lastSwingLowIdx < 0) + { + lastSwingLow = d1Low[i]; + lastSwingLowIdx = i; + } + + // Stop once we have both + if(lastSwingHighIdx >= 0 && lastSwingLowIdx >= 0) break; + } + + if(lastSwingHighIdx < 0 || lastSwingLowIdx < 0) + { + // * v10.01 FIX#234: ROOT CAUSE BUG in FIX#231. + // FIX#231 (backtest bar 0): when D1 swing unavailable, it used MTF direction as proxy bias. + // BUG: MTF was BEARISH at start of test → g_d1CHoCH_Bear=true → ALL BUY trades blocked for + // entire test until a real D1 swing formed. This is wrong — lack of D1 data means UNCERTAINTY, + // not a confirmed directional bias. The D1 CHoCH gate should not impose direction when + // insufficient history exists; the MTF filter already handles direction separately. + // FIX#234: when D1 swing unavailable, always set NEUTRAL (Valid=false, Bull=false, Bear=false). + // This means the D1 CHoCH gate becomes inactive (treats as "no gate") — both BUY and SELL allowed. + // When real D1 structure forms (enough bars), normal CHoCH detection takes over. + g_d1CHoCH_Bull = false; + g_d1CHoCH_Bear = false; + g_d1CHoCH_Valid = false; + static bool s_fix234_warned = false; + if(!s_fix234_warned) + { + PrintFormat("* FIX#234 D1 CHoCH: swing data unavailable (high=%d low=%d bars) → NEUTRAL bias | both directions OPEN | D1 gate inactive until structure forms", + lastSwingHighIdx, lastSwingLowIdx); + s_fix234_warned = true; + } + return; + } + + // --- Most recent D1 close (bar 0 = completed bar 1 in 1-indexed array) --- + double recentClose = d1Close[0]; // bar 1 (last closed D1 bar) + + // --- CHoCH detection --- + // Bearish CHoCH: close broke below last D1 swing low + // Bullish CHoCH: close broke above last D1 swing high + bool newBull = (recentClose > lastSwingHigh); + bool newBear = (recentClose < lastSwingLow); + + bool changed = false; + if(newBear && !g_d1CHoCH_Bear) + { + g_d1CHoCH_Bear = true; + g_d1CHoCH_Bull = false; + g_d1CHoCH_Valid = true; + changed = true; + PrintFormat("* FIX#196 D1 CHoCH → BEARISH | Close=%.5f < SwingLow=%.5f | BUY trades BLOCKED", + recentClose, lastSwingLow); + } + else if(newBull && !g_d1CHoCH_Bull) + { + g_d1CHoCH_Bull = true; + g_d1CHoCH_Bear = false; + g_d1CHoCH_Valid = true; + changed = true; + PrintFormat("* FIX#196 D1 CHoCH → BULLISH | Close=%.5f > SwingHigh=%.5f | SELL trades BLOCKED", + recentClose, lastSwingHigh); + } + // * v10.36 FIX#333: REMOVED forced INIT bias (was FIX#329). + // ROOT CAUSE of -31% backtest: swings exist but price is inside range (no breakout yet). + // FIX#329 forced direction via MTF (60% BEARISH) → g_d1CHoCH_Bear=true → ALL BUY blocked + // for weeks while EURUSD rallied 1.088→1.113. No real CHoCH breakout = NEUTRAL. + // FIX#234 already handles "no swing data" → NEUTRAL. Same logic must apply here: + // price inside swing range = UNCERTAIN, not a confirmed bias. Valid stays false until + // a real close beyond swing high or low confirms direction. + else if(!g_d1CHoCH_Valid) + { + // Price is inside the swing range — no CHoCH confirmed yet. + // Keep Valid=false → D1 gate stays inactive → both BUY and SELL allowed. + // A real CHoCH (newBull or newBear above) will set Valid=true when it fires. + static bool s_fix333_logged = false; + if(!s_fix333_logged) + { + PrintFormat("* FIX#333 D1 CHoCH: price inside range (Close=%.5f between SwingH=%.5f SwingL=%.5f) → NEUTRAL | gate inactive", + recentClose, lastSwingHigh, lastSwingLow); + s_fix333_logged = true; + } + } + + if(g_verboseLog && !changed) + PrintFormat("* FIX#196 D1 CHoCH: %s (no change) | Close=%.5f | SwingH=%.5f SwingL=%.5f", + g_d1CHoCH_Bull ? "BULL" : "BEAR", recentClose, lastSwingHigh, lastSwingLow); +} + +// ================================================================ +// * FIX#443: DRAW ON LIQUIDITY (DOL) COMPUTATION +// ================================================================ +// ICT principle: price is DRAWN to unswept liquidity pools (BSL/SSL). +// The nearest unswept target determines where price naturally wants to go. +// This is NOT a prediction — it is reading WHERE price delivery will occur. +// +// DOL_UP: nearest unswept BSL (buy stops above) is closer than nearest SSL. +// Price is drawn UP to take those stops. BUY trades = aligned. SELL = fighting magnet. +// DOL_DOWN: nearest unswept SSL (sell stops below) is closer. +// Price is drawn DOWN. SELL = aligned. BUY = fighting magnet. +// NEUTRAL: BSL and SSL equidistant (within 20%) or no valid levels → gate inactive. +// +// Used in EvaluateSmartEntry Gate 0 (highest priority): +// trade opposing DOL → score penalty applied, hard block if ratio > 2.5× +// trade aligned with DOL → score bonus applied +// ================================================================ +void ComputeDrawOnLiquidity() +{ + // Reset state + g_dolValid = false; + g_dolDirection = 0; + g_dolTargetPrice = 0.0; + g_dolDistance = 0.0; + g_dolOppositeDistance = 0.0; + + if(ArraySize(LIQ_Array) == 0 || g_cachedATR <= 0 || g_pipValue <= 0) return; + + double mid = (SymbolInfoDouble(_Symbol, SYMBOL_ASK) + + SymbolInfoDouble(_Symbol, SYMBOL_BID)) * 0.5; + + // Scan unswept liquidity levels: BSL (buy-side, above price) and SSL (sell-side, below) + // Only levels with strength >= 0.3 — weak levels are noise, not institutional targets + double nearestBSL = 0, nearestSSL = 0; + // TF-aware strength threshold — higher TF needs stronger institutional levels + // M5=0.30 (many small levels OK), M15=0.35, H1=0.40, H4=0.50, D1=0.60 + double _dolStrengthMin; + switch(_Period) + { + case PERIOD_M1: + case PERIOD_M5: _dolStrengthMin = 0.30; break; + case PERIOD_M15: + case PERIOD_M30: _dolStrengthMin = 0.35; break; + case PERIOD_H1: _dolStrengthMin = 0.40; break; + case PERIOD_H4: _dolStrengthMin = 0.50; break; + default: _dolStrengthMin = 0.60; break; // D1+ + } + // TF-aware neutral ratio — higher TF = wider neutral band + // M5=1.15, M15=1.20, H1=1.25, H4=1.30, D1=1.40 + double _dolNeutralRatio; + switch(_Period) + { + case PERIOD_M1: + case PERIOD_M5: _dolNeutralRatio = 1.15; break; + case PERIOD_M15: + case PERIOD_M30: _dolNeutralRatio = 1.20; break; + case PERIOD_H1: _dolNeutralRatio = 1.25; break; + case PERIOD_H4: _dolNeutralRatio = 1.30; break; + default: _dolNeutralRatio = 1.40; break; // D1+ + } + + double distBSL = DBL_MAX, distSSL = DBL_MAX; + + int n = ArraySize(LIQ_Array); + for(int i = 0; i < n; i++) + { + if(!LIQ_Array[i].isValid || LIQ_Array[i].swept) continue; + if(LIQ_Array[i].strength < _dolStrengthMin) continue; + + double dist = MathAbs(LIQ_Array[i].price - mid); + + if(LIQ_Array[i].isBSL && LIQ_Array[i].price > mid) + { + if(dist < distBSL) { distBSL = dist; nearestBSL = LIQ_Array[i].price; } + } + else if(!LIQ_Array[i].isBSL && LIQ_Array[i].price < mid) + { + if(dist < distSSL) { distSSL = dist; nearestSSL = LIQ_Array[i].price; } + } + } + + // Need at least one valid target + if(nearestBSL == 0 && nearestSSL == 0) return; + + g_dolValid = true; + + double distBSLp = (nearestBSL > 0) ? distBSL / g_pipValue : 99999.0; + double distSSLp = (nearestSSL > 0) ? distSSL / g_pipValue : 99999.0; + + // Neutral: both targets equidistant within TF-aware band — no actionable bias + if(nearestBSL > 0 && nearestSSL > 0) + { + double ratio = (distBSLp <= distSSLp) ? distSSLp / distBSLp : distBSLp / distSSLp; + if(ratio < _dolNeutralRatio) + { + g_dolDirection = 0; + g_dolTargetPrice = 0; + if(g_verboseLog) + PrintFormat("[DOL] NEUTRAL: BSL=%.1fp SSL=%.1fp ratio=%.2f", + distBSLp, distSSLp, ratio); + return; + } + } + + // Direction = nearest unswept target + if(distBSLp <= distSSLp && nearestBSL > 0) + { + g_dolDirection = 1; // UP — price drawn toward BSL above + g_dolTargetPrice = nearestBSL; + g_dolDistance = distBSLp; + g_dolOppositeDistance = distSSLp; + } + else if(distSSLp < distBSLp && nearestSSL > 0) + { + g_dolDirection = -1; // DOWN — price drawn toward SSL below + g_dolTargetPrice = nearestSSL; + g_dolDistance = distSSLp; + g_dolOppositeDistance = distBSLp; + } + else + { + // No valid winner (both sides have zero price) + g_dolValid = false; + g_dolDirection = 0; + g_dolTargetPrice = 0.0; + return; + } + + if(g_verboseLog) + { + bool oneSide = (g_dolOppositeDistance >= 99998); + if(oneSide) + PrintFormat("[DOL] %s | Target=%.5f (%.1fp) | ONE-SIDE", + g_dolDirection > 0 ? "UP" : "DOWN", + g_dolTargetPrice, g_dolDistance); + else + PrintFormat("[DOL] %s | Target=%.5f (%.1fp) | Opposite=%.1fp | Ratio=%.2f", + g_dolDirection > 0 ? "UP" : "DOWN", + g_dolTargetPrice, g_dolDistance, + g_dolOppositeDistance, + g_dolOppositeDistance / g_dolDistance); + } +} + + +void UpdateStructureBias() +{ + // * v7.4 FIX: Don't override CHoCH for a cooldown period! + // CHoCH is the authoritative real-time signal for trend change. + // UpdateStructureBias counts HH/HL vs LH/LL which LAGS heavily in sustained trends. + // In gold uptrend: HH/HL always > LH/LL -> immediately overrides bearish CHoCH -> no shorts! + // Cooldown = 20 bars x period seconds -> let CHoCH direction persist + if(g_lastCHoCHTime > 0) + { + datetime cooldownEnd = g_lastCHoCHTime + PeriodSeconds(_Period) * 20; + if(TimeCurrent() < cooldownEnd) + return; // CHoCH still active, don't override + } + int size = ArraySize(STRUCT_Array); + if(size < 4) return; + // Count higher highs/lows vs lower highs/lows + int hhCount = 0, hlCount = 0, lhCount = 0, llCount = 0; + double prevHigh = 0, prevLow = DBL_MAX; + for(int i = 0; i < size; i++) + { + if(STRUCT_Array[i].isHigh) + { + if(prevHigh > 0) + { + if(STRUCT_Array[i].price > prevHigh) hhCount++; + else lhCount++; + } + prevHigh = STRUCT_Array[i].price; + } + else + { + if(prevLow < DBL_MAX) + { + if(STRUCT_Array[i].price > prevLow) hlCount++; + else llCount++; + } + prevLow = STRUCT_Array[i].price; + } + } + // Determine bias + if(hhCount + hlCount > lhCount + llCount) + { + g_isBullishStructure = true; + } + else if(lhCount + llCount > hhCount + hlCount) + { + g_isBullishStructure = false; + } +} +//+------------------------------------------------------------------+ +//| 2. UPDATE PREMIUM/DISCOUNT ZONES - ΠΛΗΡΗΣ ΥΛΟΠΟΙΗΣΗ | +//+------------------------------------------------------------------+ +void UpdatePremiumDiscountZones(const double &high[], const double &low[], const double &close[]) +{ + int _pdLookback = (g_workingPD_LookbackBars > 0 ? g_workingPD_LookbackBars : PD_LookbackBars); + if(g_totalRates < _pdLookback) return; + // Find range high and low + double rangeHigh = high[0]; + double rangeLow = low[0]; + for(int i = 0; i < _pdLookback && i < g_totalRates; i++) + { + if(high[i] > rangeHigh) rangeHigh = high[i]; + if(low[i] < rangeLow) rangeLow = low[i]; + } + double range = rangeHigh - rangeLow; + if(range <= 0) return; + double equilibrium = rangeLow + range * 0.5; + double premiumStart = rangeLow + range * 0.618; // Above 61.8% + double discountEnd = rangeLow + range * 0.382; // Below 38.2% + double currentPrice = close[0]; + // Determine zone + if(currentPrice >= premiumStart) + { + g_currentPDZone = "PREMIUM"; + } + else if(currentPrice <= discountEnd) + { + g_currentPDZone = "DISCOUNT"; + } + else + { + g_currentPDZone = "EQUILIBRIUM"; + } + // Draw zones if enabled + if(ShowPremiumDiscount) + { + DrawPDZones(rangeHigh, rangeLow, equilibrium, premiumStart, discountEnd); + } +} +//+------------------------------------------------------------------+ +//| Draw Premium/Discount Zones | +//+------------------------------------------------------------------+ +void DrawPDZones(double rangeHigh, double rangeLow, double eq, double premStart, double discEnd) +{ + datetime startTime = iTime(_Symbol, _Period, (g_workingPD_LookbackBars > 0 ? g_workingPD_LookbackBars : PD_LookbackBars)); + datetime endTime = iTime(_Symbol, _Period, 0) + PeriodSeconds() * 20; + // Premium Zone + string premName = "ICT_PD_Premium"; + if(ObjectFind(0, premName) < 0) + { + ObjectCreate(0, premName, OBJ_RECTANGLE, 0, startTime, rangeHigh, endTime, premStart); + } + ObjectSetInteger(0, premName, OBJPROP_TIME, 0, startTime); + ObjectSetDouble(0, premName, OBJPROP_PRICE, 0, rangeHigh); + ObjectSetInteger(0, premName, OBJPROP_TIME, 1, endTime); + ObjectSetDouble(0, premName, OBJPROP_PRICE, 1, premStart); + ObjectSetInteger(0, premName, OBJPROP_COLOR, PD_PremiumColor); + ObjectSetInteger(0, premName, OBJPROP_FILL, true); + ObjectSetInteger(0, premName, OBJPROP_BACK, true); + // Discount Zone + string discName = "ICT_PD_Discount"; + if(ObjectFind(0, discName) < 0) + { + ObjectCreate(0, discName, OBJ_RECTANGLE, 0, startTime, discEnd, endTime, rangeLow); + } + ObjectSetInteger(0, discName, OBJPROP_TIME, 0, startTime); + ObjectSetDouble(0, discName, OBJPROP_PRICE, 0, discEnd); + ObjectSetInteger(0, discName, OBJPROP_TIME, 1, endTime); + ObjectSetDouble(0, discName, OBJPROP_PRICE, 1, rangeLow); + ObjectSetInteger(0, discName, OBJPROP_COLOR, PD_DiscountColor); + ObjectSetInteger(0, discName, OBJPROP_FILL, true); + ObjectSetInteger(0, discName, OBJPROP_BACK, true); + // Equilibrium Line + string eqName = "ICT_PD_Equilibrium"; + if(ObjectFind(0, eqName) < 0) + { + ObjectCreate(0, eqName, OBJ_HLINE, 0, 0, eq); + } + ObjectSetDouble(0, eqName, OBJPROP_PRICE, eq); + ObjectSetInteger(0, eqName, OBJPROP_COLOR, clrGray); + ObjectSetInteger(0, eqName, OBJPROP_STYLE, STYLE_DOT); + ObjectSetInteger(0, eqName, OBJPROP_BACK, true); +} +//+------------------------------------------------------------------+ +//| Detect FVG - OPTIMIZED | +//+------------------------------------------------------------------+ +void DetectFVG(const datetime &time[], const double &open[], const double &high[], + const double &low[], const double &close[], int limit) +{ + if(limit < 3) return; + // * FIX#203 / FIX#322 → FIXED: H4 FVG gate τώρα ελέγχεται από το EnableFVG input. + // Αν EnableFVG=false → disabled (όπως πριν). + // Αν EnableFVG=true → ενεργό και σε H4 (ο χρήστης αποφασίζει συνειδητά). + // Αφαιρέθηκε το hard architectural block που αγνοούσε το input. + if(!EnableFVG) return; + int maxBars = MathMin(limit, MaxBarsToCalculate); + // OPTIMIZATION 1: Cache minSize calculation (done once, not per iteration) + // * FIX#455a: FVG_UsePips path uses g_pipValue which is correct for Forex (pip=10×point). + // BUG: For XAUUSD (_Digits=2, _Point=0.01), g_pipValue=0.01 → 3.0×0.01=0.03 = 3 cents. + // XAUUSD H4 ATR≈$3-5 → typical FVG = $0.50-$2.00. 3-cent floor = trivially loose. + // FIX: when FVG_UsePips=true and pair is not Forex (pip/point ratio = 1), use ATR-based minimum. + // ATR-based: g_cachedATR * 0.10 = 10% of ATR as minimum FVG size (ICT: meaningful imbalance). + // Forex (ratio=10): keep FVG_MinSizePips * g_pipValue (unchanged — works correctly). + double _pipPointRatio = (g_point > 0) ? g_pipValue / g_point : 1.0; + double minSize; + if(FVG_UsePips && _pipPointRatio >= 9.0) + { + // Standard Forex: pip = 10 points (EURUSD, GBPUSD etc) + minSize = FVG_MinSizePips * g_pipValue; + } + else if(FVG_UsePips && _pipPointRatio < 2.0 && g_cachedATR > 0) + { + // Non-Forex (XAUUSD, indices etc): pip ≈ point → use ATR-relative floor instead + // 10% of ATR = minimum structurally significant FVG for this instrument + minSize = g_cachedATR * 0.10; + } + else + { + // Points-based or fallback + minSize = g_workingFVG_MinSize * g_point; + } + // OPTIMIZATION 2: Pre-calculate average body for displacement check + double avgBodyCache = 0; + if(FVG_RequireDisplacement && maxBars > 11) + { + for(int j = 1; j <= 10; j++) + { + avgBodyCache += MathAbs(close[j] - open[j]); + } + avgBodyCache /= 10; + } + // OPTIMIZATION 3: Use sliding window for avgBody instead of nested loop + double bodySum = avgBodyCache * 10; // Running sum + for(int i = 1; i < maxBars - 1; i++) + { + // Update sliding window for displacement (if needed) + if(FVG_RequireDisplacement && i > 1 && i + 10 < maxBars) + { + // Remove oldest, add newest + bodySum = bodySum - MathAbs(close[i] - open[i]) + MathAbs(close[i + 10] - open[i + 10]); + avgBodyCache = bodySum / 10; + } + // Check for Bullish FVG + double bullishGapTop = low[i - 1]; + double bullishGapBottom = high[i + 1]; + if(bullishGapBottom < bullishGapTop) + { + double gapSize = bullishGapTop - bullishGapBottom; + if(gapSize >= minSize) + { + bool hasDisplacement = true; + if(FVG_RequireDisplacement) + { + double bodySize = MathAbs(close[i] - open[i]); + hasDisplacement = (bodySize > avgBodyCache * 1.5); + } + if(hasDisplacement) + { + AddFVG(time[i], bullishGapTop, bullishGapBottom, true, false, i); + } + } + } + // Check for Bearish FVG + double bearishGapTop = low[i + 1]; + double bearishGapBottom = high[i - 1]; + if(bearishGapBottom < bearishGapTop) + { + double gapSize = bearishGapTop - bearishGapBottom; + if(gapSize >= minSize) + { + bool hasDisplacement = true; + if(FVG_RequireDisplacement) + { + double bodySize = MathAbs(close[i] - open[i]); + hasDisplacement = (bodySize > avgBodyCache * 1.5); + } + if(hasDisplacement) + { + AddFVG(time[i], bearishGapTop, bearishGapBottom, false, false, i); + } + } + } + } +} +//+------------------------------------------------------------------+ +//| OPTIMIZED AddFVG - FIXED for MQL5 struct limitations | +//+------------------------------------------------------------------+ +void AddFVG(datetime time, double top, double bottom, bool isBullish, bool isInverse, int barIndex) +{ + int size = ArraySize(FVG_Array); + // OPTIMIZATION 1: Check duplicates from END (recent FVGs more likely to duplicate) + // Limit check to last 50 entries instead of entire array + int checkLimit = MathMin(size, 50); + for(int i = size - 1; i >= size - checkLimit && i >= 0; i--) + { + if(FVG_Array[i].time == time && + MathAbs(FVG_Array[i].top - top) < g_point && + MathAbs(FVG_Array[i].bottom - bottom) < g_point) + { + return; // Duplicate found + } + } + // OPTIMIZATION 2: Handle max capacity - manual shift (ArrayCopy doesn't work with string structs) + if(size >= FVG_MaxCount) + { + // Delete oldest FVG's objects first + DeleteFVGObjects(0); + // Manual shift - still O(n) but unavoidable with string members + for(int i = 0; i < size - 1; i++) + { + FVG_Array[i] = FVG_Array[i + 1]; + } + size--; + ArrayResize(FVG_Array, size, 50); // Shrink with reserve + } + // OPTIMIZATION 3: Reserve allocation - reserves 50 extra slots + ArrayResize(FVG_Array, size + 1, 50); + // Initialize new FVG + FVG_Array[size].id = ++g_fvgIdCounter; + FVG_Array[size].time = time; + FVG_Array[size].barIndex = barIndex; + FVG_Array[size].top = top; + FVG_Array[size].bottom = bottom; + FVG_Array[size].high = top; + FVG_Array[size].low = bottom; + FVG_Array[size].ce = (top + bottom) / 2; + FVG_Array[size].premium = top - (top - bottom) * 0.25; + FVG_Array[size].discount = bottom + (top - bottom) * 0.25; + FVG_Array[size].isBullish = isBullish; + FVG_Array[size].isInverse = isInverse; + FVG_Array[size].status = FVG_STATUS_ACTIVE; + FVG_Array[size].active = true; // Added for EA compatibility + FVG_Array[size].type = isBullish ? (isInverse ? FVG_TYPE_INVERSE_BULL : FVG_TYPE_BULLISH) : + (isInverse ? FVG_TYPE_INVERSE_BEAR : FVG_TYPE_BEARISH); + FVG_Array[size].sizePoints = (top - bottom) / g_point; + FVG_Array[size].sizePips = (top - bottom) / g_pipValue; + FVG_Array[size].fillPercentage = 0; + FVG_Array[size].filled = false; + FVG_Array[size].age = 0; + FVG_Array[size].touchCount = 0; + FVG_Array[size].lastUpdate = TimeCurrent(); + FVG_Array[size].fillTime = 0; + FVG_Array[size].fillPrice = 0; + // Calculate quality and strength + FVG_Array[size].quality = CalculateFVGQuality(size); + FVG_Array[size].strength = CalculateFVGStrength(size, barIndex); + // Set object names + FVG_Array[size].objNameRect = "ICT_FVG_" + IntegerToString(FVG_Array[size].id); + FVG_Array[size].objNameLabel = "ICT_FVG_Label_" + IntegerToString(FVG_Array[size].id); + FVG_Array[size].objNameCE = "ICT_FVG_CE_" + IntegerToString(FVG_Array[size].id); + // Alert if enabled + if(FVG_AlertNew) + { + string dir = isBullish ? "Bullish" : "Bearish"; + Alert(_Symbol, " ", EnumToString(_Period), ": New ", dir, " FVG detected! Size: ", + DoubleToString(FVG_Array[size].sizePips, 1), " pips"); + } + // Update count for EA compatibility + g_fvgCount = ArraySize(FVG_Array); +} +//+------------------------------------------------------------------+ +//| Calculate FVG Quality | +//+------------------------------------------------------------------+ +ENUM_FVG_QUALITY CalculateFVGQuality(int index) +{ + if(index < 0 || index >= ArraySize(FVG_Array)) return FVG_QUALITY_LOW; + double score = 0; + // Size score + // * v9.31 FIX#119A: ATR-relative FVG sizing (was absolute 10/5 pips) + // Old: XAUUSD FVG=300p -> +25, EURUSD FVG=6p -> +15, EURUSD FVG=3p -> +5 + // Problem: same quality setup (FVG=40% ATR) scored differently by pair + // Fix: use FVG/ATR ratio -- >20% ATR = strong, >10% ATR = moderate + double _fvgATR = (g_cachedATR > 0) ? g_cachedATR / _Point * (g_pipValue / _Point) : 0; + double _fvgRatio = (_fvgATR > 0) ? FVG_Array[index].sizePips / _fvgATR : 0; + if(_fvgRatio >= 0.20 || FVG_Array[index].sizePips >= 10) score += 25; + else if(_fvgRatio >= 0.10 || FVG_Array[index].sizePips >= 5) score += 15; + else score += 5; + // Structure alignment + bool aligned = (FVG_Array[index].isBullish && g_isBullishStructure) || + (!FVG_Array[index].isBullish && !g_isBullishStructure); + if(aligned) score += 25; + else score += 8; // * v7.4 FIX: Was 0 -> counter-structure FVGs scored too low -> no shorts + // PD Zone alignment + bool pdAligned = (FVG_Array[index].isBullish && g_currentPDZone == "DISCOUNT") || + (!FVG_Array[index].isBullish && g_currentPDZone == "PREMIUM"); + if(pdAligned) score += 20; + // Killzone bonus + if(g_isInKillzone) score += 15; + // Volume bonus + if(FVG_Array[index].volumeRatio > 1.5) score += 15; + if(score >= 80) return FVG_QUALITY_PREMIUM; + if(score >= 60) return FVG_QUALITY_HIGH; + if(score >= 40) return FVG_QUALITY_MEDIUM; + return FVG_QUALITY_LOW; +} +//+------------------------------------------------------------------+ +//| Calculate FVG Strength | +//+------------------------------------------------------------------+ +double CalculateFVGStrength(int index, int barIndex) +{ + if(index < 0 || index >= ArraySize(FVG_Array)) return 0; + double strength = 0.5; + // Size factor + strength += MathMin(0.2, FVG_Array[index].sizePips / 50.0); + // Volume factor + long vol = iVolume(_Symbol, _Period, barIndex); + long avgVol = 0; + for(int i = barIndex + 1; i <= barIndex + 20 && i < g_totalRates; i++) + { + avgVol += iVolume(_Symbol, _Period, i); + } + avgVol /= 20; + if(avgVol > 0) + { + FVG_Array[index].volumeRatio = (double)vol / avgVol; + if(FVG_Array[index].volumeRatio > 1.5) + { + strength += 0.15; + } + } + // Displacement factor + double close_i = iClose(_Symbol, _Period, barIndex); + double open_i = iOpen(_Symbol, _Period, barIndex); + double bodySize = MathAbs(close_i - open_i); + FVG_Array[index].impulsiveBody = bodySize; + if(bodySize > g_cachedATR) + { + strength += 0.15; + } + return MathMin(1.0, strength); +} +//+------------------------------------------------------------------+ +//| Update FVG Status | +//+------------------------------------------------------------------+ +void UpdateFVGStatus(const datetime &time[], const double &high[], + const double &low[], const double &close[]) +{ + double currentPrice = close[0]; + for(int i = ArraySize(FVG_Array) - 1; i >= 0; i--) + { + // Skip invalid FVGs + if(FVG_Array[i].status == FVG_STATUS_INVALID) continue; + // Update age + FVG_Array[i].age++; + // [OK] CHECK FOR EXPIRY - DELETE COMPLETELY + if(FVG_AutoExpiry && FVG_Array[i].age > g_workingFVG_MaxAge) + { + FVG_Array[i].status = FVG_STATUS_EXPIRED; + FVG_Array[i].active = false; // Update active flag + DeleteFVGObjects(i); // Delete all objects + // [OK] REMOVE FROM ARRAY (keep only active) + for(int j = i; j < ArraySize(FVG_Array) - 1; j++) + FVG_Array[j] = FVG_Array[j + 1]; + ArrayResize(FVG_Array, ArraySize(FVG_Array) - 1); + g_fvgCount = ArraySize(FVG_Array); // Update count + continue; + } + // Check for mitigation/fill + if(FVG_Array[i].status == FVG_STATUS_ACTIVE) + { + double top = FVG_Array[i].top; + double bottom = FVG_Array[i].bottom; + double ce = FVG_Array[i].ce; + bool shouldDelete = false; + if(FVG_Array[i].isBullish) + { + // Bullish FVG: mitigated when price drops into it + if(low[0] <= ce) + { + FVG_Array[i].status = FVG_STATUS_MITIGATED; + FVG_Array[i].active = false; // Update active flag + FVG_Array[i].touchCount++; + if(FVG_AlertMitigation) + Alert(_Symbol, ": Bullish FVG mitigated at ", DoubleToString(ce, g_digits)); + // Check for inversion + if(FVG_ConvertToInverse && low[0] < bottom) + { + FVG_Array[i].isInverse = true; + FVG_Array[i].type = FVG_TYPE_INVERSE_BULL; + } + else + { + shouldDelete = true; // [OK] Delete if not converting to inverse + } + } + // Check for fill - COMPLETE DELETION + if(low[0] <= bottom) + { + FVG_Array[i].status = FVG_STATUS_FILLED; + FVG_Array[i].active = false; // Update active flag + FVG_Array[i].filled = true; + FVG_Array[i].fillTime = time[0]; + FVG_Array[i].fillPrice = low[0]; + FVG_Array[i].fillPercentage = 100; + if(FVG_AlertFill) + Alert(_Symbol, ": Bullish FVG completely filled!"); + shouldDelete = true; // [OK] Always delete when filled + } + else if(low[0] <= top && low[0] > bottom) + { + FVG_Array[i].fillPercentage = (top - low[0]) / (top - bottom) * 100; + } + } + else // Bearish FVG + { + // Bearish FVG: mitigated when price rises into it + if(high[0] >= ce) + { + FVG_Array[i].status = FVG_STATUS_MITIGATED; + FVG_Array[i].active = false; // Update active flag + FVG_Array[i].touchCount++; + if(FVG_AlertMitigation) + Alert(_Symbol, ": Bearish FVG mitigated at ", DoubleToString(ce, g_digits)); + // Check for inversion + if(FVG_ConvertToInverse && high[0] > top) + { + FVG_Array[i].isInverse = true; + FVG_Array[i].type = FVG_TYPE_INVERSE_BEAR; + } + else + { + shouldDelete = true; // [OK] Delete if not converting to inverse + } + } + // Check for fill - COMPLETE DELETION + if(high[0] >= top) + { + FVG_Array[i].status = FVG_STATUS_FILLED; + FVG_Array[i].active = false; // Update active flag + FVG_Array[i].filled = true; + FVG_Array[i].fillTime = time[0]; + FVG_Array[i].fillPrice = high[0]; + FVG_Array[i].fillPercentage = 100; + if(FVG_AlertFill) + Alert(_Symbol, ": Bearish FVG completely filled!"); + shouldDelete = true; // [OK] Always delete when filled + } + else if(high[0] >= bottom && high[0] < top) + { + FVG_Array[i].fillPercentage = (high[0] - bottom) / (top - bottom) * 100; + } + } + // [OK] DELETE COMPLETED FVGs + if(shouldDelete) + { + DeleteFVGObjects(i); + // Remove from array + for(int j = i; j < ArraySize(FVG_Array) - 1; j++) + FVG_Array[j] = FVG_Array[j + 1]; + ArrayResize(FVG_Array, ArraySize(FVG_Array) - 1); + g_fvgCount = ArraySize(FVG_Array); // Update count + continue; + } + } + FVG_Array[i].lastUpdate = TimeCurrent(); + } +} +//+------------------------------------------------------------------+ +//| 2. ENHANCED DrawFVGs - With Quality Stars & "FVG" Label | +//+------------------------------------------------------------------+ +void DrawFVGs(const datetime &time[]) +{ + if(!g_fvgDisplayEnabled || !ShowZoneBoxes) return; // [v6.42] ShowZoneBoxes master switch + // [v6.42] MaxBarsToShow - limit visual age + int visibleLimit = MathMin(ArraySize(FVG_Array), MaxBarsToShow > 0 ? MaxBarsToShow : 9999); + for(int i = 0; i < ArraySize(FVG_Array); i++) + { + if(FVG_Array[i].status == FVG_STATUS_INVALID || + FVG_Array[i].status == FVG_STATUS_EXPIRED) + { + DeleteFVGObjects(i); + continue; + } + // [v6.42] FVG_MinStrength filter + if(g_workingFVG_MinStrength > 0 && FVG_Array[i].strength < g_workingFVG_MinStrength) + { + DeleteFVGObjects(i); + continue; + } + // [v6.42] FVG_ShowInverse filter + if(FVG_Array[i].isInverse && !FVG_ShowInverse) + { + DeleteFVGObjects(i); + continue; + } + color fvgColor; + if(FVG_Array[i].isInverse) + fvgColor = FVG_Array[i].isBullish ? FVG_InverseBullColor : FVG_InverseBearColor; + else + fvgColor = FVG_Array[i].isBullish ? FVG_BullColor : FVG_BearColor; + datetime endTime = time[0] + PeriodSeconds() * g_workingFVG_ExtendBars; + // Rectangle + string rectName = FVG_Array[i].objNameRect; + if(ObjectFind(0, rectName) < 0) + ObjectCreate(0, rectName, OBJ_RECTANGLE, 0, FVG_Array[i].time, FVG_Array[i].top, endTime, FVG_Array[i].bottom); + ObjectSetInteger(0, rectName, OBJPROP_TIME, 0, FVG_Array[i].time); + ObjectSetDouble(0, rectName, OBJPROP_PRICE, 0, FVG_Array[i].top); + ObjectSetInteger(0, rectName, OBJPROP_TIME, 1, endTime); + ObjectSetDouble(0, rectName, OBJPROP_PRICE, 1, FVG_Array[i].bottom); + ObjectSetInteger(0, rectName, OBJPROP_COLOR, fvgColor); + ObjectSetInteger(0, rectName, OBJPROP_FILL, true); + ObjectSetInteger(0, rectName, OBJPROP_BACK, true); + // [v6.42] FVG_Transparency - 0=opaque, 100=invisible + if(FVG_Transparency > 0 && FVG_Transparency < 100) + { + int alpha = (int)(FVG_Transparency * 255 / 100); + ObjectSetInteger(0, rectName, OBJPROP_FILL, true); + } + // CE line + if(FVG_ShowCE && ShowZoneLines) // [v6.42] ShowZoneLines master + { + string ceName = FVG_Array[i].objNameCE; + if(ObjectFind(0, ceName) < 0) + ObjectCreate(0, ceName, OBJ_TREND, 0, FVG_Array[i].time, FVG_Array[i].ce, endTime, FVG_Array[i].ce); + ObjectSetInteger(0, ceName, OBJPROP_TIME, 0, FVG_Array[i].time); + ObjectSetDouble(0, ceName, OBJPROP_PRICE, 0, FVG_Array[i].ce); + ObjectSetInteger(0, ceName, OBJPROP_TIME, 1, endTime); + ObjectSetDouble(0, ceName, OBJPROP_PRICE, 1, FVG_Array[i].ce); + ObjectSetInteger(0, ceName, OBJPROP_COLOR, fvgColor); + ObjectSetInteger(0, ceName, OBJPROP_STYLE, STYLE_DOT); + ObjectSetInteger(0, ceName, OBJPROP_RAY_RIGHT, false); + } + // [NEW] ENHANCED LABEL WITH STARS + if(FVG_ShowLabels && DrawLabels) + { + string labelName = FVG_Array[i].objNameLabel; + if(ObjectFind(0, labelName) < 0) + ObjectCreate(0, labelName, OBJ_TEXT, 0, FVG_Array[i].time, FVG_Array[i].top); + // Build quality stars based on FVG quality + string qualityStars = ""; + switch(FVG_Array[i].quality) + { + case FVG_QUALITY_PREMIUM: qualityStars = " ***"; break; + case FVG_QUALITY_HIGH: qualityStars = " **"; break; + case FVG_QUALITY_MEDIUM: qualityStars = " *"; break; + default: qualityStars = ""; break; + } + string labelText = "FVG"; + if(FVG_Array[i].isInverse) labelText = "INV FVG"; + labelText += qualityStars; + labelText += " " + DoubleToString(FVG_Array[i].sizePips, 1) + "p"; + // Add direction emoji + string dirEmoji = FVG_Array[i].isBullish ? "[GREEN]" : "[RED]"; + labelText = dirEmoji + " " + labelText; + ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); + ObjectSetInteger(0, labelName, OBJPROP_COLOR, fvgColor); + ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, FVG_FontSize); + ObjectSetString(0, labelName, OBJPROP_FONT, "Arial Bold"); + ObjectSetDouble(0, labelName, OBJPROP_PRICE, FVG_Array[i].top + g_pipValue * 2); + } + // [NEW] "FVG" LABEL INSIDE BOX (TikTok style) + string insideLabelName = FVG_Array[i].objNameRect + "_Inside"; + double midPrice = (FVG_Array[i].top + FVG_Array[i].bottom) / 2; + if(ObjectFind(0, insideLabelName) >= 0) ObjectDelete(0, insideLabelName); + if(ObjectCreate(0, insideLabelName, OBJ_TEXT, 0, FVG_Array[i].time, midPrice)) + { + ObjectSetString(0, insideLabelName, OBJPROP_TEXT, "FVG"); + ObjectSetInteger(0, insideLabelName, OBJPROP_COLOR, clrWhite); + ObjectSetInteger(0, insideLabelName, OBJPROP_FONTSIZE, 10); + ObjectSetString(0, insideLabelName, OBJPROP_FONT, "Arial Bold"); + ObjectSetInteger(0, insideLabelName, OBJPROP_ANCHOR, ANCHOR_CENTER); + } + } +} +//+------------------------------------------------------------------+ +//| Delete FVG Objects | +//+------------------------------------------------------------------+ +void DeleteFVGObjects(int index) +{ + if(index < 0 || index >= ArraySize(FVG_Array)) + return; + // Delete ALL possible objects related to this FVG + CleanObject(FVG_Array[index].objNameRect); // Main rectangle + CleanObject(FVG_Array[index].objNameLabel); // Label + CleanObject(FVG_Array[index].objNameCE); // Consequent Encroachment line + // Delete additional objects that may exist + string baseName = FVG_Array[index].objNameRect; + if(StringLen(baseName) > 0) + { + CleanObject(baseName + "_Arrow"); // Entry arrow + CleanObject(baseName + "_MIT"); // Mitigation marker + CleanObject(baseName + "_Entry"); // Entry level + CleanObject(baseName + "_Quality"); // Quality indicator + CleanObject(baseName + "_Fill"); // Fill percentage + } + // Clear the names + FVG_Array[index].objNameRect = ""; + FVG_Array[index].objNameLabel = ""; + FVG_Array[index].objNameCE = ""; +} +//+------------------------------------------------------------------+ +//| Delete Order Block Objects | +//+------------------------------------------------------------------+ +void DeleteOrderBlockObjects(int index) +{ + if(index < 0 || index >= ArraySize(OB_Array)) + return; + // Generate object name from ID + string objName = "ICT_OB_" + IntegerToString(OB_Array[index].id); + // Delete main object + ObjectDelete(0, objName); + // Delete all possible related objects + ObjectDelete(0, objName + "_Label"); + ObjectDelete(0, objName + "_Strength"); + ObjectDelete(0, objName + "_Break"); + ObjectDelete(0, objName + "_Entry"); + ObjectDelete(0, objName + "_MIT"); + ObjectDelete(0, objName + "_Top"); + ObjectDelete(0, objName + "_Bottom"); + ObjectDelete(0, objName + "_Inside"); + ObjectDelete(0, objName + "_Dollar"); +} +//+------------------------------------------------------------------+ +//| Detect Order Blocks - OPTIMIZED | +//+------------------------------------------------------------------+ +void DetectOrderBlocks(const datetime &time[], const double &open[], const double &high[], + const double &low[], const double &close[], const long &volume[], int limit) +{ + if(limit < 5) return; + int maxBars = MathMin(limit, MaxBarsToCalculate); + if(maxBars < 22) return; // Need at least 22 bars for avgVol calculation + // OPTIMIZATION 1: Pre-calculate initial average volume (sliding window) + long volSum = 0; + for(int j = 3; j <= 22; j++) + { + volSum += volume[j]; + } + double volMultiplier = g_workingOBVolumeMult; // * v7.5b: Use auto-opt value instead of raw input + bool requireVolume = OB_RequireVolume; + for(int i = 2; i < maxBars - 2; i++) + { + // OPTIMIZATION 2: Update sliding window for avgVol + if(i > 2 && i + 20 < maxBars) + { + volSum = volSum - volume[i] + volume[i + 20]; + } + long avgVol = volSum / 20; + // Volume confirmation check + bool volumeConfirm = !requireVolume || (volume[i] > avgVol * volMultiplier); + if(!volumeConfirm) continue; // Early exit if volume fails + // Bullish Order Block: Last down candle before up move + bool isBullishOB = false; + if(close[i] < open[i]) // Down candle + { + // Check for subsequent bullish displacement + if((close[i - 1] > open[i - 1] && close[i - 1] > high[i]) || + (close[i - 1] > high[i] || close[i - 2] > high[i])) + { + isBullishOB = true; + } + } + // Bearish Order Block: Last up candle before down move + bool isBearishOB = false; + if(!isBullishOB && close[i] > open[i]) // Up candle (skip if already bullish) + { + // Check for subsequent bearish displacement + if((close[i - 1] < open[i - 1] && close[i - 1] < low[i]) || + (close[i - 1] < low[i] || close[i - 2] < low[i])) + { + isBearishOB = true; + } + } + if(isBullishOB || isBearishOB) + { + AddOrderBlock(time[i], high[i], low[i], open[i], close[i], + isBullishOB, volume[i], avgVol); + } + } +} +//+------------------------------------------------------------------+ +//| [NEW] MANAGE ORDER BLOCKS - ΠΡΟΣΘΗΚΗ (όχι αντικατάσταση!) | +//| Βάλε μετά το τέλος της DetectOrderBlocks() | +//+------------------------------------------------------------------+ +void ManageOrderBlocks(const datetime &time[], const double &high[], + const double &low[], const double &close[]) +{ + if(!ShowOB || ArraySize(OB_Array) == 0) return; + double currentPrice = close[0]; + for(int i = ArraySize(OB_Array) - 1; i >= 0; i--) // [OK] Backwards loop for safe deletion + { + if(!OB_Array[i].active) continue; + // Update age + OB_Array[i].age++; + bool shouldExpire = false; + // [OK] CHECK EXPIRY + if(OB_AutoExpire && OB_Array[i].age > g_workingOB_MaxAge) + shouldExpire = true; + // [OK] CHECK IF BROKEN + if(OB_Array[i].status != "BROKEN") + { + bool isBroken = false; + if(OB_Array[i].type == 1) // Bullish OB + { + // Broken if price closes below bottom + if(close[0] < OB_Array[i].bottom - g_cachedATR * 0.1) + isBroken = true; + } + else // Bearish OB + { + // Broken if price closes above top + if(close[0] > OB_Array[i].top + g_cachedATR * 0.1) + isBroken = true; + } + if(isBroken) + { + OB_Array[i].status = "BROKEN"; + OB_Array[i].active = false; // Update active flag + OB_Array[i].breakTime = time[0]; + OB_Array[i].breakPrice = close[0]; + shouldExpire = true; // [OK] Expire broken OBs + // Optional: Draw break marker + if(OB_ShowBreak) + { + string objName = "ICT_OB_" + IntegerToString(OB_Array[i].id); + if(Trendline_ShowBreaks) // [v6.42] + { + string breakName = objName + "_Break"; + ObjectDelete(0, breakName); + if(ObjectCreate(0, breakName, OBJ_TREND, 0, time[0], close[0], + time[0] + PeriodSeconds(_Period) * 5, close[0])) + { + ObjectSetInteger(0, breakName, OBJPROP_COLOR, clrRed); + ObjectSetInteger(0, breakName, OBJPROP_WIDTH, 2); + ObjectSetInteger(0, breakName, OBJPROP_STYLE, STYLE_DOT); + ObjectSetInteger(0, breakName, OBJPROP_RAY_RIGHT, false); + } + } + } + } + } + // [OK] CHECK MITIGATION + if(OB_Array[i].status == "ACTIVE" || OB_Array[i].status == "") + { + bool isMitigated = false; + if(OB_Array[i].type == 1) // Bullish OB + { + // Mitigated when price touches the OB + if(low[0] <= OB_Array[i].top && low[0] >= OB_Array[i].bottom) + isMitigated = true; + } + else // Bearish OB + { + // Mitigated when price touches the OB + if(high[0] >= OB_Array[i].bottom && high[0] <= OB_Array[i].top) + isMitigated = true; + } + if(isMitigated) + { + OB_Array[i].status = "MITIGATED"; + OB_Array[i].active = false; // Update active flag + OB_Array[i].touchCount++; + OB_Array[i].lastTouchTime = time[0]; + // Alert + if(OB_AlertMitigation) + { + string obType = (OB_Array[i].type == 1) ? "Bullish" : "Bearish"; + Alert(_Symbol, ": ", obType, " Order Block mitigated!"); + } + // Optional: Expire on mitigation + if(OB_ExpireOnMitigation) + shouldExpire = true; + // Optional: Change color when mitigated + if(!shouldExpire) + { + string objName = "ICT_OB_" + IntegerToString(OB_Array[i].id); + color mitigatedColor = (OB_Array[i].type == 1) ? + clrDarkGreen : clrDarkRed; + ObjectSetInteger(0, objName, OBJPROP_COLOR, mitigatedColor); + } + } + } + // [OK] EXPIRE AND DELETE COMPLETELY + if(shouldExpire) + { + DeleteOrderBlockObjects(i); // [OK] Delete ALL objects + OB_Array[i].active = false; + // [OK] Remove from array (keep only active) + for(int j = i; j < ArraySize(OB_Array) - 1; j++) + OB_Array[j] = OB_Array[j + 1]; + ArrayResize(OB_Array, ArraySize(OB_Array) - 1); + g_obCount = ArraySize(OB_Array); // Update count + } + } +} +//+------------------------------------------------------------------+ +//| Add Order Block to Array - OPTIMIZED | +//+------------------------------------------------------------------+ +void AddOrderBlock(datetime time, double high, double low, double open, double close, + bool isBullish, long vol, long avgVol) +{ + int size = ArraySize(OB_Array); + // OPTIMIZATION 1: Check duplicates from END, limit to last 30 entries + int checkLimit = MathMin(size, 30); + for(int i = size - 1; i >= size - checkLimit && i >= 0; i--) + { + if(OB_Array[i].time == time) return; + } + // OPTIMIZATION 2: Handle max capacity + if(size >= MAX_OB_ARRAY) + { + // Manual shift (required for structs with strings) + for(int i = 0; i < size - 1; i++) + { + OB_Array[i] = OB_Array[i + 1]; + } + size--; + ArrayResize(OB_Array, size, 30); // Shrink with reserve + } + // OPTIMIZATION 3: Reserve allocation - reserves 30 extra slots + ArrayResize(OB_Array, size + 1, 30); + OB_Array[size].id = ++g_obIdCounter; + OB_Array[size].time = time; + OB_Array[size].high = high; + OB_Array[size].low = low; + OB_Array[size].top = isBullish ? MathMax(open, close) : high; + OB_Array[size].bottom = isBullish ? low : MathMin(open, close); + OB_Array[size].isBullish = isBullish; + OB_Array[size].type = isBullish ? 1 : -1; // Initialize type field + OB_Array[size].mitigated = false; + OB_Array[size].age = 0; + OB_Array[size].volume = (double)vol; + OB_Array[size].strength = (avgVol > 0) ? (double)vol / avgVol : 1.0; + OB_Array[size].isBreakerBlock = false; + OB_Array[size].touchCount = 0; + OB_Array[size].bodySize = MathAbs(close - open); + OB_Array[size].wickRatio = (high - low > 0) ? OB_Array[size].bodySize / (high - low) : 0; + OB_Array[size].status = "ACTIVE"; + OB_Array[size].active = true; // Initialize active field + OB_Array[size].breakTime = 0; // Initialize breakTime + OB_Array[size].breakPrice = 0; // Initialize breakPrice + OB_Array[size].lastTouchTime = 0; // Initialize lastTouchTime + // Update count for EA compatibility + g_obCount = ArraySize(OB_Array); +} +//+------------------------------------------------------------------+ +//| Update Order Block Status | +//+------------------------------------------------------------------+ +void UpdateOrderBlockStatus(const datetime &time[], const double &high[], + const double &low[], const double &close[]) +{ + for(int i = ArraySize(OB_Array) - 1; i >= 0; i--) + { + // Update age + OB_Array[i].age++; + // Check for expiry + if(OB_Array[i].age > g_workingOB_MaxAge) + { + OB_Array[i].status = "EXPIRED"; + OB_Array[i].active = false; // Update active flag + continue; + } + // Check for mitigation + if(!OB_Array[i].mitigated) + { + if(OB_Array[i].isBullish) + { + // Bullish OB mitigated when price returns to it + if(low[0] <= OB_Array[i].top) + { + OB_Array[i].touchCount++; + if(low[0] <= OB_Array[i].bottom) + { + OB_Array[i].mitigated = true; + OB_Array[i].status = "MITIGATED"; + OB_Array[i].active = false; // Update active flag + // Could become breaker block if structure changes + if(!g_isBullishStructure) + { + OB_Array[i].isBreakerBlock = true; + OB_Array[i].status = "BREAKER"; + } + } + } + } + else // Bearish OB + { + if(high[0] >= OB_Array[i].bottom) + { + OB_Array[i].touchCount++; + if(high[0] >= OB_Array[i].top) + { + OB_Array[i].mitigated = true; + OB_Array[i].status = "MITIGATED"; + OB_Array[i].active = false; // Update active flag + if(g_isBullishStructure) + { + OB_Array[i].isBreakerBlock = true; + OB_Array[i].status = "BREAKER"; + } + } + } + } + } + } +} +//+------------------------------------------------------------------+ +//| ENHANCED DrawOrderBlocks - With Stars & "$" Symbol | +//+------------------------------------------------------------------+ +void DrawOrderBlocks(const datetime &time[]) +{ + if(!g_obDisplayEnabled) return; + for(int i = 0; i < ArraySize(OB_Array); i++) + { + // Check if expired or mitigated + if(OB_Array[i].status == "EXPIRED" || + (OB_Array[i].mitigated && !OB_Array[i].isBreakerBlock)) + { + string objName = "ICT_OB_" + IntegerToString(OB_Array[i].id); + ObjectDelete(0, objName); + ObjectDelete(0, objName + "_Label"); + ObjectDelete(0, objName + "_Inside"); + ObjectDelete(0, objName + "_Dollar"); + continue; + } + // Determine color + color obColor; + if(OB_Array[i].isBreakerBlock) + obColor = OB_Array[i].isBullish ? BREAKER_BearColor : BREAKER_BullColor; + else + obColor = OB_Array[i].isBullish ? OB_BullColor : OB_BearColor; + datetime endTime = time[0] + PeriodSeconds() * 50; + string objName = "ICT_OB_" + IntegerToString(OB_Array[i].id); + // Rectangle + if(ObjectFind(0, objName) < 0) + { + ObjectCreate(0, objName, OBJ_RECTANGLE, 0, + OB_Array[i].time, OB_Array[i].top, + endTime, OB_Array[i].bottom); + } + ObjectSetInteger(0, objName, OBJPROP_TIME, 0, OB_Array[i].time); + ObjectSetDouble(0, objName, OBJPROP_PRICE, 0, OB_Array[i].top); + ObjectSetInteger(0, objName, OBJPROP_TIME, 1, endTime); + ObjectSetDouble(0, objName, OBJPROP_PRICE, 1, OB_Array[i].bottom); + ObjectSetInteger(0, objName, OBJPROP_COLOR, obColor); + ObjectSetInteger(0, objName, OBJPROP_FILL, true); + ObjectSetInteger(0, objName, OBJPROP_BACK, true); + // [NEW] ENHANCED LABEL WITH STARS + if(OB_ShowStrength) + { + string labelName = objName + "_Label"; + if(ObjectFind(0, labelName) < 0) + ObjectCreate(0, labelName, OBJ_TEXT, 0, OB_Array[i].time, OB_Array[i].top); + // Quality stars based on strength + string qualityStars = ""; + if(OB_Array[i].strength >= 2.5) qualityStars = " ***"; + else if(OB_Array[i].strength >= 1.5) qualityStars = " **"; + else if(OB_Array[i].strength >= 1.0) qualityStars = " *"; + string dirEmoji = OB_Array[i].isBullish ? "[GREEN]" : "[RED]"; + string typeText = OB_Array[i].isBreakerBlock ? "BB" : "OB"; + string statusText = ""; + if(OB_Array[i].mitigated) statusText = " [M]"; + else if(OB_Array[i].touchCount > 0) statusText = " [T]"; + string labelText = StringFormat("%s %s %.1fx%s%s", + dirEmoji, typeText, + OB_Array[i].strength, + qualityStars, statusText); + ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); + ObjectSetInteger(0, labelName, OBJPROP_COLOR, obColor); + ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 8); + ObjectSetString(0, labelName, OBJPROP_FONT, "Arial Bold"); + ObjectSetDouble(0, labelName, OBJPROP_PRICE, OB_Array[i].top + g_pipValue * 3); + } + // [NEW] "ORDER BLOCK" LABEL INSIDE BOX (TikTok style) + string insideLabelName = objName + "_Inside"; + double midPrice = (OB_Array[i].top + OB_Array[i].bottom) / 2; + if(ObjectFind(0, insideLabelName) >= 0) ObjectDelete(0, insideLabelName); + if(ObjectCreate(0, insideLabelName, OBJ_TEXT, 0, OB_Array[i].time, midPrice)) + { + string insideText = OB_Array[i].isBreakerBlock ? "BREAKER" : "ORDER BLOCK"; + ObjectSetString(0, insideLabelName, OBJPROP_TEXT, insideText); + ObjectSetInteger(0, insideLabelName, OBJPROP_COLOR, clrWhite); + ObjectSetInteger(0, insideLabelName, OBJPROP_FONTSIZE, 8); + ObjectSetString(0, insideLabelName, OBJPROP_FONT, "Arial Bold"); + ObjectSetInteger(0, insideLabelName, OBJPROP_ANCHOR, ANCHOR_CENTER); + } + // [NEW] "$" SYMBOL AT ENTRY POINT (TikTok style) + string dollarName = objName + "_Dollar"; + double entryPrice = OB_Array[i].isBullish ? OB_Array[i].bottom : OB_Array[i].top; + if(ObjectFind(0, dollarName) >= 0) ObjectDelete(0, dollarName); + if(ObjectCreate(0, dollarName, OBJ_TEXT, 0, OB_Array[i].time, entryPrice)) + { + ObjectSetString(0, dollarName, OBJPROP_TEXT, "[MONEY]"); + ObjectSetInteger(0, dollarName, OBJPROP_COLOR, clrGold); + ObjectSetInteger(0, dollarName, OBJPROP_FONTSIZE, 14); + ObjectSetInteger(0, dollarName, OBJPROP_ANCHOR, OB_Array[i].isBullish ? ANCHOR_TOP : ANCHOR_BOTTOM); + } + } +} +//+------------------------------------------------------------------+ +//| Detect Liquidity - OPTIMIZED | +//+------------------------------------------------------------------+ +void DetectLiquidity(const datetime &time[], const double &high[], + const double &low[], const double &close[], int limit) +{ + int swingStrength = g_workingLIQ_SwingStrength; + if(limit < swingStrength * 2 + 1) return; + int maxBars = MathMin(limit, MaxBarsToCalculate); + // OPTIMIZATION: Cache tolerance value + double tolerance = g_cachedATR * 0.1; + // OPTIMIZATION: Single pass for both BSL and SSL + for(int i = swingStrength; i < maxBars - swingStrength; i++) + { + // =========================================================== + // Check for Swing High (BSL) + // =========================================================== + bool isSwingHigh = true; + double currentHigh = high[i]; + for(int j = 1; j <= swingStrength; j++) + { + if(currentHigh <= high[i - j] || currentHigh <= high[i + j]) + { + isSwingHigh = false; + break; + } + } + if(isSwingHigh) + { + // Check for equal highs nearby + int equalHighCount = 1; + double avgHigh = currentHigh; + // OPTIMIZATION: Limit equal high search to 20 bars + int searchLimit = MathMin(20, maxBars - i - 1); + for(int k = 1; k <= searchLimit; k++) + { + if(MathAbs(high[i + k] - currentHigh) < tolerance) + { + equalHighCount++; + avgHigh += high[i + k]; + } + } + if(equalHighCount >= 2) + { + avgHigh /= equalHighCount; + AddLiquidityLevel(time[i], avgHigh, true, equalHighCount); + } + else + { + AddLiquidityLevel(time[i], currentHigh, true, 1); + } + } + // =========================================================== + // Check for Swing Low (SSL) + // =========================================================== + bool isSwingLow = true; + double currentLow = low[i]; + for(int j = 1; j <= swingStrength; j++) + { + if(currentLow >= low[i - j] || currentLow >= low[i + j]) + { + isSwingLow = false; + break; + } + } + if(isSwingLow) + { + // Check for equal lows nearby + int equalLowCount = 1; + double avgLow = currentLow; + // OPTIMIZATION: Limit equal low search to 20 bars + int searchLimit = MathMin(20, maxBars - i - 1); + for(int k = 1; k <= searchLimit; k++) + { + if(MathAbs(low[i + k] - currentLow) < tolerance) + { + equalLowCount++; + avgLow += low[i + k]; + } + } + if(equalLowCount >= 2) + { + avgLow /= equalLowCount; + AddLiquidityLevel(time[i], avgLow, false, equalLowCount); + } + else + { + AddLiquidityLevel(time[i], currentLow, false, 1); + } + } + } +} +//+------------------------------------------------------------------+ +//| Add Liquidity Level | +//+------------------------------------------------------------------+ +void AddLiquidityLevel(datetime time, double price, bool isBSL, int touches) +{ + // Check for nearby existing level + for(int i = 0; i < ArraySize(LIQ_Array); i++) + { + if(MathAbs(LIQ_Array[i].price - price) < g_cachedATR * 0.2 && + LIQ_Array[i].isBSL == isBSL) + { + // Update existing level + LIQ_Array[i].touches += touches; + LIQ_Array[i].strength = MathMin(1.0, LIQ_Array[i].touches * 0.2); + return; + } + } + // Add new level + int size = ArraySize(LIQ_Array); + if(size >= MAX_LIQ_ARRAY) + { + for(int i = 0; i < size - 1; i++) + { + LIQ_Array[i] = LIQ_Array[i + 1]; + } + size--; + } + ArrayResize(LIQ_Array, size + 1); + LIQ_Array[size].id = size + 1; + LIQ_Array[size].time = time; + LIQ_Array[size].price = price; + LIQ_Array[size].isBSL = isBSL; + LIQ_Array[size].swept = false; + LIQ_Array[size].age = 0; + LIQ_Array[size].touches = touches; + LIQ_Array[size].strength = MathMin(1.0, touches * 0.2); + LIQ_Array[size].sweepTime = 0; + LIQ_Array[size].sweepPrice = 0; + LIQ_Array[size].isValid = true; +} +//+------------------------------------------------------------------+ +//| Check Liquidity Sweeps | +//+------------------------------------------------------------------+ +void CheckLiquiditySweeps(const datetime &time[], const double &high[], + const double &low[], const double &close[]) +{ + // * FIX#457: LIQ_SweepBuffer scaling for non-Forex pairs. + // BUG: LIQ_SweepBuffer(20) * g_pipValue = 20*0.01 = 0.20 for XAUUSD. + // XAUUSD H4 ATR≈300-500pts → 20-cent buffer = trivially small (noise-level). + // FIX: same pattern as FVG_MinSize — pip/point ratio determines calculation. + // Forex (ratio>=9): 20 points * pipValue = 2 pips ✅ + // Non-Forex (ratio<2, e.g. XAUUSD): use ATR-relative buffer = ATR * 0.05 (5% of ATR) + double _pipPointRatioLIQ = (g_point > 0) ? g_pipValue / g_point : 1.0; + double bufferPips; + if(_pipPointRatioLIQ >= 9.0) + bufferPips = LIQ_SweepBuffer * g_pipValue; // Standard Forex + else if(_pipPointRatioLIQ < 2.0 && g_cachedATR > 0) + bufferPips = g_cachedATR * 0.05; // Non-Forex: 5% of ATR + else + bufferPips = LIQ_SweepBuffer * g_point; // Fallback + for(int i = ArraySize(LIQ_Array) - 1; i >= 0; i--) + { + // Update age + LIQ_Array[i].age++; + // Check for expiry + if(LIQ_Array[i].age > g_workingLIQ_MaxAge) + { + LIQ_Array[i].isValid = false; + continue; + } + // Skip already swept + if(LIQ_Array[i].swept) continue; + if(LIQ_Array[i].isBSL) // Buy Side Liquidity + { + // Swept when price goes above and returns below + if(high[0] > LIQ_Array[i].price + bufferPips) + { + if(close[0] < LIQ_Array[i].price) + { + LIQ_Array[i].swept = true; + LIQ_Array[i].sweepTime = time[0]; + LIQ_Array[i].sweepPrice = high[0]; + if(LIQ_ShowSweeps) + { + DrawLiquiditySweep(i); + } + } + } + } + else // Sell Side Liquidity + { + // Swept when price goes below and returns above + if(low[0] < LIQ_Array[i].price - bufferPips) + { + if(close[0] > LIQ_Array[i].price) + { + LIQ_Array[i].swept = true; + LIQ_Array[i].sweepTime = time[0]; + LIQ_Array[i].sweepPrice = low[0]; + if(LIQ_ShowSweeps) + { + DrawLiquiditySweep(i); + } + } + } + } + } +} +//+------------------------------------------------------------------+ +//| Draw Liquidity Levels | +//+------------------------------------------------------------------+ +void DrawLiquidity(const datetime &time[]) +{ + if(!g_liqDisplayEnabled) return; + for(int i = 0; i < ArraySize(LIQ_Array); i++) + { + if(!LIQ_Array[i].isValid) + { + ObjectDelete(0, "ICT_LIQ_" + IntegerToString(i)); + ObjectDelete(0, "ICT_LIQ_Label_" + IntegerToString(i)); + continue; + } + color liqColor = LIQ_Array[i].isBSL ? LIQ_BSLColor : LIQ_SSLColor; + datetime endTime = time[0] + PeriodSeconds() * 50; + string objName = "ICT_LIQ_" + IntegerToString(i); + if(ObjectFind(0, objName) < 0) + { + ObjectCreate(0, objName, OBJ_TREND, 0, + LIQ_Array[i].time, LIQ_Array[i].price, + endTime, LIQ_Array[i].price); + } + ObjectSetInteger(0, objName, OBJPROP_TIME, 0, LIQ_Array[i].time); + ObjectSetDouble(0, objName, OBJPROP_PRICE, 0, LIQ_Array[i].price); + ObjectSetInteger(0, objName, OBJPROP_TIME, 1, endTime); + ObjectSetDouble(0, objName, OBJPROP_PRICE, 1, LIQ_Array[i].price); + ObjectSetInteger(0, objName, OBJPROP_COLOR, liqColor); + ObjectSetInteger(0, objName, OBJPROP_STYLE, LIQ_Array[i].swept ? STYLE_DOT : STYLE_DASH); + ObjectSetInteger(0, objName, OBJPROP_WIDTH, LIQ_Array[i].swept ? 1 : 2); + ObjectSetInteger(0, objName, OBJPROP_RAY_RIGHT, false); + // Label + string labelName = "ICT_LIQ_Label_" + IntegerToString(i); + if(ObjectFind(0, labelName) < 0) + { + ObjectCreate(0, labelName, OBJ_TEXT, 0, endTime, LIQ_Array[i].price); + } + string labelText = LIQ_Array[i].isBSL ? "BSL" : "SSL"; + if(LIQ_Array[i].swept) labelText += " [OK]"; + ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); + ObjectSetInteger(0, labelName, OBJPROP_COLOR, liqColor); + ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 8); + } +} +//+------------------------------------------------------------------+ +//| Draw Liquidity Sweep | +//+------------------------------------------------------------------+ +void DrawLiquiditySweep(int index) +{ + if(index < 0 || index >= ArraySize(LIQ_Array)) return; + string objName = "ICT_LIQ_Sweep_" + IntegerToString(index); + if(ObjectFind(0, objName) < 0) + { + ObjectCreate(0, objName, OBJ_ARROW, 0, + LIQ_Array[index].sweepTime, LIQ_Array[index].sweepPrice); + } + ObjectSetInteger(0, objName, OBJPROP_ARROWCODE, LIQ_Array[index].isBSL ? 234 : 233); + ObjectSetInteger(0, objName, OBJPROP_COLOR, clrYellow); + ObjectSetInteger(0, objName, OBJPROP_WIDTH, 2); +} +//+------------------------------------------------------------------+ +//| 6. DETECT OTE ZONES - ΠΛΗΡΗΣ ΥΛΟΠΟΙΗΣΗ | +//+------------------------------------------------------------------+ +void DetectOTEZones(const datetime &time[], const double &high[], + const double &low[], const double &close[], int limit) +{ + if(limit < OTE_LookbackBars) return; + // Find recent significant swing points + double swingHigh = 0, swingLow = DBL_MAX; + datetime swingHighTime = 0, swingLowTime = 0; + int highBar = -1, lowBar = -1; + for(int i = 0; i < OTE_LookbackBars && i < limit; i++) + { + if(high[i] > swingHigh) + { + swingHigh = high[i]; + swingHighTime = time[i]; + highBar = i; + } + if(low[i] < swingLow) + { + swingLow = low[i]; + swingLowTime = time[i]; + lowBar = i; + } + } + if(swingHigh == 0 || swingLow == DBL_MAX) return; + double range = swingHigh - swingLow; + if(range < g_cachedATR * 0.5) return; // Too small range + // Determine if bullish or bearish based on which swing came first + bool isBullish = (lowBar > highBar); // Low came before high = bullish swing + // Calculate Fibonacci levels + double level618, level786, level705; + if(isBullish) + { + level618 = swingHigh - range * 0.618; + level786 = swingHigh - range * 0.786; + level705 = swingHigh - range * 0.705; // Optimal entry + } + else + { + level618 = swingLow + range * 0.618; + level786 = swingLow + range * 0.786; + level705 = swingLow + range * 0.705; + } + // Check if OTE zone already exists + for(int i = 0; i < ArraySize(OTE_Array); i++) + { + if(MathAbs(OTE_Array[i].level618 - level618) < g_cachedATR * 0.1) + { + // Update existing + OTE_Array[i].age = 0; + return; + } + } + // Add new OTE zone + int size = ArraySize(OTE_Array); + if(size >= MAX_OTE_ARRAY) + { + for(int i = 0; i < size - 1; i++) + { + OTE_Array[i] = OTE_Array[i + 1]; + } + size--; + } + ArrayResize(OTE_Array, size + 1); + OTE_Array[size].time = time[0]; + OTE_Array[size].isBullish = isBullish; + OTE_Array[size].high = isBullish ? level618 : level786; + OTE_Array[size].low = isBullish ? level786 : level618; + OTE_Array[size].optimal = level705; + OTE_Array[size].swingHigh = swingHigh; + OTE_Array[size].swingLow = swingLow; + OTE_Array[size].level618 = level618; + OTE_Array[size].level786 = level786; + OTE_Array[size].level705 = level705; + OTE_Array[size].active = true; + OTE_Array[size].touched = false; + OTE_Array[size].age = 0; + OTE_Array[size].fibLevel = 0.705; + OTE_Array[size].isValid = true; +} +//+------------------------------------------------------------------+ +//| Draw OTE Zones | +//+------------------------------------------------------------------+ +void DrawOTEZones(const datetime &time[]) +{ + for(int i = 0; i < ArraySize(OTE_Array); i++) + { + OTE_Array[i].age++; + if(OTE_Array[i].age > g_workingOTE_MaxAge || !OTE_Array[i].isValid) + { + ObjectDelete(0, "ICT_OTE_" + IntegerToString(i)); + ObjectDelete(0, "ICT_OTE_618_" + IntegerToString(i)); + ObjectDelete(0, "ICT_OTE_786_" + IntegerToString(i)); + ObjectDelete(0, "ICT_OTE_705_" + IntegerToString(i)); + continue; + } + datetime endTime = time[0] + PeriodSeconds() * OTE_ExtendBars; + color oteColor = OTE_Array[i].isBullish ? clrDodgerBlue : clrOrangeRed; + // OTE Zone rectangle + string rectName = "ICT_OTE_" + IntegerToString(i); + if(ObjectFind(0, rectName) < 0) + { + ObjectCreate(0, rectName, OBJ_RECTANGLE, 0, + OTE_Array[i].time, OTE_Array[i].level618, + endTime, OTE_Array[i].level786); + } + ObjectSetInteger(0, rectName, OBJPROP_TIME, 0, OTE_Array[i].time); + ObjectSetDouble(0, rectName, OBJPROP_PRICE, 0, OTE_Array[i].level618); + ObjectSetInteger(0, rectName, OBJPROP_TIME, 1, endTime); + ObjectSetDouble(0, rectName, OBJPROP_PRICE, 1, OTE_Array[i].level786); + ObjectSetInteger(0, rectName, OBJPROP_COLOR, oteColor); + ObjectSetInteger(0, rectName, OBJPROP_FILL, true); + ObjectSetInteger(0, rectName, OBJPROP_BACK, true); + // 0.618 line + string line618 = "ICT_OTE_618_" + IntegerToString(i); + if(ObjectFind(0, line618) < 0) + { + ObjectCreate(0, line618, OBJ_TREND, 0, + OTE_Array[i].time, OTE_Array[i].level618, + endTime, OTE_Array[i].level618); + } + ObjectSetInteger(0, line618, OBJPROP_COLOR, oteColor); + ObjectSetInteger(0, line618, OBJPROP_STYLE, STYLE_DASH); + // 0.786 line + string line786 = "ICT_OTE_786_" + IntegerToString(i); + if(ObjectFind(0, line786) < 0) + { + ObjectCreate(0, line786, OBJ_TREND, 0, + OTE_Array[i].time, OTE_Array[i].level786, + endTime, OTE_Array[i].level786); + } + ObjectSetInteger(0, line786, OBJPROP_COLOR, oteColor); + ObjectSetInteger(0, line786, OBJPROP_STYLE, STYLE_DASH); + // 0.705 optimal entry line + string line705 = "ICT_OTE_705_" + IntegerToString(i); + if(ObjectFind(0, line705) < 0) + { + ObjectCreate(0, line705, OBJ_TREND, 0, + OTE_Array[i].time, OTE_Array[i].level705, + endTime, OTE_Array[i].level705); + } + ObjectSetInteger(0, line705, OBJPROP_COLOR, clrGold); + ObjectSetInteger(0, line705, OBJPROP_STYLE, STYLE_SOLID); + ObjectSetInteger(0, line705, OBJPROP_WIDTH, 2); + } +} +//+------------------------------------------------------------------+ +//| 7. SIGNAL ENTRY CHECKS - ΠΛΗΡΕΙΣ ΥΛΟΠΟΙΗΣΕΙΣ | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| Check FVG Entry Signal | +//+------------------------------------------------------------------+ +void CheckFVGEntrySignal(const datetime &time[], const double &high[], + const double &low[], const double &close[], + const double &open[], double currentPrice) // [NEW] Πρόσθεσε open[] +{ + for(int i = 0; i < ArraySize(FVG_Array); i++) + { + if(FVG_Array[i].status != FVG_STATUS_ACTIVE && + FVG_Array[i].status != FVG_STATUS_MITIGATED) continue; + // Check if price is in FVG zone + if(currentPrice >= FVG_Array[i].bottom && currentPrice <= FVG_Array[i].top) + { + bool isBullish = FVG_Array[i].isBullish; + // Structure alignment check + if((isBullish && !g_isBullishStructure) || (!isBullish && g_isBullishStructure)) + { + if(FVG_Array[i].touchCount < 1) continue; + } + // Calculate confluence + double confluence = 0.4; + if((isBullish && g_isBullishStructure) || (!isBullish && !g_isBullishStructure)) + confluence += 0.2; + if((isBullish && g_currentPDZone == "DISCOUNT") || + (!isBullish && g_currentPDZone == "PREMIUM")) + confluence += 0.15; + if(g_isInKillzone) + confluence += GetKillzoneQualityBonus(g_killzoneDefinitions[g_currentKillzoneIndex].type); + int obIdx; + if(IsPriceNearOB(currentPrice, obIdx, isBullish)) + confluence += 0.15; + if(confluence < g_workingMinConfluence) continue; + // Calculate quality + double quality = CalculateEntryQuality(currentPrice, isBullish, "FVG_ENTRY", confluence); + if(quality < g_workingMinEntryQuality) continue; + // Calculate SL/TP + double sl, tp; + if(isBullish) + { + sl = FVG_Array[i].bottom - g_cachedATR * 0.3; + // * v9.36 FIX#145: FVG structural SL cap -- max 2.0×ATR from entry. + // Root cause of ID=7 loss: FVG bottom was 15.2p below entry, ATR=4.9p + // → SL=15.2p, all trail/BE thresholds unreachable → guaranteed full loss. + // Defense layer 1 (at source): clamp SL so dist <= 2.0×ATR. + // Defense layer 2 (FIX#141): reject at signal evaluation if still > 2.5×ATR. + if(g_cachedATR > 0 && (currentPrice - sl) > g_cachedATR * 2.0) + sl = currentPrice - g_cachedATR * 2.0; + tp = currentPrice + (currentPrice - sl) * g_workingMinRiskReward; + } + else + { + sl = FVG_Array[i].top + g_cachedATR * 0.3; + // * v9.36 FIX#145: same cap for SELL + if(g_cachedATR > 0 && (sl - currentPrice) > g_cachedATR * 2.0) + sl = currentPrice + g_cachedATR * 2.0; + tp = currentPrice - (sl - currentPrice) * g_workingMinRiskReward; + } + // ML confidence + double mlConf = 0; + if(g_nnTrained && ArraySize(ML_Predictions) > 0) + { + ML_Prediction lastPred = ML_Predictions[ArraySize(ML_Predictions) - 1]; + if((isBullish && lastPred.category == "BULLISH") || + (!isBullish && lastPred.category == "BEARISH")) + { + mlConf = lastPred.confidence; + } + } + // =============================================================== + // [NEW] ENTRY SCORING (NEW) - ΠΡΙΝ το CreateSignal + // =============================================================== + if(InpEnableScoring) + { + // Detect candle pattern + CandlePatternStruct candlePattern = DetectCandlePattern(open, high, low, close, 0); + // Calculate confluence data + ConfluenceScoreStruct confluenceData = CalculateConfluenceData(currentPrice, isBullish); + // Calculate entry score + EntryScoreStruct entryScore = CalculateEntryScore(isBullish, currentPrice, + confluenceData, candlePattern, 0); + // Store for dashboard + g_lastEntryScore = entryScore; + // Check if meets minimum score + if(!MeetsMinimumEntryScore(entryScore)) + { + Print("[WARN] FVG Entry rejected - Score: ", entryScore.totalScore, + "/85 [", entryScore.grade, "] (Min: ", InpMinEntryScore, ")"); + continue; // Skip this FVG, try next + } + // Draw score on chart + if(InpShowScoreOnChart) + { + DrawEntryScore(time[0], currentPrice, entryScore, isBullish); + } + Print("[OK] FVG Entry accepted - Score: ", entryScore.totalScore, + "/85 [", entryScore.grade, "]"); + } + // =============================================================== + + // Add to real execution candidate list + { + double _cSlDist = MathAbs(currentPrice - sl); + double _cTp2 = isBullish ? currentPrice + _cSlDist * g_autoOptParams.tp2_rr + : currentPrice - _cSlDist * g_autoOptParams.tp2_rr; + double _cTp3 = isBullish ? currentPrice + _cSlDist * g_autoOptParams.tp3_rr + : currentPrice - _cSlDist * g_autoOptParams.tp3_rr; + int _cScore = (InpEnableScoring && g_lastEntryScore.totalScore > 0) + ? g_lastEntryScore.totalScore : (int)(quality * 85); + AddCandidate(isBullish, "FVG_ENTRY", TECH_FVG, -1, + currentPrice, sl, tp, _cTp2, _cTp3, _cScore); + } + CreateSignal(time[0], currentPrice, sl, tp, isBullish, "FVG_ENTRY", + confluence, quality, mlConf); + // * v9.34 FIX#131: Removed phantom AddMultiTPEntry. Real entry is created in ExecuteTrade path. + return; + } + } +} +//+------------------------------------------------------------------+ +//| Check Order Block Entry Signal - WITH ENTRY SCORING | +//+------------------------------------------------------------------+ +void CheckOBEntrySignal(const datetime &time[], const double &high[], + const double &low[], const double &close[], + const double &open[], double currentPrice) +{ + for(int i = 0; i < ArraySize(OB_Array); i++) + { + if(OB_Array[i].mitigated || OB_Array[i].status == "EXPIRED") continue; + double tolerance = g_cachedATR * 0.2; + bool inZone = (currentPrice >= OB_Array[i].bottom - tolerance && + currentPrice <= OB_Array[i].top + tolerance); + if(!inZone) continue; + bool isBullish = OB_Array[i].isBullish; + double confluence = 0.45; + if((isBullish && g_isBullishStructure) || (!isBullish && !g_isBullishStructure)) + confluence += 0.2; + if((isBullish && g_currentPDZone == "DISCOUNT") || + (!isBullish && g_currentPDZone == "PREMIUM")) + confluence += 0.15; + if(g_isInKillzone) + confluence += GetKillzoneQualityBonus(g_killzoneDefinitions[g_currentKillzoneIndex].type); + if(OB_Array[i].strength > 1.5) + confluence += 0.1; + if(confluence < g_workingMinConfluence) continue; + double quality = CalculateEntryQuality(currentPrice, isBullish, "OB_ENTRY", confluence); + if(quality < g_workingMinEntryQuality) continue; + double sl, tp; + if(isBullish) + { + sl = OB_Array[i].bottom - g_cachedATR * 0.3; + // * v9.36 FIX#145: OB structural SL cap -- same as FVG fix. + // OB bottom can be 10-20p below price on wide-range bars. + // Cap: sl_dist <= 2.0×ATR. FIX#141 is the backstop. + if(g_cachedATR > 0 && (currentPrice - sl) > g_cachedATR * 2.0) + sl = currentPrice - g_cachedATR * 2.0; + tp = currentPrice + (currentPrice - sl) * g_workingMinRiskReward; + } + else + { + sl = OB_Array[i].top + g_cachedATR * 0.3; + // * v9.36 FIX#145: same cap for OB SELL + if(g_cachedATR > 0 && (sl - currentPrice) > g_cachedATR * 2.0) + sl = currentPrice + g_cachedATR * 2.0; + tp = currentPrice - (sl - currentPrice) * g_workingMinRiskReward; + } + double mlConf = 0; + if(g_nnTrained && ArraySize(ML_Predictions) > 0) + { + ML_Prediction lastPred = ML_Predictions[ArraySize(ML_Predictions) - 1]; + if((isBullish && lastPred.category == "BULLISH") || + (!isBullish && lastPred.category == "BEARISH")) + { + mlConf = lastPred.confidence; + } + } + // =============================================================== + // [NEW] ENTRY SCORING + // =============================================================== + if(InpEnableScoring) + { + CandlePatternStruct candlePattern = DetectCandlePattern(open, high, low, close, 0); + ConfluenceScoreStruct confluenceData = CalculateConfluenceData(currentPrice, isBullish); + // OB Quality score based on strength + int obQualityScore = (int)(OB_Array[i].strength * 30); + EntryScoreStruct entryScore = CalculateEntryScore(isBullish, currentPrice, + confluenceData, candlePattern, obQualityScore); + g_lastEntryScore = entryScore; + if(!MeetsMinimumEntryScore(entryScore)) + { + Print("[WARN] OB Entry rejected - Score: ", entryScore.totalScore, + "/85 [", entryScore.grade, "]"); + continue; + } + if(InpShowScoreOnChart) + DrawEntryScore(time[0], currentPrice, entryScore, isBullish); + } + // =============================================================== + + // Add to real execution candidate list + { + double _cSlDist = MathAbs(currentPrice - sl); + double _cTp2 = isBullish ? currentPrice + _cSlDist * g_autoOptParams.tp2_rr + : currentPrice - _cSlDist * g_autoOptParams.tp2_rr; + double _cTp3 = isBullish ? currentPrice + _cSlDist * g_autoOptParams.tp3_rr + : currentPrice - _cSlDist * g_autoOptParams.tp3_rr; + int _cScore = (InpEnableScoring && g_lastEntryScore.totalScore > 0) + ? g_lastEntryScore.totalScore : (int)(quality * 85); + AddCandidate(isBullish, "OB_ENTRY", TECH_OB, -1, + currentPrice, sl, tp, _cTp2, _cTp3, _cScore); + } + CreateSignal(time[0], currentPrice, sl, tp, isBullish, "OB_ENTRY", + confluence, quality, mlConf); + // [NEW] MULTI-TP ENTRY + // * v9.34 FIX#131: Removed phantom AddMultiTPEntry. Real entry is created in ExecuteTrade path. + return; + } +} +//+------------------------------------------------------------------+ +//| Check Liquidity Grab Signal - WITH ENTRY SCORING | +//+------------------------------------------------------------------+ +void CheckLiquidityGrabSignal(const datetime &time[], const double &high[], + const double &low[], const double &close[], + const double &open[], double currentPrice) +{ + int size = ArraySize(LIQ_Array); + if(size == 0) return; + int checkLimit = MathMin(size, 30); + for(int i = size - 1; i >= size - checkLimit && i >= 0; i--) + { + if(!LIQ_Array[i].swept) continue; + if(LIQ_Array[i].age > 5) continue; + bool isBullish = !LIQ_Array[i].isBSL; + double sweepPrice = LIQ_Array[i].sweepPrice; + double originalPrice = LIQ_Array[i].price; + bool validSetup = false; + if(LIQ_Array[i].isBSL) + { + if(currentPrice < originalPrice && currentPrice > sweepPrice - g_cachedATR) + validSetup = true; + } + else + { + if(currentPrice > originalPrice && currentPrice < sweepPrice + g_cachedATR) + validSetup = true; + } + if(!validSetup) continue; + double confluence = 0.5; + if((isBullish && g_isBullishStructure) || (!isBullish && !g_isBullishStructure)) + confluence += 0.15; + if(g_isInKillzone) + confluence += GetKillzoneQualityBonus(g_killzoneDefinitions[g_currentKillzoneIndex].type); + if(LIQ_Array[i].touches >= 3) + confluence += 0.1; + if(confluence < g_workingMinConfluence) continue; + double quality = CalculateEntryQuality(currentPrice, isBullish, "LIQ_GRAB", confluence); + if(quality < g_workingMinEntryQuality) continue; + double sl, tp; + if(isBullish) + { + sl = LIQ_Array[i].sweepPrice - g_cachedATR * 0.2; + tp = currentPrice + (currentPrice - sl) * g_workingMinRiskReward; + } + else + { + sl = LIQ_Array[i].sweepPrice + g_cachedATR * 0.2; + tp = currentPrice - (sl - currentPrice) * g_workingMinRiskReward; + } + double mlConf = 0; + if(g_nnTrained && ArraySize(ML_Predictions) > 0) + { + ML_Prediction lastPred = ML_Predictions[ArraySize(ML_Predictions) - 1]; + if((isBullish && lastPred.category == "BULLISH") || + (!isBullish && lastPred.category == "BEARISH")) + { + mlConf = lastPred.confidence; + } + } + // =============================================================== + // [NEW] ENTRY SCORING + // =============================================================== + if(InpEnableScoring) + { + CandlePatternStruct candlePattern = DetectCandlePattern(open, high, low, close, 0); + ConfluenceScoreStruct confluenceData = CalculateConfluenceData(currentPrice, isBullish); + EntryScoreStruct entryScore = CalculateEntryScore(isBullish, currentPrice, + confluenceData, candlePattern, 0); + g_lastEntryScore = entryScore; + if(!MeetsMinimumEntryScore(entryScore)) + { + Print("[WARN] LIQ_GRAB Entry rejected - Score: ", entryScore.totalScore, + "/85 [", entryScore.grade, "]"); + continue; + } + if(InpShowScoreOnChart) + DrawEntryScore(time[0], currentPrice, entryScore, isBullish); + } + // =============================================================== + + // Add to real execution candidate list + { + double _cSlDist = MathAbs(currentPrice - sl); + double _cTp2 = isBullish ? currentPrice + _cSlDist * g_autoOptParams.tp2_rr + : currentPrice - _cSlDist * g_autoOptParams.tp2_rr; + double _cTp3 = isBullish ? currentPrice + _cSlDist * g_autoOptParams.tp3_rr + : currentPrice - _cSlDist * g_autoOptParams.tp3_rr; + int _cScore = (InpEnableScoring && g_lastEntryScore.totalScore > 0) + ? g_lastEntryScore.totalScore : (int)(quality * 85); + AddCandidate(isBullish, "LIQ_GRAB", TECH_LIQ_SWEEP, -1, + currentPrice, sl, tp, _cTp2, _cTp3, _cScore); + } + CreateSignal(time[0], currentPrice, sl, tp, isBullish, "LIQ_GRAB", + confluence, quality, mlConf); + // [NEW] MULTI-TP ENTRY + // * v9.34 FIX#131: Removed phantom AddMultiTPEntry. Real entry is created in ExecuteTrade path. + return; + } +} +//+------------------------------------------------------------------+ +//| Check OTE Entry Signal - WITH ENTRY SCORING | +//+------------------------------------------------------------------+ +void CheckOTEEntrySignal(const datetime &time[], const double &high[], + const double &low[], const double &close[], + const double &open[], double currentPrice) +{ + int size = ArraySize(OTE_Array); + if(size == 0) return; + int checkLimit = MathMin(size, 20); + for(int i = size - 1; i >= size - checkLimit && i >= 0; i--) + { + if(!OTE_Array[i].isValid || OTE_Array[i].age > g_workingOTE_MaxAge) continue; + double top = MathMax(OTE_Array[i].level618, OTE_Array[i].level786); + double bottom = MathMin(OTE_Array[i].level618, OTE_Array[i].level786); + if(currentPrice < bottom || currentPrice > top) continue; + bool isBullish = OTE_Array[i].isBullish; + if((isBullish && !g_isBullishStructure) || (!isBullish && g_isBullishStructure)) + continue; + double confluence = 0.5; + double distTo705 = MathAbs(currentPrice - OTE_Array[i].level705); + double zoneSize = MathAbs(OTE_Array[i].level618 - OTE_Array[i].level786); + if(distTo705 < zoneSize * 0.2) + confluence += 0.15; + if(g_isInKillzone) + confluence += GetKillzoneQualityBonus(g_killzoneDefinitions[g_currentKillzoneIndex].type); + int fvgIdx; + if(IsPriceInFVG(currentPrice, fvgIdx, isBullish)) + confluence += 0.15; + if(confluence < g_workingMinConfluence) continue; + double quality = CalculateEntryQuality(currentPrice, isBullish, "OTE_ENTRY", confluence); + if(quality < g_workingMinEntryQuality) continue; + double sl, tp; + if(isBullish) + { + sl = OTE_Array[i].swingLow - g_cachedATR * 0.2; + tp = OTE_Array[i].swingHigh + (OTE_Array[i].swingHigh - OTE_Array[i].swingLow) * 0.618; + } + else + { + sl = OTE_Array[i].swingHigh + g_cachedATR * 0.2; + tp = OTE_Array[i].swingLow - (OTE_Array[i].swingHigh - OTE_Array[i].swingLow) * 0.618; + } + double rr = MathAbs(tp - currentPrice) / MathAbs(currentPrice - sl); + if(rr < g_workingMinRiskReward) + { + if(isBullish) + tp = currentPrice + (currentPrice - sl) * g_workingMinRiskReward; + else + tp = currentPrice - (sl - currentPrice) * g_workingMinRiskReward; + } + double mlConf = 0; + if(g_nnTrained && ArraySize(ML_Predictions) > 0) + { + ML_Prediction lastPred = ML_Predictions[ArraySize(ML_Predictions) - 1]; + if((isBullish && lastPred.category == "BULLISH") || + (!isBullish && lastPred.category == "BEARISH")) + { + mlConf = lastPred.confidence; + } + } + // =============================================================== + // [NEW] ENTRY SCORING + // =============================================================== + if(InpEnableScoring) + { + CandlePatternStruct candlePattern = DetectCandlePattern(open, high, low, close, 0); + ConfluenceScoreStruct confluenceData = CalculateConfluenceData(currentPrice, isBullish); + EntryScoreStruct entryScore = CalculateEntryScore(isBullish, currentPrice, + confluenceData, candlePattern, 0); + g_lastEntryScore = entryScore; + if(!MeetsMinimumEntryScore(entryScore)) + { + Print("[WARN] OTE Entry rejected - Score: ", entryScore.totalScore, + "/85 [", entryScore.grade, "]"); + continue; + } + if(InpShowScoreOnChart) + DrawEntryScore(time[0], currentPrice, entryScore, isBullish); + } + // =============================================================== + + // Add to real execution candidate list + { + double _cSlDist = MathAbs(currentPrice - sl); + double _cTp2 = isBullish ? currentPrice + _cSlDist * g_autoOptParams.tp2_rr + : currentPrice - _cSlDist * g_autoOptParams.tp2_rr; + double _cTp3 = isBullish ? currentPrice + _cSlDist * g_autoOptParams.tp3_rr + : currentPrice - _cSlDist * g_autoOptParams.tp3_rr; + int _cScore = (InpEnableScoring && g_lastEntryScore.totalScore > 0) + ? g_lastEntryScore.totalScore : (int)(quality * 85); + AddCandidate(isBullish, "OTE_ENTRY", TECH_OTE, -1, + currentPrice, sl, tp, _cTp2, _cTp3, _cScore); + } + CreateSignal(time[0], currentPrice, sl, tp, isBullish, "OTE_ENTRY", + confluence, quality, mlConf); + // [NEW] MULTI-TP ENTRY + // * v9.34 FIX#131: Removed phantom AddMultiTPEntry. Real entry is created in ExecuteTrade path. + OTE_Array[i].touched = true; + return; + } +} +//+------------------------------------------------------------------+ +//| Check BOS Retest Signal - WITH ENTRY SCORING | +//+------------------------------------------------------------------+ +void CheckBOSRetestSignal(const datetime &time[], const double &high[], + const double &low[], const double &close[], + const double &open[], double currentPrice) +{ + // * FIX#372 → FIX#372b: per-TF BOS_RETEST override (pair table allow_bos_retest). + // EURUSD H4: allow_bos_retest[3]=0 (enabled) — χρήστης ελέγχει από STRATEGY_BOS_Retest. + if(g_workingAllowBOSRetest == -1) + { + if(g_verboseLog) + PrintFormat("[FIX#372] BOS_RETEST disabled for %s %s (allow_bos_retest=-1)", + _Symbol, EnumToString(_Period)); + return; + } + int size = ArraySize(STRUCT_Array); + if(size == 0) return; + int checkLimit = MathMin(size, 50); + for(int i = size - 1; i >= size - checkLimit && i >= 0; i--) + { + if(!STRUCT_Array[i].broken) continue; + if(STRUCT_Array[i].age > 50) continue; + double tolerance = g_cachedATR * 0.3; + if(MathAbs(currentPrice - STRUCT_Array[i].price) > tolerance) continue; + bool isBullish = !STRUCT_Array[i].isHigh; + // * v9.08 FIX#11: MOMENTUM FILTER -- Don't enter BOS_RETEST against immediate momentum + // v9.04 bug: Used close[bars-1-c] but arrays are ArraySetAsSeries=true (index 0 = latest) + // -> Was checking bars from 400+ candles ago instead of last 3! + // -> Result: 0 momentum blocks in entire backtest despite 27 BOS_RETEST trades + // Fix: Use close[c] directly (c=1,2,3 = last 3 completed candles with as_series=true) + { + int bars = ArraySize(close); + if(bars >= 5) + { + int bearishCount = 0; + int bullishCount = 0; + for(int c = 1; c <= 3; c++) // c=1,2,3 = last 3 completed candles (as_series) + { + if(close[c] < open[c]) bearishCount++; + else if(close[c] > open[c]) bullishCount++; + } + // BUY signal but last 3 candles mostly bearish -> pullback not done + if(isBullish && bearishCount >= 3) // H1: require ALL 3 candles bearish (>= 2 was too strict) + { + if(g_verboseLog) + Print("* v9.04 BOS_RETEST MOMENTUM BLOCK: BUY rejected -- ", bearishCount, "/3 bearish candles"); + continue; + } + // SELL signal but last 3 candles mostly bullish -> rally not done + if(!isBullish && bullishCount >= 3) // H1: require ALL 3 candles bullish + { + if(g_verboseLog) + Print("* v9.04 BOS_RETEST MOMENTUM BLOCK: SELL rejected -- ", bullishCount, "/3 bullish candles"); + continue; + } + } + } + // * v7.4 FIX: Removed hard structure block (was: if(isBullish != g_isBullishStructure) continue;) + // Counter-structure BOS retests CAN be valid reversal signals + // The confirmation cascade scoring handles this via CONF_STRUCTURE points + double confluence = 0.45; + // Reduce confluence for counter-structure (soft penalty instead of hard block) + if(isBullish != g_isBullishStructure) + confluence -= 0.15; + if(g_isInKillzone) + confluence += GetKillzoneQualityBonus(g_killzoneDefinitions[g_currentKillzoneIndex].type); + if(STRUCT_Array[i].swingStrength > 0.7) + confluence += 0.15; + if(confluence < g_workingMinConfluence) continue; + double quality = CalculateEntryQuality(currentPrice, isBullish, "BOS_RETEST", confluence); + if(quality < g_workingMinEntryQuality) continue; + double sl, tp; + if(isBullish) + { + sl = currentPrice - g_cachedATR * g_workingSL_ATRMultiplier; + tp = currentPrice + g_cachedATR * g_workingTP_ATRMultiplier; + } + else + { + sl = currentPrice + g_cachedATR * g_workingSL_ATRMultiplier; + tp = currentPrice - g_cachedATR * g_workingTP_ATRMultiplier; + } + double mlConf = 0; + if(g_nnTrained && ArraySize(ML_Predictions) > 0) + { + ML_Prediction lastPred = ML_Predictions[ArraySize(ML_Predictions) - 1]; + if((isBullish && lastPred.category == "BULLISH") || + (!isBullish && lastPred.category == "BEARISH")) + { + mlConf = lastPred.confidence; + } + } + // =============================================================== + // [NEW] ENTRY SCORING + // =============================================================== + if(InpEnableScoring) + { + CandlePatternStruct candlePattern = DetectCandlePattern(open, high, low, close, 0); + ConfluenceScoreStruct confluenceData = CalculateConfluenceData(currentPrice, isBullish); + EntryScoreStruct entryScore = CalculateEntryScore(isBullish, currentPrice, + confluenceData, candlePattern, 0); + g_lastEntryScore = entryScore; + if(!MeetsMinimumEntryScore(entryScore)) + { + Print("[WARN] BOS_RETEST Entry rejected - Score: ", entryScore.totalScore, + "/85 [", entryScore.grade, "]"); + continue; + } + if(InpShowScoreOnChart) + DrawEntryScore(time[0], currentPrice, entryScore, isBullish); + } + // =============================================================== + + // Add to real execution candidate list + { + double _cSlDist = MathAbs(currentPrice - sl); + double _cTp2 = isBullish ? currentPrice + _cSlDist * g_autoOptParams.tp2_rr + : currentPrice - _cSlDist * g_autoOptParams.tp2_rr; + double _cTp3 = isBullish ? currentPrice + _cSlDist * g_autoOptParams.tp3_rr + : currentPrice - _cSlDist * g_autoOptParams.tp3_rr; + int _cScore = (InpEnableScoring && g_lastEntryScore.totalScore > 0) + ? g_lastEntryScore.totalScore : (int)(quality * 85); + AddCandidate(isBullish, "BOS_RETEST", TECH_BOS_RETEST, -1, + currentPrice, sl, tp, _cTp2, _cTp3, _cScore); + } + CreateSignal(time[0], currentPrice, sl, tp, isBullish, "BOS_RETEST", + confluence, quality, mlConf); + // [NEW] MULTI-TP ENTRY + // * v9.34 FIX#131: Removed phantom AddMultiTPEntry. Real entry is created in ExecuteTrade path. + return; + } +} +//+------------------------------------------------------------------+ +//| 1. ENHANCED DrawSignal - With Quality Stars & Premium Visuals | +//+------------------------------------------------------------------+ +void DrawSignal(SIGNAL_Struct &signal) +{ + string baseName = "ICT_Signal_" + IntegerToString(signal.id); + int arrowCode = signal.isBullish ? 233 : 234; + color arrowColor = signal.isBullish ? SIGNAL_BuyColor : SIGNAL_SellColor; + color zoneColor = signal.isBullish ? clrLime : clrRed; + double safeATR = (g_cachedATR > 0) ? g_cachedATR : g_pipValue * 20; + datetime endTime = signal.time + PeriodSeconds() * g_workingSignalExpiryBars; + double entryQuality = signal.entryQuality; + double riskReward = signal.riskReward; + bool isBullish = signal.isBullish; + // * BUILD QUALITY STARS + string qualityStars = GetQualityStars(entryQuality); + color starsColor = GetQualityColor(entryQuality); + // [TARGET] ENTRY ARROW WITH TOOLTIP + if(!DrawArrows) return; // [v6.42] master switch + string arrowName = baseName + "_Arrow"; + if(ObjectFind(0, arrowName) < 0) + ObjectCreate(0, arrowName, OBJ_ARROW, 0, signal.time, signal.entryPrice); + ObjectSetInteger(0, arrowName, OBJPROP_ARROWCODE, arrowCode); + ObjectSetInteger(0, arrowName, OBJPROP_COLOR, arrowColor); + ObjectSetInteger(0, arrowName, OBJPROP_WIDTH, 3); + // Tooltip + string tooltip = StringFormat("%s %s\n%s\nEntry: %.5f\nSL: %.5f (%.1f pips)\nTP: %.5f (%.1f pips)\nR:R = 1:%.1f\nQuality: %.0f%% %s", + isBullish ? "[GREEN] BUY" : "[RED] SELL", + signal.strategy, qualityStars, + signal.entryPrice, signal.stopLoss, + MathAbs(signal.entryPrice - signal.stopLoss) / g_pipValue, + signal.takeProfit, + MathAbs(signal.takeProfit - signal.entryPrice) / g_pipValue, + riskReward, entryQuality, qualityStars); + ObjectSetString(0, arrowName, OBJPROP_TOOLTIP, tooltip); + if(SIGNAL_ShowLevels) + { + // [PKG] ENTRY ZONE BOX + string entryBoxName = baseName + "_EntryBox"; + double boxTop = signal.entryPrice + safeATR * 0.1; + double boxBottom = signal.entryPrice - safeATR * 0.1; + if(ObjectFind(0, entryBoxName) >= 0) ObjectDelete(0, entryBoxName); + if(ObjectCreate(0, entryBoxName, OBJ_RECTANGLE, 0, signal.time, boxTop, endTime, boxBottom)) + { + ObjectSetInteger(0, entryBoxName, OBJPROP_COLOR, zoneColor); + ObjectSetInteger(0, entryBoxName, OBJPROP_FILL, true); + ObjectSetInteger(0, entryBoxName, OBJPROP_BACK, true); + } + // "Entry Long" / "Entry Short" Label + string entryLabelName = baseName + "_EntryLabel"; + if(ObjectFind(0, entryLabelName) >= 0) ObjectDelete(0, entryLabelName); + if(ObjectCreate(0, entryLabelName, OBJ_TEXT, 0, signal.time, signal.entryPrice)) + { + ObjectSetString(0, entryLabelName, OBJPROP_TEXT, isBullish ? "Entry Long" : "Entry Short"); + ObjectSetInteger(0, entryLabelName, OBJPROP_COLOR, clrWhite); + ObjectSetInteger(0, entryLabelName, OBJPROP_FONTSIZE, 8); + ObjectSetString(0, entryLabelName, OBJPROP_FONT, "Arial Bold"); + } + // [STOP] SL ZONE + LINE + string slBoxName = baseName + "_SLBox"; + if(ObjectFind(0, slBoxName) >= 0) ObjectDelete(0, slBoxName); + if(ObjectCreate(0, slBoxName, OBJ_RECTANGLE, 0, signal.time, signal.stopLoss + safeATR*0.05, endTime, signal.stopLoss - safeATR*0.05)) + { + ObjectSetInteger(0, slBoxName, OBJPROP_COLOR, clrRed); + ObjectSetInteger(0, slBoxName, OBJPROP_FILL, true); + ObjectSetInteger(0, slBoxName, OBJPROP_BACK, true); + } + string slName = baseName + "_SL"; + if(ObjectFind(0, slName) < 0) + ObjectCreate(0, slName, OBJ_TREND, 0, signal.time, signal.stopLoss, endTime, signal.stopLoss); + ObjectSetInteger(0, slName, OBJPROP_COLOR, clrRed); + ObjectSetInteger(0, slName, OBJPROP_STYLE, STYLE_DASH); + ObjectSetInteger(0, slName, OBJPROP_WIDTH, 2); + ObjectSetInteger(0, slName, OBJPROP_RAY_RIGHT, false); + // SL Pips Label + string slLabelName = baseName + "_SLLabel"; + if(ObjectFind(0, slLabelName) >= 0) ObjectDelete(0, slLabelName); + if(ObjectCreate(0, slLabelName, OBJ_TEXT, 0, endTime, signal.stopLoss)) + { + ObjectSetString(0, slLabelName, OBJPROP_TEXT, StringFormat("SL %.1f pips", MathAbs(signal.entryPrice - signal.stopLoss) / g_pipValue)); + ObjectSetInteger(0, slLabelName, OBJPROP_COLOR, clrRed); + ObjectSetInteger(0, slLabelName, OBJPROP_FONTSIZE, 8); + ObjectSetString(0, slLabelName, OBJPROP_FONT, "Arial Bold"); + } + // [TARGET] TP ZONE + TARGET LINE + string tpBoxName = baseName + "_TPBox"; + if(ObjectFind(0, tpBoxName) >= 0) ObjectDelete(0, tpBoxName); + if(ObjectCreate(0, tpBoxName, OBJ_RECTANGLE, 0, signal.time, signal.takeProfit + safeATR*0.05, endTime, signal.takeProfit - safeATR*0.05)) + { + ObjectSetInteger(0, tpBoxName, OBJPROP_COLOR, clrLime); + ObjectSetInteger(0, tpBoxName, OBJPROP_FILL, true); + ObjectSetInteger(0, tpBoxName, OBJPROP_BACK, true); + } + string tpName = baseName + "_TP"; + if(ObjectFind(0, tpName) < 0) + ObjectCreate(0, tpName, OBJ_TREND, 0, signal.time, signal.takeProfit, endTime, signal.takeProfit); + ObjectSetInteger(0, tpName, OBJPROP_COLOR, clrLime); + ObjectSetInteger(0, tpName, OBJPROP_STYLE, STYLE_DASH); + ObjectSetInteger(0, tpName, OBJPROP_WIDTH, 2); + ObjectSetInteger(0, tpName, OBJPROP_RAY_RIGHT, false); + // TARGET Label + string tpLabelName = baseName + "_TPLabel"; + if(ObjectFind(0, tpLabelName) >= 0) ObjectDelete(0, tpLabelName); + if(ObjectCreate(0, tpLabelName, OBJ_TEXT, 0, endTime, signal.takeProfit)) + { + ObjectSetString(0, tpLabelName, OBJPROP_TEXT, StringFormat("TARGET %.1f pips", MathAbs(signal.takeProfit - signal.entryPrice) / g_pipValue)); + ObjectSetInteger(0, tpLabelName, OBJPROP_COLOR, clrLime); + ObjectSetInteger(0, tpLabelName, OBJPROP_FONTSIZE, 8); + ObjectSetString(0, tpLabelName, OBJPROP_FONT, "Arial Bold"); + } + // Entry Line + string entryLineName = baseName + "_Entry"; + if(ObjectFind(0, entryLineName) < 0) + ObjectCreate(0, entryLineName, OBJ_TREND, 0, signal.time, signal.entryPrice, endTime, signal.entryPrice); + ObjectSetInteger(0, entryLineName, OBJPROP_COLOR, arrowColor); + ObjectSetInteger(0, entryLineName, OBJPROP_STYLE, STYLE_DOT); + ObjectSetInteger(0, entryLineName, OBJPROP_RAY_RIGHT, false); + } + // [NOTE] MAIN LABEL WITH STARS + string labelName = baseName + "_Label"; + double labelPrice = isBullish ? signal.entryPrice - safeATR * 0.5 : signal.entryPrice + safeATR * 0.5; + if(ObjectFind(0, labelName) >= 0) ObjectDelete(0, labelName); + if(ObjectCreate(0, labelName, OBJ_TEXT, 0, signal.time, labelPrice)) + { + string labelText = StringFormat("%s %s %s\n%s\nQ:%.0f%% | R:R 1:%.1f", + isBullish ? "[GREEN]" : "[RED]", + isBullish ? "BUY" : "SELL", + qualityStars, signal.strategy, + entryQuality, riskReward); + ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); + ObjectSetInteger(0, labelName, OBJPROP_COLOR, starsColor); + ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 9); + ObjectSetString(0, labelName, OBJPROP_FONT, "Arial Bold"); + ObjectSetInteger(0, labelName, OBJPROP_ANCHOR, isBullish ? ANCHOR_TOP : ANCHOR_BOTTOM); + } +} +//+------------------------------------------------------------------+ +//| Update Active Signals - OPTIMIZED | +//+------------------------------------------------------------------+ +void UpdateActiveSignals(const datetime &time[], const double &high[], + const double &low[], const double &close[]) +{ + int size = ArraySize(SIGNAL_Array); + if(size == 0) return; + double currentHigh = high[0]; + double currentLow = low[0]; + double currentClose = close[0]; + datetime currentTime = time[0]; + // OPTIMIZATION: Track count during loop instead of separate loop at end + int activeCount = 0; + for(int i = size - 1; i >= 0; i--) + { + if(SIGNAL_Array[i].status != "PENDING") + { + continue; + } + // Check expiry + // * v9.16 FIX#48: AutoCleanExpiredSignals gates auto-cleanup (was dead input) + if(AutoCleanExpiredSignals && currentTime >= SIGNAL_Array[i].expiryTime) + { + SIGNAL_Array[i].status = "EXPIRED"; + SIGNAL_Array[i].exitTime = currentTime; + UpdateSignalVisual(i, "EXPIRED"); + if(AlertOnSignalExpiry) + { + Alert(_Symbol, ": Signal ", SIGNAL_Array[i].id, " expired (", SIGNAL_Array[i].strategy, ")"); + } + continue; + } + // Cache frequently used values + bool isBullish = SIGNAL_Array[i].isBullish; + double entryPrice = SIGNAL_Array[i].entryPrice; + double sl = SIGNAL_Array[i].stopLoss; + double tp = SIGNAL_Array[i].takeProfit; + bool signalClosed = false; + // Check for SL/TP hit + if(isBullish) + { + if(currentLow <= sl) + { + // SL Hit + SIGNAL_Array[i].status = "LOSS"; + SIGNAL_Array[i].result = "LOSS"; + SIGNAL_Array[i].exitTime = currentTime; + SIGNAL_Array[i].exitPrice = sl; + SIGNAL_Array[i].pnlPips = (sl - entryPrice) / g_pipValue; + UpdateSignalVisual(i, "LOSS"); + UpdatePerformanceStats(SIGNAL_Array[i], false); + SaveTradeRecord(SIGNAL_Array[i], "BUY", currentTime); + if(Signal_AlertSL) + { + Alert(_Symbol, ": Signal ", SIGNAL_Array[i].id, " hit SL (", + DoubleToString(SIGNAL_Array[i].pnlPips, 1), " pips)"); + } + signalClosed = true; + } + else if(currentHigh >= tp) + { + // TP Hit + SIGNAL_Array[i].status = "WIN"; + SIGNAL_Array[i].result = "WIN"; + SIGNAL_Array[i].exitTime = currentTime; + SIGNAL_Array[i].exitPrice = tp; + SIGNAL_Array[i].pnlPips = (tp - entryPrice) / g_pipValue; + UpdateSignalVisual(i, "WIN"); + UpdatePerformanceStats(SIGNAL_Array[i], true); + SaveTradeRecord(SIGNAL_Array[i], "BUY", currentTime); + // * v9.16 FIX#49: FORCE CLOSE actual EA positions on Signal WIN + // BUG: Signal Tracker uses candle HIGH (catches intra-bar TP touch) + // but MT5 "Open prices only" only checks at bar open -> position stays open + // Result: Signal turns green but position doesn't close! + // FIX: When Signal detects WIN via HIGH/LOW, force-close matching positions + if(EA_Enabled) + { + ForceCloseMatchingPositions(true, entryPrice); + } + if(Signal_AlertTP) + { + Alert(_Symbol, ": Signal ", SIGNAL_Array[i].id, " hit TP! (+", + DoubleToString(SIGNAL_Array[i].pnlPips, 1), " pips)"); + } + signalClosed = true; + } + } + else // Bearish signal + { + if(currentHigh >= sl) + { + // SL Hit + SIGNAL_Array[i].status = "LOSS"; + SIGNAL_Array[i].result = "LOSS"; + SIGNAL_Array[i].exitTime = currentTime; + SIGNAL_Array[i].exitPrice = sl; + SIGNAL_Array[i].pnlPips = (entryPrice - sl) / g_pipValue; + UpdateSignalVisual(i, "LOSS"); + UpdatePerformanceStats(SIGNAL_Array[i], false); + SaveTradeRecord(SIGNAL_Array[i], "SELL", currentTime); + if(Signal_AlertSL) + { + Alert(_Symbol, ": Signal ", SIGNAL_Array[i].id, " hit SL (", + DoubleToString(SIGNAL_Array[i].pnlPips, 1), " pips)"); + } + signalClosed = true; + } + else if(currentLow <= tp) + { + // TP Hit + SIGNAL_Array[i].status = "WIN"; + SIGNAL_Array[i].result = "WIN"; + SIGNAL_Array[i].exitTime = currentTime; + SIGNAL_Array[i].exitPrice = tp; + SIGNAL_Array[i].pnlPips = (entryPrice - tp) / g_pipValue; + UpdateSignalVisual(i, "WIN"); + UpdatePerformanceStats(SIGNAL_Array[i], true); + SaveTradeRecord(SIGNAL_Array[i], "SELL", currentTime); + // * v9.16 FIX#49: FORCE CLOSE (bearish mirror) + if(EA_Enabled) + { + ForceCloseMatchingPositions(false, entryPrice); + } + if(Signal_AlertTP) + { + Alert(_Symbol, ": Signal ", SIGNAL_Array[i].id, " hit TP! (+", + DoubleToString(SIGNAL_Array[i].pnlPips, 1), " pips)"); + } + signalClosed = true; + } + } + if(signalClosed) continue; + // Signal still active - count it + activeCount++; + // Check for approaching entry alert + if(AlertOnApproachingEntry && i < MAX_SIGNAL_ARRAY && !g_signalApproachAlerts[i]) + { + double distanceToEntry = MathAbs(currentClose - entryPrice) / g_pipValue; + if(distanceToEntry <= ApproachingEntryPips) + { + Alert(_Symbol, ": Price approaching signal ", SIGNAL_Array[i].id, + " entry (", DoubleToString(distanceToEntry, 1), " pips away)"); + g_signalApproachAlerts[i] = (datetime)currentTime; + } + } + } + // OPTIMIZATION: No need for separate counting loop + g_activeSignalCount = activeCount; +} +//+------------------------------------------------------------------+ +//| Helper function to save trade record (reduces code duplication) | +//+------------------------------------------------------------------+ +void SaveTradeRecord(SIGNAL_Struct &signal, string direction, datetime exitTime) +{ + TradeRecord trade; + trade.id = signal.id; + trade.entryTime = signal.time; + trade.exitTime = exitTime; + trade.entryPrice = signal.entryPrice; + trade.exitPrice = signal.exitPrice; + trade.stopLoss = signal.stopLoss; + trade.takeProfit = signal.takeProfit; + trade.direction = direction; + trade.strategy = signal.strategy; + trade.quality = signal.entryQuality; + trade.result = signal.result; + trade.pnlPips = signal.pnlPips; + trade.riskReward = signal.riskReward; + trade.killzoneType = signal.killzone; + trade.marketPhase = signal.marketPhase; + SaveTradeToHistory(trade); +} +//+------------------------------------------------------------------+ +//| Detect Breaker Blocks - OPTIMIZED | +//+------------------------------------------------------------------+ +void DetectBreakerBlocks(const datetime &time[], const double &open[], const double &high[], + const double &low[], const double &close[], int limit) +{ + int obSize = ArraySize(OB_Array); + int bbSize = ArraySize(BREAKER_Array); + if(obSize == 0) return; + // OPTIMIZATION: Check from END, limit to last 50 OBs + int checkLimit = MathMin(obSize, 50); + for(int i = obSize - 1; i >= obSize - checkLimit && i >= 0; i--) + { + // Skip if not a breaker block + if(!OB_Array[i].isBreakerBlock || OB_Array[i].status != "BREAKER") + continue; + // OPTIMIZATION: Check duplicates from END, limit to last 30 + bool exists = false; + int bbCheckLimit = MathMin(bbSize, 30); + for(int j = bbSize - 1; j >= bbSize - bbCheckLimit && j >= 0; j--) + { + if(BREAKER_Array[j].time == OB_Array[i].time && + BREAKER_Array[j].top == OB_Array[i].top) + { + exists = true; + // Update existing + BREAKER_Array[j].age = OB_Array[i].age; + BREAKER_Array[j].touches = OB_Array[i].touchCount; + break; + } + } + // Add new breaker block + if(!exists && bbSize < MAX_BREAKER_ARRAY) + { + // OPTIMIZATION: Reserve allocation + ArrayResize(BREAKER_Array, bbSize + 1, 30); + BREAKER_Array[bbSize].id = OB_Array[i].id; + BREAKER_Array[bbSize].time = OB_Array[i].time; + BREAKER_Array[bbSize].high = OB_Array[i].high; + BREAKER_Array[bbSize].low = OB_Array[i].low; + BREAKER_Array[bbSize].top = OB_Array[i].top; + BREAKER_Array[bbSize].bottom = OB_Array[i].bottom; + // Breaker trades in OPPOSITE direction of original OB + BREAKER_Array[bbSize].isBullish = !OB_Array[i].isBullish; + BREAKER_Array[bbSize].active = true; + BREAKER_Array[bbSize].mitigated = false; + BREAKER_Array[bbSize].touches = OB_Array[i].touchCount; + BREAKER_Array[bbSize].age = OB_Array[i].age; + BREAKER_Array[bbSize].strength = OB_Array[i].strength; + bbSize++; // Update local size tracker + } + } + // Update status of existing breaker blocks + double currentLow = low[0]; + double currentHigh = high[0]; + double tolerance = g_cachedATR * 0.1; + int maxAge = g_workingBB_MaxAge; // [v6.42] Use g_workingBB_MaxAge input (was OB_MaxAge * 2) + int writeIdx = 0; + bbSize = ArraySize(BREAKER_Array); // Refresh size + for(int i = 0; i < bbSize; i++) + { + if(!BREAKER_Array[i].active) + { + // Keep inactive but recent entries for a while + if(BREAKER_Array[i].age < g_workingBB_MaxAge) // [v6.42] was OB_MaxAge + { + if(i != writeIdx) + BREAKER_Array[writeIdx] = BREAKER_Array[i]; + writeIdx++; + } + continue; + } + BREAKER_Array[i].age++; + // Expiry check + if(BREAKER_Array[i].age > maxAge) + { + BREAKER_Array[i].active = false; + continue; + } + // Mitigation check + if(BREAKER_Array[i].isBullish) + { + // Bullish breaker mitigated when price drops below bottom + if(currentLow < BREAKER_Array[i].bottom - tolerance) + { + BREAKER_Array[i].mitigated = true; + BREAKER_Array[i].active = false; + } + else if(currentLow <= BREAKER_Array[i].top && currentLow >= BREAKER_Array[i].bottom) + { + BREAKER_Array[i].touches++; + } + } + else + { + // Bearish breaker mitigated when price rises above top + if(currentHigh > BREAKER_Array[i].top + tolerance) + { + BREAKER_Array[i].mitigated = true; + BREAKER_Array[i].active = false; + } + else if(currentHigh >= BREAKER_Array[i].bottom && currentHigh <= BREAKER_Array[i].top) + { + BREAKER_Array[i].touches++; + } + } + // Keep this entry + if(i != writeIdx) + BREAKER_Array[writeIdx] = BREAKER_Array[i]; + writeIdx++; + } + // Compact array if needed + if(writeIdx < bbSize) + { + ArrayResize(BREAKER_Array, writeIdx, 30); + } +} +//+------------------------------------------------------------------+ +//| 2. DRAW BREAKER BLOCKS - ΠΛΗΡΗΣ ΥΛΟΠΟΙΗΣΗ | +//| Αντικαθιστά τη γραμμή ~9814 | +//+------------------------------------------------------------------+ +void DrawBreakerBlocks(const datetime &time[]) +{ + if(!g_breakerDisplayEnabled) return; + for(int i = 0; i < ArraySize(BREAKER_Array); i++) + { + // Διαγραφή αντικειμένων για ανενεργά breaker blocks + if(!BREAKER_Array[i].active) + { + ObjectDelete(0, "ICT_BB_" + IntegerToString(BREAKER_Array[i].id)); + ObjectDelete(0, "ICT_BB_Label_" + IntegerToString(BREAKER_Array[i].id)); + continue; + } + // Καθορισμός χρώματος - Breakers έχουν αντίθετο χρώμα από την κατεύθυνση trade + color bbColor = BREAKER_Array[i].isBullish ? BREAKER_BullColor : BREAKER_BearColor; + // Υπολογισμός χρόνου λήξης ορθογωνίου + datetime endTime = time[0] + PeriodSeconds() * BREAKER_ExtendBars; // [v6.42] was 80 + string objName = "ICT_BB_" + IntegerToString(BREAKER_Array[i].id); + // Δημιουργία/Ενημέρωση ορθογωνίου + if(ObjectFind(0, objName) < 0) + { + ObjectCreate(0, objName, OBJ_RECTANGLE, 0, + BREAKER_Array[i].time, BREAKER_Array[i].top, + endTime, BREAKER_Array[i].bottom); + } + ObjectSetInteger(0, objName, OBJPROP_TIME, 0, BREAKER_Array[i].time); + ObjectSetDouble(0, objName, OBJPROP_PRICE, 0, BREAKER_Array[i].top); + ObjectSetInteger(0, objName, OBJPROP_TIME, 1, endTime); + ObjectSetDouble(0, objName, OBJPROP_PRICE, 1, BREAKER_Array[i].bottom); + ObjectSetInteger(0, objName, OBJPROP_COLOR, bbColor); + ObjectSetInteger(0, objName, OBJPROP_FILL, true); + ObjectSetInteger(0, objName, OBJPROP_BACK, true); + ObjectSetInteger(0, objName, OBJPROP_WIDTH, 2); + ObjectSetInteger(0, objName, OBJPROP_STYLE, STYLE_DASH); + // Label για Breaker Block + string labelName = "ICT_BB_Label_" + IntegerToString(BREAKER_Array[i].id); + if(ObjectFind(0, labelName) < 0) + { + ObjectCreate(0, labelName, OBJ_TEXT, 0, BREAKER_Array[i].time, BREAKER_Array[i].top); + } + string direction = BREAKER_Array[i].isBullish ? "[^]" : "[v]"; + string labelText = "BB " + direction + " S:" + DoubleToString(BREAKER_Array[i].strength, 1); + if(BREAKER_Array[i].touches > 1) + labelText += " T:" + IntegerToString(BREAKER_Array[i].touches); + ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); + ObjectSetInteger(0, labelName, OBJPROP_COLOR, bbColor); + ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 8); + ObjectSetString(0, labelName, OBJPROP_FONT, "Arial Bold"); + ObjectSetDouble(0, labelName, OBJPROP_PRICE, BREAKER_Array[i].top + g_cachedATR * 0.1); + } +} +//+------------------------------------------------------------------+ +//| Detect Mitigation Blocks - OPTIMIZED | +//+------------------------------------------------------------------+ +void DetectMitigationBlocks(const datetime &time[], const double &open[], const double &high[], + const double &low[], const double &close[], int limit) +{ + int fvgSize = ArraySize(FVG_Array); + int obSize = ArraySize(OB_Array); + int mbSize = ArraySize(MITIGATION_Array); + // 1. Detect from mitigated FVGs - OPTIMIZED + if(fvgSize > 0) + { + int fvgCheckLimit = MathMin(fvgSize, 50); + for(int i = fvgSize - 1; i >= fvgSize - fvgCheckLimit && i >= 0; i--) + { + if(FVG_Array[i].status != FVG_STATUS_MITIGATED || FVG_Array[i].filled) + continue; + // Check for duplicate - from END, limit to 30 + bool exists = false; + int mbCheckLimit = MathMin(mbSize, 30); + for(int j = mbSize - 1; j >= mbSize - mbCheckLimit && j >= 0; j--) + { + if(MITIGATION_Array[j].time == FVG_Array[i].time) + { + exists = true; + MITIGATION_Array[j].age++; + break; + } + } + if(!exists && mbSize < MAX_MITIGATION_ARRAY) + { + // OPTIMIZATION: Reserve allocation + ArrayResize(MITIGATION_Array, mbSize + 1, 30); + MITIGATION_Array[mbSize].id = FVG_Array[i].id; + MITIGATION_Array[mbSize].time = FVG_Array[i].time; + MITIGATION_Array[mbSize].high = FVG_Array[i].high; + MITIGATION_Array[mbSize].low = FVG_Array[i].low; + // Calculate zone after mitigation (50% of original FVG) + double midPoint = (FVG_Array[i].high + FVG_Array[i].low) / 2; + if(FVG_Array[i].type == FVG_TYPE_BULLISH) + { + MITIGATION_Array[mbSize].top = FVG_Array[i].high; + MITIGATION_Array[mbSize].bottom = midPoint; + MITIGATION_Array[mbSize].isBullish = true; + } + else + { + MITIGATION_Array[mbSize].top = midPoint; + MITIGATION_Array[mbSize].bottom = FVG_Array[i].low; + MITIGATION_Array[mbSize].isBullish = false; + } + MITIGATION_Array[mbSize].active = true; + MITIGATION_Array[mbSize].mitigated = false; + MITIGATION_Array[mbSize].touches = 0; + MITIGATION_Array[mbSize].age = 0; + MITIGATION_Array[mbSize].strength = FVG_Array[i].quality == FVG_QUALITY_PREMIUM ? 1.5 : + FVG_Array[i].quality == FVG_QUALITY_HIGH ? 1.2 : 1.0; + mbSize++; + } + } + } + // 2. Detect from mitigated Order Blocks - OPTIMIZED + if(obSize > 0) + { + int obCheckLimit = MathMin(obSize, 50); + double pointTolerance = g_point * 10; + for(int i = obSize - 1; i >= obSize - obCheckLimit && i >= 0; i--) + { + if(!OB_Array[i].mitigated || OB_Array[i].isBreakerBlock || OB_Array[i].status != "MITIGATED") + continue; + // Check for duplicate - from END, limit to 30 + bool exists = false; + mbSize = ArraySize(MITIGATION_Array); // Refresh size + int mbCheckLimit = MathMin(mbSize, 30); + for(int j = mbSize - 1; j >= mbSize - mbCheckLimit && j >= 0; j--) + { + if(MITIGATION_Array[j].time == OB_Array[i].time && + MathAbs(MITIGATION_Array[j].top - OB_Array[i].top) < pointTolerance) + { + exists = true; + MITIGATION_Array[j].touches = OB_Array[i].touchCount; + break; + } + } + if(!exists && mbSize < MAX_MITIGATION_ARRAY) + { + ArrayResize(MITIGATION_Array, mbSize + 1, 30); + MITIGATION_Array[mbSize].id = OB_Array[i].id + 10000; + MITIGATION_Array[mbSize].time = OB_Array[i].time; + MITIGATION_Array[mbSize].high = OB_Array[i].high; + MITIGATION_Array[mbSize].low = OB_Array[i].low; + MITIGATION_Array[mbSize].top = OB_Array[i].top; + MITIGATION_Array[mbSize].bottom = OB_Array[i].bottom; + MITIGATION_Array[mbSize].isBullish = OB_Array[i].isBullish; + MITIGATION_Array[mbSize].active = true; + MITIGATION_Array[mbSize].mitigated = false; + MITIGATION_Array[mbSize].touches = OB_Array[i].touchCount; + MITIGATION_Array[mbSize].age = OB_Array[i].age; + MITIGATION_Array[mbSize].strength = OB_Array[i].strength; + mbSize++; + } + } + } + // 3. Update status and cleanup - OPTIMIZED (single pass) + mbSize = ArraySize(MITIGATION_Array); + if(mbSize == 0) return; + double currentLow = low[0]; + double currentHigh = high[0]; + double tolerance = g_cachedATR * 0.05; + int writeIdx = 0; + for(int i = 0; i < mbSize; i++) + { + if(!MITIGATION_Array[i].active) continue; + MITIGATION_Array[i].age++; + // Expiry check + if(MITIGATION_Array[i].age > g_workingMB_MaxAge) // [v6.42] Use g_workingMB_MaxAge (was OB_MaxAge) + { + MITIGATION_Array[i].active = false; + continue; + } + // Full mitigation check + if(MITIGATION_Array[i].isBullish) + { + if(currentLow < MITIGATION_Array[i].bottom - tolerance) + { + MITIGATION_Array[i].mitigated = true; + MITIGATION_Array[i].active = false; + continue; + } + else if(currentLow <= MITIGATION_Array[i].top) + { + MITIGATION_Array[i].touches++; + } + } + else + { + if(currentHigh > MITIGATION_Array[i].top + tolerance) + { + MITIGATION_Array[i].mitigated = true; + MITIGATION_Array[i].active = false; + continue; + } + else if(currentHigh >= MITIGATION_Array[i].bottom) + { + MITIGATION_Array[i].touches++; + } + } + // Keep this entry + if(i != writeIdx) + MITIGATION_Array[writeIdx] = MITIGATION_Array[i]; + writeIdx++; + } + // Compact array if needed + if(writeIdx < mbSize) + { + ArrayResize(MITIGATION_Array, writeIdx, 30); + } +} +//+------------------------------------------------------------------+ +//| 4. DRAW MITIGATION BLOCKS - ΠΛΗΡΗΣ ΥΛΟΠΟΙΗΣΗ | +//| Αντικαθιστά τη γραμμή ~9827 | +//+------------------------------------------------------------------+ +void DrawMitigationBlocks(const datetime &time[]) +{ + if(!g_mitigationDisplayEnabled) return; + for(int i = 0; i < ArraySize(MITIGATION_Array); i++) + { + string objName = "ICT_MB_" + IntegerToString(MITIGATION_Array[i].id); + string labelName = "ICT_MB_Label_" + IntegerToString(MITIGATION_Array[i].id); + // Διαγραφή για ανενεργά blocks + if(!MITIGATION_Array[i].active) + { + ObjectDelete(0, objName); + ObjectDelete(0, labelName); + continue; + } + // Χρώμα βάσει κατεύθυνσης + color mbColor = MITIGATION_Array[i].isBullish ? MITIGATION_BullColor : MITIGATION_BearColor; + // Χρόνος λήξης + datetime endTime = time[0] + PeriodSeconds() * MITIGATION_ExtendBars; // [v6.42] was 60 + // Δημιουργία/Ενημέρωση ορθογωνίου + if(ObjectFind(0, objName) < 0) + { + ObjectCreate(0, objName, OBJ_RECTANGLE, 0, + MITIGATION_Array[i].time, MITIGATION_Array[i].top, + endTime, MITIGATION_Array[i].bottom); + } + ObjectSetInteger(0, objName, OBJPROP_TIME, 0, MITIGATION_Array[i].time); + ObjectSetDouble(0, objName, OBJPROP_PRICE, 0, MITIGATION_Array[i].top); + ObjectSetInteger(0, objName, OBJPROP_TIME, 1, endTime); + ObjectSetDouble(0, objName, OBJPROP_PRICE, 1, MITIGATION_Array[i].bottom); + ObjectSetInteger(0, objName, OBJPROP_COLOR, mbColor); + ObjectSetInteger(0, objName, OBJPROP_FILL, true); + ObjectSetInteger(0, objName, OBJPROP_BACK, true); + ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1); + ObjectSetInteger(0, objName, OBJPROP_STYLE, STYLE_DOT); + // Label + if(ObjectFind(0, labelName) < 0) + { + ObjectCreate(0, labelName, OBJ_TEXT, 0, + MITIGATION_Array[i].time, MITIGATION_Array[i].top); + } + string direction = MITIGATION_Array[i].isBullish ? "[^]" : "[v]"; + string labelText = "MB " + direction; + if(MITIGATION_Array[i].touches > 0) + labelText += " T:" + IntegerToString(MITIGATION_Array[i].touches); + ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); + ObjectSetInteger(0, labelName, OBJPROP_COLOR, mbColor); + ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 7); + ObjectSetDouble(0, labelName, OBJPROP_PRICE, MITIGATION_Array[i].top + g_cachedATR * 0.05); + } +} +//+------------------------------------------------------------------+ +//| Check Mitigation Block Signal - OPTIMIZED | +//+------------------------------------------------------------------+ +void CheckMitigationBlockSignal(const datetime &time[], const double &high[], + const double &low[], const double &close[], double currentPrice) +{ + int size = ArraySize(MITIGATION_Array); + if(size == 0) return; + // OPTIMIZATION: Check from END, limit to last 30 + int checkLimit = MathMin(size, 30); + // Cache frequently used values + double tolerance = g_cachedATR * 0.15; + double atr = g_cachedATR; + for(int i = size - 1; i >= size - checkLimit && i >= 0; i--) + { + if(!MITIGATION_Array[i].active) continue; + // Check if price is in mitigation block zone + bool inZone = (currentPrice >= MITIGATION_Array[i].bottom - tolerance && + currentPrice <= MITIGATION_Array[i].top + tolerance); + if(!inZone) continue; + bool isBullish = MITIGATION_Array[i].isBullish; + // * v7.4 FIX: Removed hard structure block -- cascade scoring handles direction + // Was: if((isBullish && !g_isBullishStructure) || (!isBullish && g_isBullishStructure)) continue; + // Premium/Discount zone check + bool pdAligned = (isBullish && g_currentPDZone == "DISCOUNT") || + (!isBullish && g_currentPDZone == "PREMIUM"); + // Calculate confluence + double confluence = 0.35; // Base confluence for MB + // Bonus for touches (multiple touches = stronger zone) + int touches = MITIGATION_Array[i].touches; + if(touches >= 2) confluence += 0.1; + if(touches >= 3) confluence += 0.1; + // Bonus for PD zone alignment + if(pdAligned) confluence += 0.15; + // Bonus for strength + if(MITIGATION_Array[i].strength > 1.2) confluence += 0.1; + // Killzone bonus + if(g_isInKillzone && g_currentKillzoneIndex >= 0) + { + confluence += GetKillzoneQualityBonus(g_killzoneDefinitions[g_currentKillzoneIndex].type); + } + // Check FVG confluence + int fvgIdx; + if(IsPriceInFVG(currentPrice, fvgIdx, isBullish)) + { + confluence += 0.15; + } + // Minimum confluence check + if(confluence < g_workingMinConfluence) continue; + // Calculate entry quality + double quality = CalculateEntryQuality(currentPrice, isBullish, "MB_ENTRY", confluence); + if(quality < g_workingMinEntryQuality) continue; + // Calculate SL/TP + double sl, tp, risk; + if(isBullish) + { + // SL below the bottom of mitigation block + sl = MITIGATION_Array[i].bottom - atr * 0.25; + risk = currentPrice - sl; + if(risk <= 0) continue; // [OK] FIX: Guard against division by zero + tp = currentPrice + risk * g_workingMinRiskReward; + // Alternative: use next resistance level (optimized - limit search) + int liqSize = ArraySize(LIQ_Array); + int liqCheckLimit = MathMin(liqSize, 20); + for(int j = liqSize - 1; j >= liqSize - liqCheckLimit && j >= 0; j--) + { + if(!LIQ_Array[j].isBSL && !LIQ_Array[j].swept && + LIQ_Array[j].price > currentPrice) + { + double potentialTP = LIQ_Array[j].price; + double potentialRR = (potentialTP - currentPrice) / risk; + if(potentialRR >= g_workingMinRiskReward && potentialRR <= g_workingMinRiskReward * 2) + { + tp = potentialTP; + break; + } + } + } + // Validate + if(sl >= currentPrice || tp <= currentPrice) continue; + } + else + { + // SL above the top of mitigation block + sl = MITIGATION_Array[i].top + atr * 0.25; + risk = sl - currentPrice; + if(risk <= 0) continue; // [OK] FIX: Guard against division by zero + tp = currentPrice - risk * g_workingMinRiskReward; + // Alternative: use next support level (optimized - limit search) + int liqSize = ArraySize(LIQ_Array); + int liqCheckLimit = MathMin(liqSize, 20); + for(int j = liqSize - 1; j >= liqSize - liqCheckLimit && j >= 0; j--) + { + if(LIQ_Array[j].isBSL && !LIQ_Array[j].swept && + LIQ_Array[j].price < currentPrice) + { + double potentialTP = LIQ_Array[j].price; + double potentialRR = (currentPrice - potentialTP) / risk; + if(potentialRR >= g_workingMinRiskReward && potentialRR <= g_workingMinRiskReward * 2) + { + tp = potentialTP; + break; + } + } + } + // Validate + if(sl <= currentPrice || tp >= currentPrice) continue; + } + // Calculate actual R:R + double actualRR; + if(isBullish) + actualRR = (tp - currentPrice) / (currentPrice - sl); + else + actualRR = (currentPrice - tp) / (sl - currentPrice); + if(actualRR < g_workingMinRiskReward) continue; + // Create signal + + // Add to real execution candidate list + { + double _cSlDist = MathAbs(currentPrice - sl); + double _cTp2 = isBullish ? currentPrice + _cSlDist * g_autoOptParams.tp2_rr + : currentPrice - _cSlDist * g_autoOptParams.tp2_rr; + double _cTp3 = isBullish ? currentPrice + _cSlDist * g_autoOptParams.tp3_rr + : currentPrice - _cSlDist * g_autoOptParams.tp3_rr; + int _cScore = (InpEnableScoring && g_lastEntryScore.totalScore > 0) + ? g_lastEntryScore.totalScore : (int)(quality * 85); + AddCandidate(isBullish, "MB_ENTRY", TECH_BREAKER, -1, + currentPrice, sl, tp, _cTp2, _cTp3, _cScore); + } + CreateSignal(time[0], currentPrice, sl, tp, isBullish, "MB_ENTRY", + confluence, quality, MITIGATION_Array[i].id); + // Mark MB as used + MITIGATION_Array[i].touches++; + return; // One signal at a time + } +} +//+------------------------------------------------------------------+ +//| Check Breaker Block Signal - OPTIMIZED | +//+------------------------------------------------------------------+ +void CheckBreakerBlockSignal(const datetime &time[], const double &high[], + const double &low[], const double &close[], double currentPrice) +{ + int size = ArraySize(OB_Array); + if(size == 0) return; + // OPTIMIZATION: Check from END, limit to last 40 + int checkLimit = MathMin(size, 40); + // Cache frequently used values + double tolerance = g_cachedATR * 0.2; + double slMultiplier = g_cachedATR * 0.3; + for(int i = size - 1; i >= size - checkLimit && i >= 0; i--) + { + if(!OB_Array[i].isBreakerBlock) continue; + // Check if price is in breaker block zone + if(currentPrice < OB_Array[i].bottom - tolerance || + currentPrice > OB_Array[i].top + tolerance) + continue; + // Breaker blocks trade in opposite direction of original OB + bool isBullish = !OB_Array[i].isBullish; + // Structure alignment check + if((isBullish && !g_isBullishStructure) || (!isBullish && g_isBullishStructure)) + continue; + // Calculate confluence + double confluence = 0.5; // High confluence for breakers + // Killzone bonus + if(g_isInKillzone && g_currentKillzoneIndex >= 0) + { + confluence += GetKillzoneQualityBonus(g_killzoneDefinitions[g_currentKillzoneIndex].type); + } + // Minimum confluence check + if(confluence < g_workingMinConfluence) continue; + // Calculate entry quality + double quality = CalculateEntryQuality(currentPrice, isBullish, "BB_ENTRY", confluence); + if(quality < g_workingMinEntryQuality) continue; + // Calculate SL/TP + double sl, tp; + if(isBullish) + { + sl = OB_Array[i].bottom - slMultiplier; + tp = currentPrice + (currentPrice - sl) * g_workingMinRiskReward; + } + else + { + sl = OB_Array[i].top + slMultiplier; + tp = currentPrice - (sl - currentPrice) * g_workingMinRiskReward; + } + // Create signal + + // Add to real execution candidate list + { + double _cSlDist = MathAbs(currentPrice - sl); + double _cTp2 = isBullish ? currentPrice + _cSlDist * g_autoOptParams.tp2_rr + : currentPrice - _cSlDist * g_autoOptParams.tp2_rr; + double _cTp3 = isBullish ? currentPrice + _cSlDist * g_autoOptParams.tp3_rr + : currentPrice - _cSlDist * g_autoOptParams.tp3_rr; + int _cScore = (InpEnableScoring && g_lastEntryScore.totalScore > 0) + ? g_lastEntryScore.totalScore : (int)(quality * 85); + AddCandidate(isBullish, "BB_ENTRY", TECH_BREAKER, -1, + currentPrice, sl, tp, _cTp2, _cTp3, _cScore); + } + CreateSignal(time[0], currentPrice, sl, tp, isBullish, "BB_ENTRY", + confluence, quality, 0); + return; // One signal at a time + } +} +//+------------------------------------------------------------------+ +//| Detect Order Flow Imbalance - OPTIMIZED | +//+------------------------------------------------------------------+ +void DetectOrderFlowImbalance(const datetime &time[], const double &open[], const double &high[], + const double &low[], const double &close[], const long &volume[], int limit) +{ + int ofiSize = ArraySize(OFI_Array); + // Early exit if array is full + if(ofiSize >= MAX_OFI_ARRAY) return; + int maxBars = MathMin(limit, 50); + if(maxBars < 21) return; // Need at least 21 bars for avgVol calculation + // OPTIMIZATION: Pre-calculate average volume for the period (once, not per bar) + long totalVol = 0; + for(int v = 1; v <= 20; v++) + { + totalVol += volume[v]; + } + double baseAvgVol = (double)totalVol / 20.0; + double volThreshold = baseAvgVol * 1.5; + for(int i = 0; i < maxBars - 20; i++) + { + // Early exit if array becomes full during loop + if(ofiSize >= MAX_OFI_ARRAY) break; + double range = high[i] - low[i]; + if(range == 0) continue; + double body = MathAbs(close[i] - open[i]); + double bodyRatio = body / range; + // Strong imbalance if body > 70% of range + if(bodyRatio <= 0.7) continue; + // OPTIMIZATION: Rolling average volume calculation + // Instead of recalculating 20 bars each time, use sliding window + if(i > 0) + { + // Adjust average: remove bar that's now out of window, add new bar + totalVol = totalVol - volume[i] + volume[i + 20]; + volThreshold = ((double)totalVol / 20.0) * 1.5; + } + // Volume confirmation + if(volume[i] <= (long)volThreshold) continue; + // Check for duplicate - from END, limit to last 20 + bool exists = false; + int checkLimit = MathMin(ofiSize, 20); + for(int j = ofiSize - 1; j >= ofiSize - checkLimit && j >= 0; j--) + { + if(OFI_Array[j].time == time[i]) + { + exists = true; + break; + } + } + if(exists) continue; + // Strong OFI detected - add to array + // OPTIMIZATION: Reserve allocation + ArrayResize(OFI_Array, ofiSize + 1, 50); + OFI_Array[ofiSize].id = ofiSize + 1; + OFI_Array[ofiSize].time = time[i]; + OFI_Array[ofiSize].price = close[i]; + OFI_Array[ofiSize].isBullish = (close[i] > open[i]); + OFI_Array[ofiSize].active = true; + OFI_Array[ofiSize].strength = bodyRatio; + OFI_Array[ofiSize].volume = (double)volume[i]; + ofiSize++; + } +} +void CalculateVolumeProfile(const datetime &time[], const double &high[], const double &low[], + const double &close[], const long &volume[], int limit) +{ + // Simplified volume profile + int bars = MathMin(limit, (g_workingVP_Period > 0 ? g_workingVP_Period : VP_Period)); + if(bars < 10) return; + // Find range + double rangeHigh = high[0]; + double rangeLow = low[0]; + for(int i = 0; i < bars; i++) + { + if(high[i] > rangeHigh) rangeHigh = high[i]; + if(low[i] < rangeLow) rangeLow = low[i]; + } + double step = (rangeHigh - rangeLow) / VP_Rows; + if(step <= 0) return; + // Reset VP levels + ArrayResize(VP_Levels, VP_Rows); + // Calculate volume at each price level + for(int level = 0; level < VP_Rows; level++) + { + double levelLow = rangeLow + level * step; + double levelHigh = levelLow + step; + long totalVol = 0; + for(int i = 0; i < bars; i++) + { + // Check if bar touches this level + if(high[i] >= levelLow && low[i] <= levelHigh) + { + totalVol += volume[i]; + } + } + VP_Levels[level].price = (levelLow + levelHigh) / 2; + VP_Levels[level].volume = (double)totalVol; + VP_Levels[level].type = "VOLUME"; + VP_Levels[level].strength = 0; + } + // Find POC (Point of Control) - highest volume level + double maxVol = 0; + int pocIndex = 0; + for(int i = 0; i < VP_Rows; i++) + { + if(VP_Levels[i].volume > maxVol) + { + maxVol = VP_Levels[i].volume; + pocIndex = i; + } + } + // Normalize and set types + for(int i = 0; i < VP_Rows; i++) + { + VP_Levels[i].strength = (maxVol > 0) ? VP_Levels[i].volume / maxVol : 0; + if(i == pocIndex) + VP_Levels[i].type = "POC"; + else if(VP_Levels[i].strength > 0.7) + VP_Levels[i].type = "HVN"; // High Volume Node + else if(VP_Levels[i].strength < 0.3) + VP_Levels[i].type = "LVN"; // Low Volume Node + } +} +void DrawVolumeProfile(const datetime &time[]) +{ + if(ArraySize(VP_Levels) == 0) return; + datetime startTime = time[(g_workingVP_Period > 0 ? g_workingVP_Period : VP_Period)]; + datetime endTime = time[0]; + for(int i = 0; i < ArraySize(VP_Levels); i++) + { + string objName = "ICT_VP_" + IntegerToString(i); + // Draw as horizontal histogram bars + double barLength = (endTime - startTime) * VP_Levels[i].strength * 0.3; + if(ObjectFind(0, objName) < 0) + { + ObjectCreate(0, objName, OBJ_RECTANGLE, 0, + startTime, VP_Levels[i].price - g_pipValue, + startTime + (datetime)barLength, VP_Levels[i].price + g_pipValue); + } + color vpColor; + if(VP_Levels[i].type == "POC") + vpColor = clrGold; + else if(VP_Levels[i].type == "HVN") + vpColor = clrDodgerBlue; + else + vpColor = clrGray; + ObjectSetInteger(0, objName, OBJPROP_COLOR, vpColor); + ObjectSetInteger(0, objName, OBJPROP_FILL, true); + ObjectSetInteger(0, objName, OBJPROP_BACK, true); + } +} +//+------------------------------------------------------------------+ +//| Detect Market Maker Phase - OPTIMIZED | +//+------------------------------------------------------------------+ +void DetectMarketMakerPhase(const datetime &time[], const double &open[], const double &high[], + const double &low[], const double &close[], const long &volume[], int limit) +{ + int lookback = MathMin(limit, (g_workingMM_Lookback > 0 ? g_workingMM_Lookback : MM_LookbackPeriod)); + if(lookback < 10) return; + int mmSize = ArraySize(MM_Phases); + // Early exit if array is full + if(mmSize >= MAX_MM_ARRAY) + { + // Compact array - keep only recent half + int keepCount = MAX_MM_ARRAY / 2; + for(int i = 0; i < keepCount; i++) + { + MM_Phases[i] = MM_Phases[mmSize - keepCount + i]; + } + ArrayResize(MM_Phases, keepCount, 20); + mmSize = keepCount; + } + // OPTIMIZATION: Calculate range in single pass + double rangeHigh = high[0]; + double rangeLow = low[0]; + double recentVolTotal = 0; + double olderVolTotal = 0; + int halfLookback = lookback / 2; + for(int i = 0; i < lookback; i++) + { + if(high[i] > rangeHigh) rangeHigh = high[i]; + if(low[i] < rangeLow) rangeLow = low[i]; + // Calculate volatility in same loop + double barRange = high[i] - low[i]; + if(i < halfLookback) + recentVolTotal += barRange; + else + olderVolTotal += barRange; + } + double range = rangeHigh - rangeLow; + if(range <= 0) return; + // Calculate metrics + double currentPrice = close[0]; + double pricePosition = (currentPrice - rangeLow) / range; + double startPrice = close[lookback - 1]; + double priceChange = (startPrice > 0) ? (currentPrice - startPrice) / startPrice : 0; + // Calculate volatility ratio + double recentVol = recentVolTotal / halfLookback; + double olderVol = olderVolTotal / (lookback - halfLookback); + double volRatio = (olderVol > 0) ? recentVol / olderVol : 1.0; + // Determine phase + string phase = PHASE_NONE; + double confidence = 0; + // OPTIMIZATION: Use else-if chain for mutually exclusive conditions + if(MathAbs(priceChange) < 0.02 && volRatio < 0.8) + { + // Low change, decreasing volatility = Accumulation or Distribution + if(pricePosition < 0.4) + { + phase = PHASE_ACCUMULATION; + confidence = 0.6 + (0.4 - pricePosition); + } + else if(pricePosition > 0.6) + { + phase = PHASE_DISTRIBUTION; + confidence = 0.6 + (pricePosition - 0.6); + } + } + else if(priceChange > 0.02) + { + phase = PHASE_MARKUP; + confidence = MathMin(1.0, priceChange * 10); + } + else if(priceChange < -0.02) + { + phase = PHASE_MARKDOWN; + confidence = MathMin(1.0, MathAbs(priceChange) * 10); + } + // Update global phase + g_currentPhase = phase; + // Skip storing if same as last phase (reduce redundant entries) + if(mmSize > 0 && MM_Phases[mmSize - 1].phase == phase && + MM_Phases[mmSize - 1].confidence == confidence) + { + // Just update time of existing entry + MM_Phases[mmSize - 1].time = time[0]; + MM_Phases[mmSize - 1].priceLevel = currentPrice; + return; + } + // Store new phase - OPTIMIZATION: Reserve allocation + ArrayResize(MM_Phases, mmSize + 1, 20); + MM_Phases[mmSize].time = time[0]; + MM_Phases[mmSize].phase = phase; + MM_Phases[mmSize].priceLevel = currentPrice; + MM_Phases[mmSize].confidence = confidence; + MM_Phases[mmSize].isActive = true; +} +//+------------------------------------------------------------------+ +//| Draw Market Maker Phases | +//+------------------------------------------------------------------+ +void DrawMarketMakerPhases(const datetime &time[]) +{ + if(ArraySize(MM_Phases) == 0) return; + int size = ArraySize(MM_Phases); + int drawLimit = MathMin(size, 5); + for(int i = size - 1; i >= size - drawLimit && i >= 0; i--) + { + string objName = "ICT_MM_Phase_" + IntegerToString(i); + if(ObjectFind(0, objName) < 0) + { + ObjectCreate(0, objName, OBJ_TEXT, 0, MM_Phases[i].time, MM_Phases[i].priceLevel); + } + color phaseColor; + if(MM_Phases[i].phase == PHASE_ACCUMULATION) + phaseColor = clrLime; + else if(MM_Phases[i].phase == PHASE_MARKUP) + phaseColor = clrDodgerBlue; + else if(MM_Phases[i].phase == PHASE_DISTRIBUTION) + phaseColor = clrOrange; + else if(MM_Phases[i].phase == PHASE_MARKDOWN) + phaseColor = clrRed; + else + phaseColor = clrGray; + string labelText = MM_Phases[i].phase; + if(MM_ShowConfidence) + labelText += " " + DoubleToString(MM_Phases[i].confidence * 100, 0) + "%"; + ObjectSetString(0, objName, OBJPROP_TEXT, labelText); + ObjectSetInteger(0, objName, OBJPROP_COLOR, phaseColor); + ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, 9); + } +} +void CheckMarketMakerSignal(const datetime &time[], const double &high[], const double &low[], + const double &close[], const long &volume[], double currentPrice) +{ + // Generate signals based on market maker phases + if(g_currentPhase == PHASE_ACCUMULATION) + { + // Look for bullish entry at discount + if(g_currentPDZone == "DISCOUNT") + { + double confluence = 0.45; + confluence += 0.15; // Phase bonus + if(g_isInKillzone) + confluence += GetKillzoneQualityBonus(g_killzoneDefinitions[g_currentKillzoneIndex].type); + // Check for FVG or OB confluence + int fvgIdx, obIdx; + if(IsPriceInFVG(currentPrice, fvgIdx, true)) confluence += 0.1; + if(IsPriceNearOB(currentPrice, obIdx, true)) confluence += 0.1; + if(confluence >= g_workingMinConfluence) + { + double quality = CalculateEntryQuality(currentPrice, true, "MM_MODEL", confluence); + if(quality >= g_workingMinEntryQuality) + { + double sl = currentPrice - g_cachedATR * g_workingSL_ATRMultiplier; + double tp = currentPrice + g_cachedATR * g_workingTP_ATRMultiplier; + + { + double _cSlDist = MathAbs(currentPrice - sl); + double _cTp2 = true ? currentPrice + _cSlDist * g_autoOptParams.tp2_rr + : currentPrice - _cSlDist * g_autoOptParams.tp2_rr; + double _cTp3 = true ? currentPrice + _cSlDist * g_autoOptParams.tp3_rr + : currentPrice - _cSlDist * g_autoOptParams.tp3_rr; + int _cScore = (InpEnableScoring && g_lastEntryScore.totalScore > 0) + ? g_lastEntryScore.totalScore : (int)(quality * 85); + AddCandidate(true, "MM_MODEL", TECH_JUDAS, -1, + currentPrice, sl, tp, _cTp2, _cTp3, _cScore); + } + CreateSignal(time[0], currentPrice, sl, tp, true, "MM_MODEL", + confluence, quality, 0); + } + } + } + } + else if(g_currentPhase == PHASE_DISTRIBUTION) + { + // Look for bearish entry at premium + if(g_currentPDZone == "PREMIUM") + { + double confluence = 0.45; + confluence += 0.15; + if(g_isInKillzone) + confluence += GetKillzoneQualityBonus(g_killzoneDefinitions[g_currentKillzoneIndex].type); + int fvgIdx, obIdx; + if(IsPriceInFVG(currentPrice, fvgIdx, false)) confluence += 0.1; + if(IsPriceNearOB(currentPrice, obIdx, false)) confluence += 0.1; + if(confluence >= g_workingMinConfluence) + { + double quality = CalculateEntryQuality(currentPrice, false, "MM_MODEL", confluence); + if(quality >= g_workingMinEntryQuality) + { + double sl = currentPrice + g_cachedATR * g_workingSL_ATRMultiplier; + double tp = currentPrice - g_cachedATR * g_workingTP_ATRMultiplier; + + { + double _cSlDist = MathAbs(currentPrice - sl); + double _cTp2 = false ? currentPrice + _cSlDist * g_autoOptParams.tp2_rr + : currentPrice - _cSlDist * g_autoOptParams.tp2_rr; + double _cTp3 = false ? currentPrice + _cSlDist * g_autoOptParams.tp3_rr + : currentPrice - _cSlDist * g_autoOptParams.tp3_rr; + int _cScore = (InpEnableScoring && g_lastEntryScore.totalScore > 0) + ? g_lastEntryScore.totalScore : (int)(quality * 85); + AddCandidate(false, "MM_MODEL", TECH_JUDAS, -1, + currentPrice, sl, tp, _cTp2, _cTp3, _cScore); + } + CreateSignal(time[0], currentPrice, sl, tp, false, "MM_MODEL", + confluence, quality, 0); + } + } + } + } +} +void CheckMultiConfluenceSignals(const datetime &time[], const double &open[], const double &high[], + const double &low[], const double &close[], const long &volume[], + double currentPrice) +{ + // Look for entries with multiple confluences + int confluenceCount = 0; + double totalConfluence = 0; + bool determinedBullish = g_isBullishStructure; + // Check FVG + int fvgIdx; + if(IsPriceInFVG(currentPrice, fvgIdx, determinedBullish)) + { + confluenceCount++; + totalConfluence += 0.2; + } + // Check OB + int obIdx; + if(IsPriceNearOB(currentPrice, obIdx, determinedBullish)) + { + confluenceCount++; + totalConfluence += 0.2; + } + // Check OTE + int oteIdx; + if(IsPriceInOTE(currentPrice, oteIdx)) + { + if(OTE_Array[oteIdx].isBullish == determinedBullish) + { + confluenceCount++; + totalConfluence += 0.2; + } + } + // Check PD zone + if((determinedBullish && g_currentPDZone == "DISCOUNT") || + (!determinedBullish && g_currentPDZone == "PREMIUM")) + { + confluenceCount++; + totalConfluence += 0.15; + } + // Check Killzone + if(g_isInKillzone) + { + confluenceCount++; + totalConfluence += GetKillzoneQualityBonus(g_killzoneDefinitions[g_currentKillzoneIndex].type); + } + // Check structure + if(determinedBullish == g_isBullishStructure) + { + totalConfluence += 0.15; + } + // Need at least 3 confluences for multi-confluence signal + if(confluenceCount >= 3 && totalConfluence >= g_workingMinConfluence) + { + double quality = CalculateEntryQuality(currentPrice, determinedBullish, "MULTI_CONFLUENCE", totalConfluence); + if(quality >= g_workingMinEntryQuality) + { + double sl, tp; + if(determinedBullish) + { + sl = currentPrice - g_cachedATR * g_workingSL_ATRMultiplier; + tp = currentPrice + g_cachedATR * g_workingTP_ATRMultiplier; + } + else + { + sl = currentPrice + g_cachedATR * g_workingSL_ATRMultiplier; + tp = currentPrice - g_cachedATR * g_workingTP_ATRMultiplier; + } + double mlConf = 0; + if(g_nnTrained && ArraySize(ML_Predictions) > 0) + { + ML_Prediction lastPred = ML_Predictions[ArraySize(ML_Predictions) - 1]; + if((determinedBullish && lastPred.category == "BULLISH") || + (!determinedBullish && lastPred.category == "BEARISH")) + { + mlConf = lastPred.confidence; + } + } + // Add to real execution candidate list + { + double _cSlDist = MathAbs(currentPrice - sl); + double _cTp2 = determinedBullish ? currentPrice + _cSlDist * g_autoOptParams.tp2_rr + : currentPrice - _cSlDist * g_autoOptParams.tp2_rr; + double _cTp3 = determinedBullish ? currentPrice + _cSlDist * g_autoOptParams.tp3_rr + : currentPrice - _cSlDist * g_autoOptParams.tp3_rr; + int _cScore = (InpEnableScoring && g_lastEntryScore.totalScore > 0) + ? g_lastEntryScore.totalScore : (int)(quality * 85); + AddCandidate(determinedBullish, "MULTI_CONFLUENCE", TECH_FVG, -1, + currentPrice, sl, tp, _cTp2, _cTp3, _cScore); + } + CreateSignal(time[0], currentPrice, sl, tp, determinedBullish, "MULTI_CONFLUENCE", + totalConfluence, quality, mlConf); + } + } +} +//+------------------------------------------------------------------+ +//| Initialize Risk Management System | +//+------------------------------------------------------------------+ +void InitializeRiskManagement() +{ + RISK_Data.currentRisk = 0; + RISK_Data.dailyPL = 0; + RISK_Data.tradesCount = 0; + RISK_Data.winRate = 0; + RISK_Data.maxDrawdown = 0; + RISK_Data.lastResetTime = TimeCurrent(); + RISK_Data.equityHigh = AccountInfoDouble(ACCOUNT_BALANCE); + RISK_Data.currentDrawdown = 0; + RISK_Data.dailyLimitReached = false; + RISK_Data.maxTradesReached = false; + Print("[OK] Risk Management initialized"); + PrintFormat(" Initial Equity: $%.2f", RISK_Data.equityHigh); +} +//+------------------------------------------------------------------+ +//| Update Risk Metrics | +//+------------------------------------------------------------------+ +void UpdateRiskMetrics() +{ + double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY); + // Update equity high + if(currentEquity > RISK_Data.equityHigh) + { + RISK_Data.equityHigh = currentEquity; + } + // Calculate drawdown + if(RISK_Data.equityHigh > 0) + { + RISK_Data.currentDrawdown = (RISK_Data.equityHigh - currentEquity) / + RISK_Data.equityHigh * 100.0; + // Update max drawdown + if(RISK_Data.currentDrawdown > RISK_Data.maxDrawdown) + { + RISK_Data.maxDrawdown = RISK_Data.currentDrawdown; + } + } + // Check daily limits + double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE); + double dailyLossLimit = accountBalance * DailyLossLimit / 100.0; + if(RISK_Data.dailyPL < -dailyLossLimit) + { + RISK_Data.dailyLimitReached = true; + if(g_verboseLog) + { + PrintFormat("[WARN] Daily loss limit reached: %.2f / %.2f", + RISK_Data.dailyPL, -dailyLossLimit); + } + } + // Check max trades + if(RISK_Data.tradesCount >= MaxTradesPerDay) + { + RISK_Data.maxTradesReached = true; + if(g_verboseLog) + { + PrintFormat("[WARN] Max trades per day reached: %d / %d", + RISK_Data.tradesCount, MaxTradesPerDay); + } + } +} +//+------------------------------------------------------------------+ +//| Initialize Broker Configuration | +//+------------------------------------------------------------------+ +void InitializeBrokerConfig() +{ + // * v9.31 FIX#102: Use pair-specific commission from AutoOpt profile if available + double effectiveComm = (g_workingCommissionPerLot > 0) ? g_workingCommissionPerLot : COST_CommissionPerLot; + g_brokerConfig.commissionPerLot = effectiveComm; + g_brokerConfig.commissionPerSide = COST_CommissionRoundTrip ? 2.0 : 1.0; + g_brokerConfig.isCommissionInCurrency = true; + g_brokerConfig.avgSlippagePoints = COST_ExpectedSlippage; + g_brokerConfig.maxAcceptableSpread = COST_MaxSpreadPoints; + g_brokerConfig.maxAcceptableCost = COST_MaxCostPercent; + Print("[OK] Broker configuration initialized"); + PrintFormat(" Commission: $%.2f per lot", g_brokerConfig.commissionPerLot); + PrintFormat(" Max Spread: %.1f points", g_brokerConfig.maxAcceptableSpread); +} +//+------------------------------------------------------------------+ +//| Initialize Working Variables from Input Parameters | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| Auto-Detect Symbol and Apply Optimal Settings | +//+------------------------------------------------------------------+ +void AutoDetectSymbolAndOptimize() +{ + if(!PAIR_OptimizationEnabled || !PAIR_AutoDetect) + { + Print("[WARN] Auto Symbol Optimization: DISABLED"); + return; + } + string symbol = _Symbol; + Print("==========================================================="); + Print("[SEARCH] AUTO SYMBOL DETECTION & OPTIMIZATION"); + Print("==========================================================="); + Print("Symbol: ", symbol); + // Initialize with defaults + g_symbolProfile.name = symbol; + g_symbolProfile.category = "UNKNOWN"; + g_symbolProfile.pointsMultiplier = 1.0; + g_symbolProfile.volatilityFactor = 1.0; + g_symbolProfile.optimalMinConfluence = MinConfluence; + g_symbolProfile.optimalRiskReward = MinRiskReward; + g_symbolProfile.optimalRisk = AccountRiskPercent; + g_symbolProfile.optimalSwingStrength = STRUCT_SwingStrength; + g_symbolProfile.tradeAsian = SessionAsian; + g_symbolProfile.tradeLondon = SessionLondon; + g_symbolProfile.tradeNY = SessionNewYork; + g_symbolProfile.preferredTimeframes = "M15,H1,H4"; + //=============================================================== + // GOLD / XAUUSD DETECTION + //=============================================================== + if(StringFind(symbol, "XAU") >= 0 || + StringFind(symbol, "GOLD") >= 0 || + StringFind(symbol, "GLD") >= 0) + { + g_symbolProfile.category = "GOLD"; + g_symbolProfile.pointsMultiplier = 5.0; // Gold κινείται 5x περισσότερο + g_symbolProfile.volatilityFactor = 2.5; + g_symbolProfile.optimalMinConfluence = 0.75; // Υψηλότερη απαίτηση + g_symbolProfile.optimalRiskReward = 2.0; + g_symbolProfile.optimalRisk = 0.5; // Πιο conservative + g_symbolProfile.optimalSwingStrength = 5; // Ισχυρότερα swings + g_symbolProfile.tradeAsian = false; // ΟΧΙ Asian! + g_symbolProfile.tradeLondon = true; + g_symbolProfile.tradeNY = true; + g_symbolProfile.preferredTimeframes = "M15,H1"; + // * v7.4 FIX: Gold spread is 30-300 points -- override user's spread limit if too tight + if(g_workingMaxSpreadPips < 80.0) + { + g_workingMaxSpreadPips = 100.0; + Print(" * Spread Limit auto-adjusted: ", DoubleToString(EA_MaxSpreadPips, 1), + " -> 100.0 pips (Gold requires wider spread tolerance)"); + } + Print("[OK] GOLD DETECTED - Applying optimized settings"); + Print(" Points Multiplier: x", g_symbolProfile.pointsMultiplier); + Print(" Min Confluence: ", g_symbolProfile.optimalMinConfluence); + Print(" Risk:Reward: 1:", g_symbolProfile.optimalRiskReward); + Print(" Risk per Trade: ", g_symbolProfile.optimalRisk, "%"); + Print(" Sessions: London + NY ONLY"); + } + //=============================================================== + // SILVER / XAGUSD DETECTION + //=============================================================== + else if(StringFind(symbol, "XAG") >= 0 || + StringFind(symbol, "SILVER") >= 0) + { + g_symbolProfile.category = "SILVER"; + g_symbolProfile.pointsMultiplier = 4.0; + g_symbolProfile.volatilityFactor = 2.2; + g_symbolProfile.optimalMinConfluence = 0.72; + g_symbolProfile.optimalRiskReward = 2.0; + g_symbolProfile.optimalRisk = 0.6; + g_symbolProfile.optimalSwingStrength = 5; + g_symbolProfile.tradeAsian = false; + g_symbolProfile.tradeLondon = true; + g_symbolProfile.tradeNY = true; + // * v7.4 FIX: Silver also has wide spreads + if(g_workingMaxSpreadPips < 60.0) + { + g_workingMaxSpreadPips = 80.0; + Print(" * Spread Limit auto-adjusted for Silver: ", DoubleToString(EA_MaxSpreadPips, 1), " -> 80.0 pips"); + } + Print("[OK] SILVER DETECTED - Applying optimized settings"); + } + //=============================================================== + // EUR/USD (Major Pair) + //=============================================================== + else if(StringFind(symbol, "EURUSD") >= 0 || StringFind(symbol, "EUR/USD") >= 0) + { + g_symbolProfile.category = "FOREX_MAJOR"; + g_symbolProfile.pointsMultiplier = 1.0; + g_symbolProfile.volatilityFactor = 1.0; + g_symbolProfile.optimalMinConfluence = 0.65; + g_symbolProfile.optimalRiskReward = 1.5; + g_symbolProfile.optimalRisk = 1.0; + g_symbolProfile.optimalSwingStrength = 3; + g_symbolProfile.tradeAsian = false; + g_symbolProfile.tradeLondon = true; + g_symbolProfile.tradeNY = true; + g_symbolProfile.preferredTimeframes = "M15,M30,H1"; + Print("[OK] EUR/USD DETECTED - Standard forex settings"); + } + //=============================================================== + // GBP/USD + //=============================================================== + else if(StringFind(symbol, "GBPUSD") >= 0 || StringFind(symbol, "GBP/USD") >= 0) + { + g_symbolProfile.category = "FOREX_MAJOR"; + g_symbolProfile.pointsMultiplier = 1.3; + g_symbolProfile.volatilityFactor = 1.4; + g_symbolProfile.optimalMinConfluence = 0.68; + g_symbolProfile.optimalRiskReward = 1.8; + g_symbolProfile.optimalRisk = 0.8; + g_symbolProfile.optimalSwingStrength = 4; + g_symbolProfile.tradeAsian = false; + g_symbolProfile.tradeLondon = true; + g_symbolProfile.tradeNY = true; + Print("[OK] GBP/USD DETECTED - High volatility settings"); + } + //=============================================================== + // USD/JPY + //=============================================================== + else if(StringFind(symbol, "USDJPY") >= 0 || StringFind(symbol, "USD/JPY") >= 0) + { + g_symbolProfile.category = "FOREX_MAJOR"; + g_symbolProfile.pointsMultiplier = 1.1; + g_symbolProfile.volatilityFactor = 1.1; + g_symbolProfile.optimalMinConfluence = 0.67; + g_symbolProfile.optimalRiskReward = 1.6; + g_symbolProfile.optimalRisk = 0.9; + g_symbolProfile.optimalSwingStrength = 3; + g_symbolProfile.tradeAsian = true; // JPY trades in Asian too + g_symbolProfile.tradeLondon = true; + g_symbolProfile.tradeNY = true; + Print("[OK] USD/JPY DETECTED - Asian session enabled"); + } + //=============================================================== + // AUD/USD + //=============================================================== + else if(StringFind(symbol, "AUDUSD") >= 0 || StringFind(symbol, "AUD/USD") >= 0) + { + g_symbolProfile.category = "FOREX_MAJOR"; + g_symbolProfile.pointsMultiplier = 1.0; + g_symbolProfile.volatilityFactor = 1.2; + g_symbolProfile.optimalMinConfluence = 0.66; + g_symbolProfile.optimalRiskReward = 1.5; + g_symbolProfile.optimalRisk = 0.9; + g_symbolProfile.optimalSwingStrength = 3; + g_symbolProfile.tradeAsian = true; + g_symbolProfile.tradeLondon = true; + g_symbolProfile.tradeNY = true; + Print("[OK] AUD/USD DETECTED - Commodity currency settings"); + } + //=============================================================== + // USD/CAD + //=============================================================== + else if(StringFind(symbol, "USDCAD") >= 0 || StringFind(symbol, "USD/CAD") >= 0) + { + g_symbolProfile.category = "FOREX_MAJOR"; + g_symbolProfile.pointsMultiplier = 1.0; + g_symbolProfile.volatilityFactor = 1.1; + g_symbolProfile.optimalMinConfluence = 0.66; + g_symbolProfile.optimalRiskReward = 1.5; + g_symbolProfile.optimalRisk = 0.9; + g_symbolProfile.optimalSwingStrength = 3; + g_symbolProfile.tradeAsian = false; + g_symbolProfile.tradeLondon = true; + g_symbolProfile.tradeNY = true; // CAD trades best in NY + Print("[OK] USD/CAD DETECTED - NY focus settings"); + } + //=============================================================== + // INDICES (US30, NAS100, SPX500) + //=============================================================== + else if(StringFind(symbol, "US30") >= 0 || + StringFind(symbol, "US100") >= 0 || + StringFind(symbol, "NAS") >= 0 || + StringFind(symbol, "SPX") >= 0 || + StringFind(symbol, "S&P") >= 0 || + StringFind(symbol, "DOW") >= 0) + { + g_symbolProfile.category = "INDEX"; + g_symbolProfile.pointsMultiplier = 3.0; + g_symbolProfile.volatilityFactor = 1.8; + g_symbolProfile.optimalMinConfluence = 0.70; + g_symbolProfile.optimalRiskReward = 1.8; + g_symbolProfile.optimalRisk = 0.7; + g_symbolProfile.optimalSwingStrength = 4; + g_symbolProfile.tradeAsian = false; + g_symbolProfile.tradeLondon = false; + g_symbolProfile.tradeNY = true; // Only NY session + Print("[OK] US INDEX DETECTED - NY session only"); + // * FIX#SPREAD_INDEX: US100.cash FTMO real spread ~180-210p → auto-adjust if limit too tight + if(g_workingMaxSpreadPips < 200.0) + { + g_workingMaxSpreadPips = 250.0; + Print(" * Spread Limit auto-adjusted for US100 Index: ", DoubleToString(EA_MaxSpreadPips, 1), + " -> 250.0 pips (FTMO US100.cash avg spread ~190p)"); + } + } + //=============================================================== + // * v9.31 FIX#118: AUDJPY (was UNKNOWN SYMBOL -> 0 trades) + //=============================================================== + else if(StringFind(symbol, "AUDJPY") >= 0 || StringFind(symbol, "AUD/JPY") >= 0) + { + g_symbolProfile.category = "Cross"; + g_symbolProfile.pointsMultiplier = 1.0; + g_symbolProfile.volatilityFactor = 1.3; + g_symbolProfile.optimalMinConfluence = 0.60; + g_symbolProfile.optimalRiskReward = 1.8; + g_symbolProfile.optimalRisk = 0.8; + g_symbolProfile.optimalSwingStrength = 3; + g_symbolProfile.tradeAsian = true; + g_symbolProfile.tradeLondon = true; + g_symbolProfile.tradeNY = false; + Print("[OK] FIX#118 AUD/JPY DETECTED - Cross pair"); + } + //=============================================================== + // * v9.31 FIX#118: USDCHF (was UNKNOWN SYMBOL -> 0 trades) + //=============================================================== + else if(StringFind(symbol, "USDCHF") >= 0 || StringFind(symbol, "USD/CHF") >= 0) + { + g_symbolProfile.category = "Major"; + g_symbolProfile.pointsMultiplier = 1.0; + g_symbolProfile.volatilityFactor = 0.9; + g_symbolProfile.optimalMinConfluence = 0.55; + g_symbolProfile.optimalRiskReward = 1.6; + g_symbolProfile.optimalRisk = 0.8; + g_symbolProfile.optimalSwingStrength = 3; + g_symbolProfile.tradeAsian = false; + g_symbolProfile.tradeLondon = true; + g_symbolProfile.tradeNY = true; + Print("[OK] FIX#118 USD/CHF DETECTED - Safe haven major"); + } + //=============================================================== + // * v9.31 FIX#118: GBPJPY (was UNKNOWN SYMBOL -> 0 trades) + //=============================================================== + else if(StringFind(symbol, "GBPJPY") >= 0 || StringFind(symbol, "GBP/JPY") >= 0) + { + g_symbolProfile.category = "Cross"; + g_symbolProfile.pointsMultiplier = 1.0; + g_symbolProfile.volatilityFactor = 1.8; + g_symbolProfile.optimalMinConfluence = 0.70; + g_symbolProfile.optimalRiskReward = 2.0; + g_symbolProfile.optimalRisk = 0.7; + g_symbolProfile.optimalSwingStrength = 4; + g_symbolProfile.tradeAsian = true; + g_symbolProfile.tradeLondon = true; + g_symbolProfile.tradeNY = false; + Print("[OK] FIX#118 GBP/JPY DETECTED - Volatile cross pair"); + } + //=============================================================== + // * v9.31 FIX#118: US500/SPX (was UNKNOWN SYMBOL -> 0 trades) + //=============================================================== + else if(StringFind(symbol, "US500") >= 0 || StringFind(symbol, "SP500") >= 0 || + StringFind(symbol, "SPX500") >= 0 || StringFind(symbol, "S500") >= 0) + { + g_symbolProfile.category = "Index"; + g_symbolProfile.pointsMultiplier = 5.0; + g_symbolProfile.volatilityFactor = 2.0; + g_symbolProfile.optimalMinConfluence = 0.70; + g_symbolProfile.optimalRiskReward = 2.0; + g_symbolProfile.optimalRisk = 0.5; + g_symbolProfile.optimalSwingStrength = 4; + g_symbolProfile.tradeAsian = false; + g_symbolProfile.tradeLondon = false; + g_symbolProfile.tradeNY = true; + Print("[OK] FIX#118 US500/SPX DETECTED - NY session only"); + // * FIX#SPREAD_INDEX: US500.cash FTMO real spread ~56p → auto-adjust if limit too tight + if(g_workingMaxSpreadPips < 50.0) + { + g_workingMaxSpreadPips = 70.0; + Print(" * Spread Limit auto-adjusted for US500 Index: ", DoubleToString(EA_MaxSpreadPips, 1), + " -> 70.0 pips (FTMO US500.cash avg spread ~56p)"); + } + } + //=============================================================== + // CRYPTO (BTC, ETH) + //=============================================================== + else if(StringFind(symbol, "BTC") >= 0 || + StringFind(symbol, "ETH") >= 0 || + StringFind(symbol, "CRYPTO") >= 0) + { + g_symbolProfile.category = "CRYPTO"; + g_symbolProfile.pointsMultiplier = 8.0; + g_symbolProfile.volatilityFactor = 4.0; + g_symbolProfile.optimalMinConfluence = 0.80; + g_symbolProfile.optimalRiskReward = 2.5; + g_symbolProfile.optimalRisk = 0.3; + g_symbolProfile.optimalSwingStrength = 6; + g_symbolProfile.tradeAsian = true; + g_symbolProfile.tradeLondon = true; + g_symbolProfile.tradeNY = true; + Print("[OK] CRYPTO DETECTED - Extra volatile settings"); + } + //=============================================================== + // DEFAULT / UNKNOWN + //=============================================================== + else + { + // * v9.31 FIX#118b: Was hard return -> 0 trades. Now: safe defaults applied. + Print("[WARN] FIX#118 UNKNOWN SYMBOL '", symbol, "' - Applying safe Major defaults"); + g_symbolProfile.category = "Major"; + g_symbolProfile.pointsMultiplier = 1.0; + g_symbolProfile.volatilityFactor = 1.0; + g_symbolProfile.optimalMinConfluence = 0.60; + g_symbolProfile.optimalRiskReward = 1.8; + g_symbolProfile.optimalRisk = 0.8; + g_symbolProfile.optimalSwingStrength = 3; + g_symbolProfile.tradeAsian = false; + g_symbolProfile.tradeLondon = true; + g_symbolProfile.tradeNY = true; + // NOTE: No return -- continue to APPLY OPTIMIZATIONS + } + //=============================================================== + // APPLY OPTIMIZATIONS (if enabled) + //=============================================================== + if(PAIR_UseOptimalSettings) + { + Print("\n[FIX] APPLYING SYMBOL-SPECIFIC OPTIMIZATIONS:"); + // * v7.4: R:R and Risk% overrides REMOVED - EA_MinRR and EA_RiskPercent are GLOBAL masters + // g_workingMinRiskReward and g_workingAccountRiskPercent no longer overridden by pair profile + // FVG adjustments (technical, not risk - keep!) + g_workingFVG_MinSize *= g_symbolProfile.pointsMultiplier; + Print(" [OK] FVG Min Size adjusted by ", g_symbolProfile.pointsMultiplier, "x"); + // Swing Strength adjustments + g_workingSTRUCT_SwingStrength = g_symbolProfile.optimalSwingStrength; + g_workingLIQ_SwingStrength = g_symbolProfile.optimalSwingStrength + 2; + Print(" [OK] Swing Strength set to ", g_symbolProfile.optimalSwingStrength); + // Confluence adjustments + if(g_symbolProfile.optimalMinConfluence > 0) + { + g_workingMinConfluence = g_symbolProfile.optimalMinConfluence; + Print(" [OK] Min Confluence set to ", g_symbolProfile.optimalMinConfluence); + } + // Session filters + if(PAIR_SessionFilter) + { + g_workingSessionAsian = g_symbolProfile.tradeAsian; + g_workingSessionLondon = g_symbolProfile.tradeLondon; + g_workingSessionNewYork = g_symbolProfile.tradeNY; + // * v9.13 FIX#29: User's explicit session=false overrides pair profile + if(!SessionAsian) g_workingSessionAsian = false; + if(!SessionLondon) g_workingSessionLondon = false; + if(!SessionNewYork) g_workingSessionNewYork = false; + Print(" [OK] Session Filter:"); + Print(" Asian: ", (g_symbolProfile.tradeAsian ? "ENABLED" : "DISABLED")); + Print(" London: ", (g_symbolProfile.tradeLondon ? "ENABLED" : "DISABLED")); + Print(" NY: ", (g_symbolProfile.tradeNY ? "ENABLED" : "DISABLED")); + } + g_autoOptimizationApplied = true; + Print("\n[OK] AUTO-OPTIMIZATION COMPLETE!"); + } + else + { + Print("[i] Symbol detected but auto-optimization disabled in settings"); + } + Print("===========================================================\n"); +} +//+------------------------------------------------------------------+ +//| Adapt Parameters to Current Timeframe | +//+------------------------------------------------------------------+ +void AdaptParametersToTimeframe() +{ + Print("[GEAR] Adapting parameters for timeframe: ", EnumToString(_Period)); + // * FIX#455b: FVG_MaxAge was hardcoded per TF — user input was ignored after OnInit. + // FIX: use input as base, TF applies a ratio. H4=1.0 reference (FVG_MaxAge default=300→H4). + // M1=0.17x, M5=0.33x, M15=0.50x, M30=0.67x, H1=0.67x, H4=1.0x, D1=1.67x, W1=3.33x. + // User can now tune FVG_MaxAge and all TFs scale proportionally. + int _fvgBase = (FVG_MaxAge > 0) ? FVG_MaxAge : 300; + switch(_Period) + { + case PERIOD_M1: + g_workingFVG_MinSize *= 0.5; + g_workingFVG_MaxAge = MathMax(20, (int)(_fvgBase * 0.17)); + g_workingFVG_ExtendBars = 20; + g_workingRefreshRate = 1; + g_workingSL_ATRMultiplier *= 0.8; + g_workingTP_ATRMultiplier *= 0.8; + Print(" Mode: SCALPING (M1)"); + break; + case PERIOD_M5: + g_workingFVG_MinSize *= 0.8; + g_workingFVG_MaxAge = MathMax(30, (int)(_fvgBase * 0.33)); + g_workingFVG_ExtendBars = 30; + g_workingRefreshRate = 3; + g_workingSL_ATRMultiplier *= 0.9; + g_workingTP_ATRMultiplier *= 0.9; + Print(" Mode: INTRADAY (M5)"); + break; + case PERIOD_M15: + g_workingFVG_MaxAge = MathMax(50, (int)(_fvgBase * 0.50)); + g_workingFVG_ExtendBars = 50; + g_workingRefreshRate = 5; + Print(" Mode: INTRADAY (M15)"); + break; + case PERIOD_M30: + g_workingFVG_MinSize *= 1.2; + g_workingFVG_MaxAge = MathMax(80, (int)(_fvgBase * 0.67)); + g_workingFVG_ExtendBars = 60; + g_workingRefreshRate = 10; + Print(" Mode: SWING (M30)"); + break; + case PERIOD_H1: + g_workingFVG_MinSize *= 1.5; + g_workingFVG_MaxAge = MathMax(100, (int)(_fvgBase * 0.67)); + g_workingFVG_ExtendBars = 80; + g_workingRefreshRate = 15; + g_workingSL_ATRMultiplier *= 1.1; + g_workingTP_ATRMultiplier *= 1.1; + Print(" Mode: SWING (H1)"); + break; + case PERIOD_H4: + g_workingFVG_MinSize *= 2.0; + g_workingFVG_MaxAge = _fvgBase; // H4 = 1.0× (reference TF) + g_workingFVG_ExtendBars = 100; + g_workingRefreshRate = 30; + g_workingSL_ATRMultiplier *= 1.2; + g_workingTP_ATRMultiplier *= 1.2; + Print(" Mode: POSITION (H4)"); + break; + case PERIOD_D1: + g_workingFVG_MinSize *= 3.0; + g_workingFVG_MaxAge = MathMax(200, (int)(_fvgBase * 1.67)); + g_workingFVG_ExtendBars = 150; + g_workingRefreshRate = 60; + g_workingSL_ATRMultiplier *= 1.5; + g_workingTP_ATRMultiplier *= 1.5; + Print(" Mode: POSITION (D1)"); + break; + case PERIOD_W1: + g_workingFVG_MinSize *= 5.0; + g_workingFVG_MaxAge = MathMax(500, (int)(_fvgBase * 3.33)); + g_workingFVG_ExtendBars = 200; + g_workingRefreshRate = 120; + g_workingSL_ATRMultiplier *= 2.0; + g_workingTP_ATRMultiplier *= 2.0; + Print(" Mode: LONG-TERM (W1)"); + break; + default: + Print(" Mode: DEFAULT"); + break; + } + PrintFormat(" FVG Min Size: %.1f points", g_workingFVG_MinSize); + PrintFormat(" FVG Max Age: %d bars", g_workingFVG_MaxAge); + PrintFormat(" SL Multiplier: %.1fx ATR", g_workingSL_ATRMultiplier); + PrintFormat(" TP Multiplier: %.1fx ATR", g_workingTP_ATRMultiplier); + PrintFormat(" Refresh Rate: %d seconds", g_workingRefreshRate); +} +//+------------------------------------------------------------------+ +//| End of Additional Required Functions | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| [NEW] CORE ICT LOGIC UPDATES & INTEGRATION v5.0 | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| OnCalculate - Main Entry Point - v5.1 WITH CLEANUP SYSTEM | +//| [OK] Ενσωματωμένο αυτόματο cleanup για FVGs, OBs, Trendlines | +//| [OK] Κρατάει μόνο ενεργά και τωρινά στοιχεία | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| OnCalculate - DISABLED IN EA MODE | +//| All indicator analysis logic moved to EA_ProcessAnalysis() | +//| Called from OnNewBar() and OnTick() respectively | +//+------------------------------------------------------------------+ +#ifndef COMPILE_AS_EA +//+------------------------------------------------------------------+ +//| OnCalculate - INDICATOR MODE | +//| When compiled as indicator, this handles all analysis via shared | +//| RunSharedAnalysis(). EA trading functions are skipped. | +//+------------------------------------------------------------------+ +int OnCalculate(const int rates_total, + const int prev_calculated, + const datetime &time[], + const double &open[], + const double &high[], + const double &low[], + const double &close[], + const long &tick_volume[], + const long &volume[], + const int &spread[]) +{ + if(!g_initSuccess) return 0; + g_tickStart = GetTickCount(); // [v6.42] Processing time measurement + // Set arrays as series (newest = index 0) + ArraySetAsSeries(time, true); + ArraySetAsSeries(open, true); + ArraySetAsSeries(high, true); + ArraySetAsSeries(low, true); + ArraySetAsSeries(close, true); + ArraySetAsSeries(tick_volume, true); + ArraySetAsSeries(volume, true); + ArraySetAsSeries(spread, true); + g_totalRates = rates_total; + // New bar detection + static datetime lastBarTime = 0; + bool isNewBar = (time[0] != lastBarTime); + if(isNewBar) lastBarTime = time[0]; + // Only process on new bar (performance) + if(!isNewBar && prev_calculated > 0) return rates_total; + // Detection limit + // * FIX#DETECT-LIMIT: same fix as OnNewBar — was hardcoded 200, now TF-aware + int detectionLimit = MathMin(rates_total - 1, MathMin(MaxBarsToCalculate, MathMax(g_workingFVG_MaxAge, 200))); + // =========================================================== + // RUN SHARED ANALYSIS (same code as EA OnNewBar) + // =========================================================== + // * v9.40a FIX-B: AutoOpt recalc on every bar (OnTimer fires 0-2x in 35-day backtest) + UpdateAutoOptimization(); + RunSharedAnalysis(time, open, high, low, close, tick_volume, rates_total, detectionLimit); + // =========================================================== + // INDICATOR-SPECIFIC: Signals, Dashboard, Maintenance + // =========================================================== + if(EnableSignals) + { + UpdateActiveSignals(time, high, low, close); + GenerateSignals(time, open, high, low, close, tick_volume); + } + // Dashboard update + if(ShowDashboard && g_workingShowDashboard) + { + UpdateDashboard(); + } + if(Dash_Enabled) + { + UpdateProfessionalDashboard(); + } + // Periodic maintenance + PerformMaintenanceTasks(time[0]); + return rates_total; +} +#endif // !COMPILE_AS_EA +//+------------------------------------------------------------------+ +//| [OK] BONUS: Keep Only Recent Objects (Performance Boost) | +//+------------------------------------------------------------------+ +void LimitObjectsByCount(int maxFVGs = 50, int maxOBs = 30, int maxTLs = 20) +{ + // Limit FVGs to most recent maxFVGs + while(ArraySize(FVG_Array) > maxFVGs) + { + // Find oldest inactive FVG + int oldestIndex = -1; + datetime oldestTime = TimeCurrent(); + for(int i = 0; i < ArraySize(FVG_Array); i++) + { + if(FVG_Array[i].status != FVG_STATUS_ACTIVE && + FVG_Array[i].time < oldestTime) + { + oldestTime = FVG_Array[i].time; + oldestIndex = i; + } + } + if(oldestIndex >= 0) + { + DeleteFVGObjects(oldestIndex); + for(int j = oldestIndex; j < ArraySize(FVG_Array) - 1; j++) + FVG_Array[j] = FVG_Array[j + 1]; + ArrayResize(FVG_Array, ArraySize(FVG_Array) - 1); + g_fvgCount = ArraySize(FVG_Array); // Update count + } + else + break; // All are active, don't delete + } + // Similar logic for OBs and TLs... + // (Add if needed) +} +//+------------------------------------------------------------------+ +//| Update Cached Indicators | +//+------------------------------------------------------------------+ +void UpdateCachedIndicators() +{ + datetime currentTime = TimeCurrent(); + // * FIX#452b: Cache update interval = g_workingRefreshRate seconds (was hardcoded 5s). + // RefreshRate input now controls both intra-bar throttle AND cache update frequency. + // H4=30s, H1=15s, M15=5s, M5=3s — matches actual bar speed. + // EnableCache=false → always recalculate (unchanged). + int _cacheInterval = (g_workingRefreshRate > 0) ? g_workingRefreshRate : 5; + if(EnableCache && (currentTime - g_lastCacheUpdate) < _cacheInterval) return; + // Update MA (Moving Average) + if(g_maHandle != INVALID_HANDLE) + { + double ma[]; + ArraySetAsSeries(ma, true); + if(CopyBuffer(g_maHandle, 0, 0, 1, ma) > 0) + { + g_cachedMA = ma[0]; + // * v9.16 FIX#48: ShowMA draws MA level on chart (was dead input) + if(ShowMA && g_cachedMA > 0) + { + string maObjName = "ICT_MA_Line"; + if(ObjectFind(0, maObjName) < 0) + ObjectCreate(0, maObjName, OBJ_HLINE, 0, 0, g_cachedMA); + ObjectSetDouble(0, maObjName, OBJPROP_PRICE, g_cachedMA); + ObjectSetInteger(0, maObjName, OBJPROP_COLOR, clrDodgerBlue); + ObjectSetInteger(0, maObjName, OBJPROP_STYLE, STYLE_DOT); + ObjectSetInteger(0, maObjName, OBJPROP_WIDTH, 1); + ObjectSetInteger(0, maObjName, OBJPROP_BACK, true); + ObjectSetString(0, maObjName, OBJPROP_TOOLTIP, StringFormat("MA(%d) = %s", MAPeriod, DoubleToString(g_cachedMA, _Digits))); + } + else if(!ShowMA) + ObjectDelete(0, "ICT_MA_Line"); + } + ArrayFree(ma); + } + // Update ATR (Average True Range) + if(g_atrHandle != INVALID_HANDLE) + { + double atr[]; + ArraySetAsSeries(atr, true); + if(CopyBuffer(g_atrHandle, 0, 0, 1, atr) > 0 && atr[0] > 0) + { + g_cachedATR = atr[0]; + } + ArrayFree(atr); + } + // Update RSI (Relative Strength Index) + if(g_rsiHandle != INVALID_HANDLE) + { + double rsi[]; + ArraySetAsSeries(rsi, true); + if(CopyBuffer(g_rsiHandle, 0, 0, 1, rsi) > 0) + { + g_cachedRSI = rsi[0]; + } + ArrayFree(rsi); + } + // ── FIX#501: TWO-LEVEL HTF ADX — middle TF + higher TF ──────── + // Framework: Entry TF → Middle TF → Higher TF (3-level hierarchy) + // M5 EA: M15(mid) + H1(high) + // M15 EA: H1(mid) + H4(high) + // H1 EA: H4(mid) + D1(high) ← EA ANGEL + // H4 EA: D1(mid) + W1(high) + // D1 EA: W1(mid) + W1(high) (capped at W1) + // Level 1a: ADX_mid < 25 = intermediate TF not trending + // Level 1b: ADX_high < 20 = macro TF ranging (stricter threshold) + { + ENUM_TIMEFRAMES _midTF, _highTF; + if(_Period <= PERIOD_M5) + { _midTF = PERIOD_M15; _highTF = PERIOD_H1; } + else if(_Period <= PERIOD_M30) + { _midTF = PERIOD_H1; _highTF = PERIOD_H4; } + else if(_Period <= PERIOD_H1) + { _midTF = PERIOD_H4; _highTF = PERIOD_D1; } + else if(_Period <= PERIOD_H4) + { _midTF = PERIOD_D1; _highTF = PERIOD_W1; } + else + { _midTF = PERIOD_W1; _highTF = PERIOD_W1; } + + // Middle TF ADX + int _hMid = iADX(_Symbol, _midTF, 14); + if(_hMid != INVALID_HANDLE) + { + double _b[]; ArraySetAsSeries(_b, true); + if(CopyBuffer(_hMid, 0, 0, 1, _b) > 0) g_cachedADX_HTF_mid = _b[0]; + IndicatorRelease(_hMid); + } + // Higher TF ADX (macro context) + int _hHigh = iADX(_Symbol, _highTF, 14); + if(_hHigh != INVALID_HANDLE) + { + double _b[]; ArraySetAsSeries(_b, true); + if(CopyBuffer(_hHigh, 0, 0, 1, _b) > 0) g_cachedADX_HTF_high = _b[0]; + IndicatorRelease(_hHigh); + } + } + // ── FIX#501: MA slope — flat MA confirms ranging ────────────── + // Slope = |MA[0] - MA[5]| / ATR. < 0.30 = flat (ranging), > 0.60 = steep (trending) + if(g_maHandle != INVALID_HANDLE && g_cachedATR > 0) + { + double _maBuf[]; + ArraySetAsSeries(_maBuf, true); + if(CopyBuffer(g_maHandle, 0, 0, 6, _maBuf) >= 6) + g_cachedMA_Slope = MathAbs(_maBuf[0] - _maBuf[5]) / g_cachedATR; + } + g_lastCacheUpdate = currentTime; +} +//+------------------------------------------------------------------+ +//| Process ML Predictions (v5.0 Enhanced) | +//+------------------------------------------------------------------+ +void ProcessMLPredictions(const datetime &time[], const double &open[], + const double &high[], const double &low[], + const double &close[], const long &volume[]) +{ + if(!g_nnInitialized) return; + datetime currentTime = TimeCurrent(); + // Check if training is needed + if(!g_nnTrained && g_totalRates >= MLLookbackPeriod + NN_PREDICTION_HORIZON + 100) + { + if((currentTime - g_lastNNTraining) > 3600) // Train at most once per hour + { + TrainNeuralNetwork(time, open, high, low, close, volume); + } + } + // Auto-optimize if enabled + if(ML_AutoOptimize && g_nnTrained) + { + static int barsSinceOptimize = 0; + barsSinceOptimize++; + if(barsSinceOptimize >= ML_OptimizeInterval) + { + Print("[SYNC] Auto-optimizing Neural Network..."); + TrainNeuralNetwork(time, open, high, low, close, volume); + barsSinceOptimize = 0; + } + } + // Generate prediction + if(g_nnTrained && (currentTime - g_lastNNPrediction) >= MLUpdateInterval) + { + GenerateNNPrediction(time, open, high, low, close, volume); + g_lastNNPrediction = currentTime; + } + // Update prediction visualization + if(ShowPricePrediction) + { + UpdatePredictionVisualization(time, close); + } + if(ShowProbabilityHeatmap) + { + UpdateProbabilityHeatmap(time, high, low, close); + } +} +//+------------------------------------------------------------------+ +//| Generate Neural Network Prediction | +//+------------------------------------------------------------------+ +void GenerateNNPrediction(const datetime &time[], const double &open[], + const double &high[], const double &low[], + const double &close[], const long &volume[]) +{ + if(!g_nnInitialized || !g_nnTrained || !g_nnReadyForUse) + { + g_nnTP1WinProb = 0.5; // Neutral when not trained + return; + } + // * Auto-retrain every 50 new trades (online learning) + if(g_tradeHistoryCount >= g_nnLastTrainCount + 50 && g_tradeHistoryCount >= 30) + { + Print("[BOT] NN v9.10: Retraining on ", g_tradeHistoryCount, " trades (50 new since last train)"); + TrainNeuralNetwork(time, open, high, low, close, volume); + } + // Extract current market features + double features[]; + ArrayResize(features, NN_INPUT_FEATURES); + ExtractFeatures(0, time, open, high, low, close, volume, features); + // Forward pass + g_neuralNet.isTraining = false; + ForwardPass(features); + // Read output + int outputIdx = g_neuralNet.numLayers - 1; + double winProb = g_neuralNet.layers[outputIdx].neurons[0].output; + double lossProb = (NN_OUTPUT_NODES >= 3) + ? g_neuralNet.layers[outputIdx].neurons[2].output + : (1.0 - winProb); + // g_nnTP1WinProb = P(TP1 hits before SL) + // Normalize: remove neutral class influence + double total = winProb + lossProb; + g_nnTP1WinProb = (total > 0.01) ? winProb / total : 0.5; + // Clamp to reasonable range (NN never has perfect certainty) + g_nnTP1WinProb = MathMax(0.25, MathMin(0.80, g_nnTP1WinProb)); + // * v10.29 FIX#316: TP2 probability from neutral node (repurposed) + // NN_OUTPUT_NODES=3: node[1] (neutral) → repurpose as TP2 signal + // High neutral prob = price likely moves but may not hold → moderate runner + double neutralProb316 = (NN_OUTPUT_NODES >= 3) + ? g_neuralNet.layers[outputIdx].neurons[1].output : 0.3; + g_nnTP2WinProb = MathMax(0.10, MathMin(0.70, neutralProb316 * 0.7 + winProb * 0.3)); + // * v10.29 FIX#316: Dynamic optimal exit R + // Combines TP1 win prob + TP2 signal to suggest how long to hold + // Low TP1 prob → exit early (0.80R) | High TP2 prob → hold longer (2.0R) + // Default 1.20R = current se_minrr[3] — ML adjusts within ±0.4R + g_nnOptimalExitR = 1.20 + (g_nnTP2WinProb - 0.30) * 1.5; // range: 0.75R to 1.65R + g_nnOptimalExitR = MathMax(0.75, MathMin(2.00, g_nnOptimalExitR)); + // =============================================================== + // Store in ML_Predictions array (for dashboard / logging) + // =============================================================== + ML_Prediction pred; + pred.timestamp = time[0]; + pred.bullishProb = winProb; + pred.neutralProb = (NN_OUTPUT_NODES >= 3) ? g_neuralNet.layers[outputIdx].neurons[1].output : 0; + pred.bearishProb = lossProb; + pred.confidence = g_nnTP1WinProb; + // Category reflects TP1 hit probability + if(g_nnTP1WinProb >= 0.60) + { + pred.category = "HIGH_WIN"; // Strong probability TP1 hits + pred.expectedMove = g_cachedATR * 1.5 / g_pipValue; + pred.targetPrice = close[0] + (g_ea_signal.isBullish ? g_cachedATR * 1.5 : -g_cachedATR * 1.5); + } + else if(g_nnTP1WinProb >= 0.50) + { + pred.category = "MODERATE"; + pred.expectedMove = g_cachedATR * 1.0 / g_pipValue; + pred.targetPrice = close[0]; + } + else + { + pred.category = "LOW_WIN"; // Low probability -- caution + pred.expectedMove = 0; + pred.targetPrice = close[0]; + } + pred.actualDirection = ""; + pred.wasCorrect = false; + // Store prediction + int psize = ArraySize(ML_Predictions); + if(psize >= 500) + { + for(int i = 0; i < 499; i++) ML_Predictions[i] = ML_Predictions[i + 1]; + ArrayResize(ML_Predictions, 500); + psize = 499; + } + else + ArrayResize(ML_Predictions, psize + 1); + ML_Predictions[psize] = pred; + // Validate old predictions + ValidatePreviousPredictions(close); + if(VerboseMLLogging) + PrintFormat("[BOT] NN v9.10 TP1WinProb=%.1f%% | WIN:%.2f LOSS:%.2f | Category=%s", + g_nnTP1WinProb * 100, winProb, lossProb, pred.category); + ArrayFree(features); +} +//+------------------------------------------------------------------+ +//| * v9.10 ValidatePreviousPredictions -- check if predictions came true | +//+------------------------------------------------------------------+ +void ValidatePreviousPredictions(const double &close[]) +{ + int lookback = NN_PREDICTION_HORIZON; + int size = ArraySize(ML_Predictions); + for(int i = size - 1; i >= 0 && i >= size - 50; i--) + { + if(ML_Predictions[i].wasCorrect || ML_Predictions[i].actualDirection != "") continue; + if(ArraySize(close) < lookback + 1) continue; + // Simple check: did g_nnTP1WinProb >= 0.55 correspond to a WIN trade? + // We use the most recent trade result as ground truth + if(g_tradeHistoryCount > 0) + { + TradeRecord lastTrade = g_tradeHistory[g_tradeHistoryCount - 1]; + bool tradeIsRecent = (TimeCurrent() - lastTrade.exitTime < 4 * 3600); + if(tradeIsRecent && lastTrade.exitTime > ML_Predictions[i].timestamp) + { + bool predWin = (ML_Predictions[i].confidence >= 0.55); + bool actualWin = (lastTrade.result == "WIN"); + ML_Predictions[i].actualDirection = actualWin ? "WIN" : "LOSS"; + ML_Predictions[i].wasCorrect = (predWin == actualWin); + } + } + } +} +//+------------------------------------------------------------------+ +//| Validate Previous ML Predictions | +//+------------------------------------------------------------------+ +//| Update ML Accuracy Statistics | +//+------------------------------------------------------------------+ +void UpdateMLAccuracy() +{ + int total = 0; + int correct = 0; + for(int i = 0; i < ArraySize(ML_Predictions); i++) + { + if(ML_Predictions[i].actualDirection != "") + { + total++; + if(ML_Predictions[i].wasCorrect) correct++; + } + } + if(total > 0) + { + g_perfData.mlAccuracy = (double)correct / total * 100.0; + } +} +//+------------------------------------------------------------------+ +//| Generate Trading Signals (v5.0 Enhanced) | +//+------------------------------------------------------------------+ +// ── FIX#502: SCENARIO-FILTERED SIGNAL GENERATION ───────────────────── +// Replaces GenerateSignals() when g_scenarioProfile.isValid is true. +// Only calls the signal generators allowed for the current scenario. +// Eliminates wasted computation: in ranging market, TC never runs at all. +void GenerateSignals_ForScenario(const datetime &time[], const double &open[], + const double &high[], const double &low[], + const double &close[], const long &volume[]) +{ + // Same filters as GenerateSignals() + if(g_workingUseSessionFilter && !IsInTradingSession()) return; + if(!PassesKillzoneFilter()) return; + if(EA_EnableSpreadFilter && EA_CheckSpreadOnEntry) + { + double spread = SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * g_point / g_pipValue; + if(spread > g_workingMaxSpreadPips) return; + } + if(g_workingUseATRFilter) + { + double atrPips = g_cachedATR / g_pipValue; + if(atrPips < g_workingATRMinValue || atrPips > g_workingATRMaxValue) return; + } + if(EnableRiskMgmt && g_tradesThisDay >= MaxTradesPerDay) return; + + double currentPrice = close[0]; + + // SmartExit Re-Entry: delegate to full GenerateSignals (complex logic) + if(g_seReEntry.active) + { + GenerateSignals(time, open, high, low, close, volume); + return; + } + + if(g_scenarioProfile.allowFVG && STRATEGY_FVG_Entry && !g_seReEntryReady) + CheckFVGEntrySignal(time, high, low, close, open, currentPrice); + + if(g_scenarioProfile.allowOB && STRATEGY_OB_Entry) + CheckOBEntrySignal(time, high, low, close, open, currentPrice); + + if(g_scenarioProfile.allowBOS && STRATEGY_BOS_Retest) + CheckBOSRetestSignal(time, high, low, close, open, currentPrice); + + if(g_scenarioProfile.allowLIQ && STRATEGY_LIQ_Grab) + CheckLiquidityGrabSignal(time, high, low, close, open, currentPrice); + + if(g_scenarioProfile.allowOTE && STRATEGY_OTE_Entry) + CheckOTEEntrySignal(time, high, low, close, open, currentPrice); + + if(g_scenarioProfile.allowOB && STRATEGY_BB_Entry) + CheckBreakerBlockSignal(time, high, low, close, currentPrice); + if(g_scenarioProfile.allowOB && STRATEGY_MB_Entry) + CheckMitigationBlockSignal(time, high, low, close, currentPrice); + + if(g_scenarioProfile.allowBOS && STRATEGY_MM_Model) + CheckMarketMakerSignal(time, high, low, close, volume, currentPrice); + + // ML, Killzone, MultiConfluence — always allowed + if(STRATEGY_ML_Signal && g_nnTrained) + CheckMLSignal(time, open, high, low, close, volume, currentPrice); + if(STRATEGY_Killzone && g_isInKillzone) + CheckKillzoneSignal(time, high, low, close, currentPrice); + CheckMultiConfluenceSignals(time, open, high, low, close, volume, currentPrice); + + if(g_scenarioProfile.allowTC && EnableTrendCont && g_workingAllowTC != -1) + CheckTCEntrySignal(time, high, low, close, open, currentPrice); + + // TBS — always (false breakout valid in most scenarios) + CheckTBSEntrySignal(time, high, low, close, open, currentPrice); + + if(g_scenarioProfile.allowMEANREV) + { + bool _mrEnabled = (g_workingMR_Enabled == 1) || + (g_workingMR_Enabled == 0 && Regime_UseMeanReversion); + if(_mrEnabled) + CheckMeanReversionSignal(time, high, low, close, open, currentPrice); + } + + if(g_verboseLog) + PrintFormat("[FIX#502] ForScenario=%s | FVG=%s OB=%s BOS=%s OTE=%s TC=%s LIQ=%s MR=%s", + g_scenarioProfile.description, + g_scenarioProfile.allowFVG?"Y":"N", g_scenarioProfile.allowOB?"Y":"N", + g_scenarioProfile.allowBOS?"Y":"N", g_scenarioProfile.allowOTE?"Y":"N", + g_scenarioProfile.allowTC?"Y":"N", g_scenarioProfile.allowLIQ?"Y":"N", + g_scenarioProfile.allowMEANREV?"Y":"N"); +} + +void GenerateSignals(const datetime &time[], const double &open[], + const double &high[], const double &low[], + const double &close[], const long &volume[]) +{ + // Apply session filter + if(g_workingUseSessionFilter && !IsInTradingSession()) + { + return; + } + // Apply Killzone filter (v5.0) + if(!PassesKillzoneFilter()) + { + return; + } + // Apply spread filter + // * v7.4 FIX: Use g_workingMaxSpreadPips (auto-adapted for Gold/Silver) + if(EA_EnableSpreadFilter && EA_CheckSpreadOnEntry) + { + double spread = SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * g_point / g_pipValue; + if(spread > g_workingMaxSpreadPips) + { + if(g_verboseLog) + Print("Indicator spread filter: ", DoubleToString(spread, 1), " > ", DoubleToString(g_workingMaxSpreadPips, 1), " pips"); + return; + } + } + // Apply ATR filter + if(g_workingUseATRFilter) + { + double atrPips = g_cachedATR / g_pipValue; + if(atrPips < g_workingATRMinValue || atrPips > g_workingATRMaxValue) + { + return; + } + } + // Check daily trade limit + if(EnableRiskMgmt && g_tradesThisDay >= MaxTradesPerDay) + { + return; + } + double currentPrice = close[0]; + // ================================================================ + // * FIX#424: SmartExit Re-Entry check + // ================================================================ + // After SmartExit closes at profit, g_seReEntry.active=true with + // the zone info stored. If price returns to the zone within maxBars + // AND direction still valid AND no open positions → synthesize signal + // directly, bypassing the mitigated OB/FVG check. + // Guards: + // 1. Re-entry window not expired (maxBars) + // 2. Price inside zone boundaries (touch, not close) + // 3. Same direction still valid (D1/H4 MTF bias unchanged) + // 4. No existing open position in same direction + // 5. Consumes the slot immediately (g_seReEntry.active=false) + if(g_seReEntry.active) + { + // Check if window expired + int _bars424 = Bars(_Symbol, _Period, g_seReEntry.closedAt, TimeCurrent()); + if(_bars424 > g_seReEntry.maxBars) + { + g_seReEntry.active = false; + if(g_verboseLog) + PrintFormat("* FIX#424 RE-ENTRY EXPIRED: %d bars elapsed (max=%d)", + _bars424, g_seReEntry.maxBars); + } + else + { + // Check if price is touching the zone + bool _isBuy424 = (g_seReEntry.direction == 1); + double _ask424 = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + double _bid424 = SymbolInfoDouble(_Symbol, SYMBOL_BID); + double _price424 = _isBuy424 ? _ask424 : _bid424; + bool _inZone424 = (_price424 >= g_seReEntry.zoneBottom && + _price424 <= g_seReEntry.zoneTop); + // Check direction still valid: MTF must not oppose + bool _dirOK424 = true; + if(MTF_Enabled && g_mtfAnalysis.totalTFs >= 2) + { + bool _mtfBull = (g_mtfAnalysis.overallDirection == MTF_BULLISH || + g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH || + g_mtfAnalysis.overallDirection == MTF_NEUTRAL); + if(_isBuy424 && !_mtfBull) _dirOK424 = false; + if(!_isBuy424 && _mtfBull) _dirOK424 = false; + } + // Check no open position in same direction + bool _noSameDir424 = _isBuy424 ? !g_ea_has_open_buy : !g_ea_has_open_sell; + if(_inZone424 && _dirOK424 && _noSameDir424) + { + // Synthesize a signal directly from stored zone data + double _atr424 = g_cachedATR > 0 ? g_cachedATR : MathAbs(g_seReEntry.zoneTop - g_seReEntry.zoneBottom); + double _sl424 = _isBuy424 ? g_seReEntry.zoneBottom - _atr424 * 0.30 + : g_seReEntry.zoneTop + _atr424 * 0.30; + double _slDist = MathAbs(_price424 - _sl424); + double _mrr424 = (g_autoOptParams.min_rr > 0) ? g_autoOptParams.min_rr : 1.60; + double _tp1424 = _isBuy424 ? _price424 + _slDist * _mrr424 + : _price424 - _slDist * _mrr424; + double _tp2424 = _isBuy424 ? _price424 + _slDist * _mrr424 * 1.5 + : _price424 - _slDist * _mrr424 * 1.5; + double _tp3424 = _isBuy424 ? _price424 + _slDist * _mrr424 * 2.0 + : _price424 - _slDist * _mrr424 * 2.0; + // Validate RR + if(_slDist > 0 && MathAbs(_tp1424 - _price424) / _slDist >= _mrr424) + { + g_ea_signal.isValid = true; + g_ea_signal.isBullish = _isBuy424; + g_ea_signal.type = "FIX424_REENTRY"; + g_ea_signal.entryPrice = _price424; + g_ea_signal.stopLoss = _sl424; + g_ea_signal.tp1 = _tp1424; + g_ea_signal.tp2 = _tp2424; + g_ea_signal.tp3 = _tp3424; + g_ea_signal.zoneTop = g_seReEntry.zoneTop; + g_ea_signal.zoneBottom = g_seReEntry.zoneBottom; + g_ea_signal.zoneType = g_seReEntry.zoneType; + g_ea_signal.score = 70; // fixed score — SmartEntry will re-evaluate + g_seReEntry.active = false; // consume slot + PrintFormat("* FIX#424 RE-ENTRY TRIGGERED: %s @ %.5f zone[%.5f-%.5f] %s SL=%.5f TP1=%.5f bars=%d", + _isBuy424?"BUY":"SELL", _price424, + g_seReEntry.zoneBottom, g_seReEntry.zoneTop, + g_seReEntry.zoneType, _sl424, _tp1424, _bars424); + // Signal is set in g_ea_signal — skip strategy scanning below. + // g_seReEntryReady flag tells the end of EA_CheckSignals to bypass + // candidate list and go straight to SmartEntry validation. + g_seReEntryReady = true; + } + } + } + } + // =============================================================== + // STRATEGY 1: FVG Entry + // =============================================================== + if(!g_seReEntryReady && STRATEGY_FVG_Entry) + { + CheckFVGEntrySignal(time, high, low, close, open, currentPrice); + } + // =============================================================== + // STRATEGY 2: Order Block Entry + // =============================================================== + if(STRATEGY_OB_Entry) + { + CheckOBEntrySignal(time, high, low, close, open, currentPrice); + } + // =============================================================== + // STRATEGY 3: BOS Retest + // =============================================================== + if(STRATEGY_BOS_Retest) + { + CheckBOSRetestSignal(time, high, low, close, open, currentPrice); + } + // =============================================================== + // STRATEGY 4: Liquidity Grab + // =============================================================== + if(STRATEGY_LIQ_Grab) + { + CheckLiquidityGrabSignal(time, high, low, close, open, currentPrice); + } + // =============================================================== + // STRATEGY 5: OTE Entry + // =============================================================== + if(STRATEGY_OTE_Entry) + { + CheckOTEEntrySignal(time, high, low, close, open, currentPrice); + } + // =============================================================== + // STRATEGY 6: Breaker Block Entry + // =============================================================== + if(STRATEGY_BB_Entry) + { + CheckBreakerBlockSignal(time, high, low, close,currentPrice); + } + // =============================================================== + // STRATEGY 7: Mitigation Block Entry + // =============================================================== + if(STRATEGY_MB_Entry) + { + CheckMitigationBlockSignal(time, high, low, close,currentPrice); + } + // =============================================================== + // STRATEGY 8: Market Maker Model + // =============================================================== + if(STRATEGY_MM_Model) + { + CheckMarketMakerSignal(time, high, low, close, volume, currentPrice); + } + // =============================================================== + // STRATEGY 9: ML Signal (v5.0 Enhanced) + // =============================================================== + if(STRATEGY_ML_Signal && g_nnTrained) + { + CheckMLSignal(time, open, high, low, close, volume, currentPrice); + } + // =============================================================== + // STRATEGY 10: Killzone Entry (v5.0 NEW) + // =============================================================== + if(STRATEGY_Killzone && g_isInKillzone) + { + CheckKillzoneSignal(time, high, low, close,currentPrice); + } + // =============================================================== + // MULTI-CONFLUENCE SIGNALS + // =============================================================== + CheckMultiConfluenceSignals(time, open, high, low, close, volume, currentPrice); + + // Strategy 11: Trend Continuation (EMA pullback from g_tcSetups[]) + if(EnableTrendCont && g_workingAllowTC != -1) + CheckTCEntrySignal(time, high, low, close, open, currentPrice); + + // Strategy 12: TBS False Breakout Reversal (from g_tbsSetups[]) + CheckTBSEntrySignal(time, high, low, close, open, currentPrice); + + // Strategy 13: Mean Reversion (CHOPPY/RANGING regime only) + bool _mrEnabled = (g_workingMR_Enabled == 1) || + (g_workingMR_Enabled == 0 && Regime_UseMeanReversion); + if(_mrEnabled) + CheckMeanReversionSignal(time, high, low, close, open, currentPrice); +} + +//+------------------------------------------------------------------+ +//| Strategy 11 — Trend Continuation (EMA pullback) | +//+------------------------------------------------------------------+ +void CheckTCEntrySignal(const datetime &time[], const double &high[], + const double &low[], const double &close[], + const double &open[], double currentPrice) +{ + int n = ArraySize(g_tcSetups); + if(n == 0) return; + + for(int i = 0; i < n; i++) + { + if(!g_tcSetups[i].active) continue; + // Age cap: 3 bars on H1 and above, 5 bars on M5/M15 + int maxAge = (_Period >= PERIOD_H1) ? 3 : 5; + if(g_tcSetups[i].age > maxAge) { g_tcSetups[i].active = false; continue; } + + bool isBullish = g_tcSetups[i].isBullish; + + // Structure must align + if(isBullish && !g_isBullishStructure) continue; + if(!isBullish && g_isBullishStructure) continue; + + // MTF: no strong opposition + bool mtfStrongOpposed = ( isBullish && g_mtfAnalysis.overallDirection == MTF_STRONG_BEARISH) || + (!isBullish && g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH); + if(mtfStrongOpposed) continue; + + // Price must be at or near the entry level (within 0.3×ATR) + double tolerance = g_cachedATR * 0.30; + if(MathAbs(currentPrice - g_tcSetups[i].entryPrice) > tolerance) continue; + + // SL/TP from setup (already computed in DetectTrendCont) + double sl = g_tcSetups[i].stopLoss; + double tp = g_tcSetups[i].tp1; + if(sl <= 0 || tp <= 0) continue; + double slDist = MathAbs(currentPrice - sl); + if(slDist < g_cachedATR * 0.3) continue; // SL too tight + + double confluence = 0.55; + if(g_isInKillzone) confluence += 0.10; + if((isBullish && g_currentPDZone == "DISCOUNT") || + (!isBullish && g_currentPDZone == "PREMIUM")) confluence += 0.10; + if(confluence < g_workingMinConfluence) continue; + + double quality = CalculateEntryQuality(currentPrice, isBullish, "TC_ENTRY", confluence); + if(quality < g_workingMinEntryQuality) continue; + + if(InpEnableScoring) + { + CandlePatternStruct cp = DetectCandlePattern(open, high, low, close, 0); + ConfluenceScoreStruct cd = CalculateConfluenceData(currentPrice, isBullish); + EntryScoreStruct es = CalculateEntryScore(isBullish, currentPrice, cd, cp, 0); + g_lastEntryScore = es; + if(!MeetsMinimumEntryScore(es)) continue; + } + + g_tcSetups[i].active = false; // consume setup + + // Add to real execution candidate list + { + double _cSlDist = MathAbs(currentPrice - sl); + double _cTp2 = isBullish ? currentPrice + _cSlDist * g_autoOptParams.tp2_rr + : currentPrice - _cSlDist * g_autoOptParams.tp2_rr; + double _cTp3 = isBullish ? currentPrice + _cSlDist * g_autoOptParams.tp3_rr + : currentPrice - _cSlDist * g_autoOptParams.tp3_rr; + int _cScore = (InpEnableScoring && g_lastEntryScore.totalScore > 0) + ? g_lastEntryScore.totalScore : (int)(quality * 85); + AddCandidate(isBullish, "TC_ENTRY", TECH_TREND_CONT, -1, + currentPrice, sl, tp, _cTp2, _cTp3, _cScore); + } + CreateSignal(time[0], currentPrice, sl, tp, isBullish, "TC_ENTRY", + confluence, quality, 0); + return; + } +} + +//+------------------------------------------------------------------+ +//| Strategy 12 — TBS False Breakout Reversal | +//+------------------------------------------------------------------+ +void CheckTBSEntrySignal(const datetime &time[], const double &high[], + const double &low[], const double &close[], + const double &open[], double currentPrice) +{ + int n = ArraySize(g_tbsSetups); + if(n == 0) return; + + for(int i = 0; i < n; i++) + { + if(!g_tbsSetups[i].active) continue; + if(g_tbsSetups[i].status == TBS_EXPIRED || g_tbsSetups[i].status == TBS_TRIGGERED) continue; + + bool isBullish = (g_tbsSetups[i].type == TBS_BULLISH); + + // Price must have returned back inside (confirmed reversal): + // Bullish: swept SSL below, now price > liquidityLevel + // Bearish: swept BSL above, now price < liquidityLevel + if( isBullish && currentPrice <= g_tbsSetups[i].liquidityLevel) continue; + if(!isBullish && currentPrice >= g_tbsSetups[i].liquidityLevel) continue; + + double sl = g_tbsSetups[i].stopLoss; + double tp = g_tbsSetups[i].tp1; + if(sl <= 0 || tp <= 0) continue; + + double confluence = 0.55; + if(g_tbsSetups[i].withTrend) confluence += 0.10; + if(g_tbsSetups[i].inKillzone) confluence += 0.10; + if(g_tbsSetups[i].hasOBNearby) confluence += 0.08; + if(g_tbsSetups[i].hasFVGNearby) confluence += 0.08; + if(confluence < g_workingMinConfluence) continue; + + double quality = CalculateEntryQuality(currentPrice, isBullish, "TBS_ENTRY", confluence); + if(quality < g_workingMinEntryQuality) continue; + + if(InpEnableScoring) + { + CandlePatternStruct cp = DetectCandlePattern(open, high, low, close, 0); + ConfluenceScoreStruct cd = CalculateConfluenceData(currentPrice, isBullish); + EntryScoreStruct es = CalculateEntryScore(isBullish, currentPrice, cd, cp, 0); + g_lastEntryScore = es; + if(!MeetsMinimumEntryScore(es)) continue; + } + + g_tbsSetups[i].status = TBS_TRIGGERED; + g_tbsSetups[i].triggered = true; + g_tbsSetups[i].active = false; + + // Add to real execution candidate list + { + double _cSlDist = MathAbs(currentPrice - sl); + double _cTp2 = isBullish ? currentPrice + _cSlDist * g_autoOptParams.tp2_rr + : currentPrice - _cSlDist * g_autoOptParams.tp2_rr; + double _cTp3 = isBullish ? currentPrice + _cSlDist * g_autoOptParams.tp3_rr + : currentPrice - _cSlDist * g_autoOptParams.tp3_rr; + int _cScore = (InpEnableScoring && g_lastEntryScore.totalScore > 0) + ? g_lastEntryScore.totalScore : (int)(quality * 85); + AddCandidate(isBullish, "TBS_ENTRY", TECH_TBS, -1, + currentPrice, sl, tp, _cTp2, _cTp3, _cScore); + } + CreateSignal(time[0], currentPrice, sl, tp, isBullish, "TBS_ENTRY", + confluence, quality, 0); + return; + } +} + +//+------------------------------------------------------------------+ +//| Strategy 13 — Mean Reversion (CHOPPY/RANGING regime) | +//+------------------------------------------------------------------+ +void CheckMeanReversionSignal(const datetime &time[], const double &high[], + const double &low[], const double &close[], + const double &open[], double currentPrice) +{ + // Only fires in CHOPPY or RANGING regimes + bool isRanging = (g_regimeData.regime == REGIME_CHOPPY || + g_regimeData.regime == REGIME_RANGING || + g_regimeData.regime == REGIME_RANGING_TIGHT || + g_regimeData.regime == REGIME_RANGING_WIDE); + if(!isRanging) return; + + // RSI exhaustion: oversold → BUY, overbought → SELL + int rsiBuy = (g_workingMR_RSIBuy > 0) ? g_workingMR_RSIBuy : 35; + int rsiSell = (g_workingMR_RSISell > 0) ? g_workingMR_RSISell : 65; + if(g_cachedRSI <= 0) return; + + bool tryBuy = (g_cachedRSI <= rsiBuy); + bool trySell = (g_cachedRSI >= rsiSell); + if(!tryBuy && !trySell) return; + + bool isBullish = tryBuy; + + // Need an OB or swept liquidity zone nearby as anchor + int obIdx = -1; + bool hasAnchor = IsPriceNearOB(currentPrice, obIdx, isBullish); + if(!hasAnchor) return; + + // SL: beyond the OB extreme + MR buffer + double slBuf = g_cachedATR * ((g_workingMR_SLMult > 0) ? g_workingMR_SLMult : 0.30); + double sl, tp; + if(isBullish) + { + double obBottom = (obIdx >= 0 && obIdx < ArraySize(OB_Array)) ? OB_Array[obIdx].bottom : currentPrice - g_cachedATR; + sl = obBottom - slBuf; + double slDist = currentPrice - sl; + if(slDist < g_cachedATR * 0.3) return; + tp = currentPrice + slDist * g_workingMinRiskReward; + } + else + { + double obTop = (obIdx >= 0 && obIdx < ArraySize(OB_Array)) ? OB_Array[obIdx].top : currentPrice + g_cachedATR; + sl = obTop + slBuf; + double slDist = sl - currentPrice; + if(slDist < g_cachedATR * 0.3) return; + tp = currentPrice - slDist * g_workingMinRiskReward; + } + + double confluence = 0.50; + if(g_isInKillzone) confluence += 0.10; + if(hasAnchor) confluence += 0.10; + if(confluence < g_workingMinConfluence) return; + + double quality = CalculateEntryQuality(currentPrice, isBullish, "MEAN_REV", confluence); + if(quality < g_workingMinEntryQuality) return; + + if(InpEnableScoring) + { + CandlePatternStruct cp = DetectCandlePattern(open, high, low, close, 0); + ConfluenceScoreStruct cd = CalculateConfluenceData(currentPrice, isBullish); + EntryScoreStruct es = CalculateEntryScore(isBullish, currentPrice, cd, cp, 0); + g_lastEntryScore = es; + if(!MeetsMinimumEntryScore(es)) return; + } + + + // Add to real execution candidate list + { + double _cSlDist = MathAbs(currentPrice - sl); + double _cTp2 = isBullish ? currentPrice + _cSlDist * g_autoOptParams.tp2_rr + : currentPrice - _cSlDist * g_autoOptParams.tp2_rr; + double _cTp3 = isBullish ? currentPrice + _cSlDist * g_autoOptParams.tp3_rr + : currentPrice - _cSlDist * g_autoOptParams.tp3_rr; + int _cScore = (InpEnableScoring && g_lastEntryScore.totalScore > 0) + ? g_lastEntryScore.totalScore : (int)(quality * 85); + AddCandidate(isBullish, "MEAN_REV", TECH_MEAN_REV, -1, + currentPrice, sl, tp, _cTp2, _cTp3, _cScore); + } + CreateSignal(time[0], currentPrice, sl, tp, isBullish, "MEAN_REV", + confluence, quality, 0); +} + +//+------------------------------------------------------------------+ +//| Check ML Signal (v5.0 Enhanced) | +//+------------------------------------------------------------------+ +void CheckMLSignal(const datetime &time[], const double &open[], + const double &high[], const double &low[], + const double &close[], const long &volume[], + double currentPrice) +{ + if(!g_nnTrained || ArraySize(ML_Predictions) == 0) return; + // Get latest prediction + ML_Prediction lastPred = ML_Predictions[ArraySize(ML_Predictions) - 1]; + // Only proceed if confidence is above threshold + if(lastPred.confidence < MLConfidenceThreshold) return; + // Skip neutral predictions + if(lastPred.category == "NEUTRAL") return; + bool isBullish = (lastPred.category == "BULLISH"); + // Check structure alignment + bool structureAligned = (isBullish && g_isBullishStructure) || + (!isBullish && !g_isBullishStructure); + // Check PD zone alignment + bool pdAligned = (isBullish && g_currentPDZone == "DISCOUNT") || + (!isBullish && g_currentPDZone == "PREMIUM"); + // Calculate base confluence + double confluence = 0.3; // Base ML confluence + if(structureAligned) confluence += 0.2; + if(pdAligned) confluence += 0.15; + // Add Killzone bonus (v5.0) + if(g_isInKillzone && g_currentKillzoneIndex >= 0) + { + confluence += GetKillzoneQualityBonus(g_killzoneDefinitions[g_currentKillzoneIndex].type); + } + // Check for FVG/OB confluence + int fvgIdx, obIdx; + if(IsPriceInFVG(currentPrice, fvgIdx, isBullish)) + { + confluence += 0.15; + } + if(IsPriceNearOB(currentPrice, obIdx, isBullish)) + { + confluence += 0.15; + } + // Minimum confluence check + if(confluence < g_workingMinConfluence) return; + // Calculate entry quality + double quality = CalculateEntryQuality(currentPrice, isBullish, "ML_SIGNAL", confluence); + if(quality < g_workingMinEntryQuality) return; + // Calculate SL/TP + double atr = g_cachedATR; + double sl, tp; + if(isBullish) + { + sl = currentPrice - atr * g_workingSL_ATRMultiplier; + tp = currentPrice + atr * g_workingTP_ATRMultiplier; + } + else + { + sl = currentPrice + atr * g_workingSL_ATRMultiplier; + tp = currentPrice - atr * g_workingTP_ATRMultiplier; + } + // Calculate RR + double rr = MathAbs(tp - currentPrice) / MathAbs(currentPrice - sl); + if(rr < g_workingMinRiskReward) return; + // Create signal + + // Add to real execution candidate list + { + double _cSlDist = MathAbs(currentPrice - sl); + double _cTp2 = isBullish ? currentPrice + _cSlDist * g_autoOptParams.tp2_rr + : currentPrice - _cSlDist * g_autoOptParams.tp2_rr; + double _cTp3 = isBullish ? currentPrice + _cSlDist * g_autoOptParams.tp3_rr + : currentPrice - _cSlDist * g_autoOptParams.tp3_rr; + int _cScore = (InpEnableScoring && g_lastEntryScore.totalScore > 0) + ? g_lastEntryScore.totalScore : (int)(quality * 85); + AddCandidate(isBullish, "ML_SIGNAL", TECH_FVG, -1, + currentPrice, sl, tp, _cTp2, _cTp3, _cScore); + } + CreateSignal(time[0], currentPrice, sl, tp, isBullish, "ML_SIGNAL", + confluence, quality, lastPred.confidence); +} +//+------------------------------------------------------------------+ +//| Check Killzone Signal (v5.0 NEW) | +//+------------------------------------------------------------------+ +void CheckKillzoneSignal(const datetime &time[], const double &high[], + const double &low[], const double &close[], + double currentPrice) +{ + if(!g_isInKillzone || g_currentKillzoneIndex < 0) return; + // Direct access without GetPointer + if(!g_activeKillzones[g_currentKillzoneIndex].isActive) return; + // * v9.12 FIX: Ensure ATR is valid before using it in SL calculations. + // g_cachedATR can be 0 when CheckKillzoneSignal runs before UpdateCachedATR on a new bar. + // If ATR=0: maxSLdist=0 -> sl = currentPrice - 0 = currentPrice -> SL AT ENTRY -> broken trade. + // Fix: refresh ATR locally if g_cachedATR is zero or suspiciously large. + if(g_cachedATR <= 0 || (g_cachedATR / g_pipValue) > 50.0) + { + // Refresh from handle + if(g_atrHandle != INVALID_HANDLE) + { + double _atrBuf[]; + ArrayResize(_atrBuf, 1); + ArraySetAsSeries(_atrBuf, true); + if(CopyBuffer(g_atrHandle, 0, 0, 1, _atrBuf) > 0 && _atrBuf[0] > 0) + g_cachedATR = _atrBuf[0]; + } + // If still invalid, skip -- do not trade with unknown ATR + if(g_cachedATR <= 0 || (g_cachedATR / g_pipValue) > 50.0) + { + if(g_verboseLog) + Print("* v9.12 KZ SKIP: g_cachedATR invalid (", DoubleToString(g_cachedATR / g_pipValue, 1), "p) -- skipping killzone signal to avoid bad SL"); + return; + } + } + // Need some range development + if(g_activeKillzones[g_currentKillzoneIndex].range < g_cachedATR * 0.3) return; + // Check for breakout setup + if(!g_activeKillzones[g_currentKillzoneIndex].breakoutOccurred) return; + bool isBullish = (g_activeKillzones[g_currentKillzoneIndex].direction > 0); + // Check structure alignment + bool structureAligned = (isBullish && g_isBullishStructure) || + (!isBullish && !g_isBullishStructure); + // * v7.4 FIX: Removed hard return -- use confluence penalty instead + // Was: if(!structureAligned) return; + // Calculate confluence + double confluence = 0.4; // Base killzone confluence + if(!structureAligned) confluence -= 0.15; // * v7.4: Soft penalty for counter-structure + confluence += GetKillzoneQualityBonus(g_activeKillzones[g_currentKillzoneIndex].type); + // Check for FVG confluence + int fvgIdx; + if(IsPriceInFVG(currentPrice, fvgIdx, isBullish)) + { + confluence += 0.15; + } + // Check for OB confluence + int obIdx; + if(IsPriceNearOB(currentPrice, obIdx, isBullish)) + { + confluence += 0.15; + } + if(confluence < g_workingMinConfluence) return; + // Calculate entry quality + double quality = CalculateEntryQuality(currentPrice, isBullish, "KILLZONE_ENTRY", confluence); + if(quality < g_workingMinEntryQuality) return; + // Calculate SL/TP using killzone range + double sl, tp; + if(isBullish) + { + sl = g_activeKillzones[g_currentKillzoneIndex].low - g_cachedATR * 0.2; + // * v9.11 FIX#25: Cap SL distance at 3xATR + // Evidence: BUY SL stuck at ~1.1602 (Jan low) for entire test -> 200-418 pip SL! + // Cause: killzone .low tracks absolute session low, not recent swing + // KZ SL is anchored to the session low which can be far from price. + // Cap relative to ATR so TP1 stays reachable on intraday timeframes. + double _kzSlCapMult = (_Period >= PERIOD_H4) ? 2.5 : 1.5; + double maxSLdist = g_cachedATR * _kzSlCapMult; + if((currentPrice - sl) > maxSLdist) + { + if(g_verboseLog) + PrintFormat("* KZ BUY SL capped: %.5f -> %.5f (%.1fxATR=%.1fp)", + sl, currentPrice - maxSLdist, _kzSlCapMult, maxSLdist / g_pipValue); + sl = currentPrice - maxSLdist; + } + tp = currentPrice + (currentPrice - sl) * g_workingMinRiskReward; + } + else + { + sl = g_activeKillzones[g_currentKillzoneIndex].high + g_cachedATR * 0.2; + // * v9.11 FIX#25: Cap SL distance at 3xATR + // Cap relative to ATR so TP1 stays reachable on intraday timeframes. + double _kzSlCapMult2 = (_Period >= PERIOD_H4) ? 2.5 : 1.5; + double maxSLdist = g_cachedATR * _kzSlCapMult2; + if((sl - currentPrice) > maxSLdist) + { + if(g_verboseLog) + PrintFormat("* KZ SELL SL capped: %.5f -> %.5f (%.1fxATR=%.1fp)", + sl, currentPrice + maxSLdist, _kzSlCapMult2, maxSLdist / g_pipValue); + sl = currentPrice + maxSLdist; + } + tp = currentPrice - (sl - currentPrice) * g_workingMinRiskReward; + } + // ML confidence (if available) + double mlConf = 0; + if(g_nnTrained && ArraySize(ML_Predictions) > 0) + { + ML_Prediction lastPred = ML_Predictions[ArraySize(ML_Predictions) - 1]; + if((isBullish && lastPred.category == "BULLISH") || + (!isBullish && lastPred.category == "BEARISH")) + { + mlConf = lastPred.confidence; + } + } + // Create signal + + // Add to real execution candidate list + { + double _cSlDist = MathAbs(currentPrice - sl); + double _cTp2 = isBullish ? currentPrice + _cSlDist * g_autoOptParams.tp2_rr + : currentPrice - _cSlDist * g_autoOptParams.tp2_rr; + double _cTp3 = isBullish ? currentPrice + _cSlDist * g_autoOptParams.tp3_rr + : currentPrice - _cSlDist * g_autoOptParams.tp3_rr; + int _cScore = (InpEnableScoring && g_lastEntryScore.totalScore > 0) + ? g_lastEntryScore.totalScore : (int)(quality * 85); + AddCandidate(isBullish, "KILLZONE_ENTRY", TECH_SILVER_BULLET, -1, + currentPrice, sl, tp, _cTp2, _cTp3, _cScore); + } + CreateSignal(time[0], currentPrice, sl, tp, isBullish, "KILLZONE_ENTRY", + confluence, quality, mlConf); +} +//+------------------------------------------------------------------+ +//| Create Signal (v5.0 + PAIR OPTIMIZATION + COST ANALYSIS) OPTIMIZED| +//+------------------------------------------------------------------+ +void CreateSignal(datetime signalTime, double entryPrice, double sl, double tp, + bool isBullish, string strategy, double confluence, + double quality, double mlConfidence) +{ + // =============================================================== + // [TARGET] PAIR OPTIMIZATION CHECK + // =============================================================== + if(confluence < GetOptimalMinConfluence()) + { + if(g_verboseLog) Print("[X] Signal rejected: Confluence ", confluence, + " < required ", GetOptimalMinConfluence()); + return; + } + if(PAIR_SessionFilter && !IsInOptimalSession()) + { + if(g_verboseLog) Print("[WARN] Signal outside optimal session for ", _Symbol); + } + // =============================================================== + // [MONEY] COST ANALYSIS VALIDATION + // Use effective SL (with sl_min floor) — raw zone SL may be tiny (2-3p) + // which would inflate cost% and reject valid trades. + // =============================================================== + double sl_effective = sl; + if(g_workingSL_MinPips > 0) + { + double sl_min_price = g_workingSL_MinPips * g_pipValue; + sl_effective = isBullish ? MathMin(sl, entryPrice - sl_min_price) + : MathMax(sl, entryPrice + sl_min_price); + } + double lotSize = g_minLot; + if(EnableRiskMgmt) + { + lotSize = CalculatePositionSize(entryPrice, sl_effective); + } + if(EnableCostAnalysis && g_costAnalysisEnabled) + { + TradingCosts costs = CalculateFullTradingCosts(lotSize, entryPrice, sl_effective, isBullish); + if(!costs.isCostAcceptable) + { + if(g_verboseLog) Print("[X] Signal rejected due to costs: ", costs.rejectReason); + if(COST_RejectHighCost) return; + } + if(COST_AdjustTP && costs.breakEvenPoints > 0) + { + double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); + double tpAdjustment = costs.breakEvenPoints * point; + if(isBullish) { + tp += tpAdjustment; + } else { + tp -= tpAdjustment; + } + if(g_verboseLog) Print("[CHART] TP adjusted for costs. Break-even: ", + DoubleToString(costs.breakEvenPoints, 1), " points"); + } + g_lastTradeCosts = costs; + } + // =============================================================== + // CHECK FOR DUPLICATE SIGNALS - OPTIMIZED (last 20 only) + // =============================================================== + int size = ArraySize(SIGNAL_Array); + int checkLimit = MathMin(size, 20); + for(int i = size - 1; i >= size - checkLimit && i >= 0; i--) + { + if(SIGNAL_Array[i].strategy == strategy && + MathAbs(SIGNAL_Array[i].entryPrice - entryPrice) < g_cachedATR * 0.2 && + SIGNAL_Array[i].status == "PENDING") + { + return; // Similar signal already exists + } + } + // =============================================================== + // CREATE NEW SIGNAL + // =============================================================== + SIGNAL_Struct signal; + signal.id = ++g_signalIdCounter; + signal.time = signalTime; + signal.entryPrice = entryPrice; + signal.stopLoss = sl; + signal.takeProfit = tp; + signal.isBullish = isBullish; + signal.strategy = strategy; + signal.confluence = confluence; + signal.entryQuality = quality; + signal.status = "PENDING"; + // [v6.42] Use hours if set, otherwise bars + if(SignalExpiryHours > 0) + signal.expiryTime = signalTime + SignalExpiryHours * 3600; + else + signal.expiryTime = signalTime + PeriodSeconds() * g_workingSignalExpiryBars; + signal.riskReward = MathAbs(tp - entryPrice) / MathAbs(entryPrice - sl); + signal.marketPhase = g_currentPhase; + signal.pdZone = g_currentPDZone; + signal.structureBias = g_isBullishStructure ? "BULLISH" : "BEARISH"; + // v5.0 Enhancements + signal.killzone = g_isInKillzone ? g_killzoneDefinitions[g_currentKillzoneIndex].type : KZ_NONE; + signal.mlConfidence = mlConfidence; + signal.structureAligned = (isBullish == g_isBullishStructure); + signal.pdZoneAligned = (isBullish && g_currentPDZone == "DISCOUNT") || + (!isBullish && g_currentPDZone == "PREMIUM"); + signal.lotSize = lotSize; + // =============================================================== + // ADD TO ARRAY - OPTIMIZED with max capacity & reserve + // =============================================================== + if(size >= MAX_SIGNAL_ARRAY) + { + // Remove oldest signal + for(int i = 0; i < size - 1; i++) + { + SIGNAL_Array[i] = SIGNAL_Array[i + 1]; + } + size--; + ArrayResize(SIGNAL_Array, size, 20); + } + ArrayResize(SIGNAL_Array, size + 1, 20); // Reserve 20 extra slots + SIGNAL_Array[size] = signal; + g_activeSignalCount++; + // [v6.42] Signal alert system + if(Signal_AlertNew) + { + string alertMsg = StringFormat("%s: New %s signal - %s | Entry: %s", + _Symbol, signal.isBullish ? "BUY" : "SELL", signal.strategy, + DoubleToString(signal.entryPrice, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS))); + bool isHighQuality = (AlertOnHighQuality && signal.entryQuality >= HighQualityThreshold); + if(EnableSoundAlerts && (isHighQuality || !AlertOnHighQuality)) + PlaySound("alert.wav"); + if(EnablePushNotifications && (isHighQuality || !AlertOnHighQuality)) + SendNotification(alertMsg); + if(EnableEmailAlerts && (isHighQuality || !AlertOnHighQuality)) + SendMail("Signal: " + _Symbol, alertMsg); + } + g_lastSignalCreated = signalTime; + // =============================================================== + // DRAW SIGNAL ON CHART + // =============================================================== + if(ShowSignals) + { + DrawSignal(signal); + } + // =============================================================== + // ALERT — VISUAL ONLY (CreateSignal path — no real trade execution) + // Real trades go through EA_CheckSignals → SelectBestCandidate → AddMultiTPEntry + // =============================================================== + if(EnableAlerts) + { + string kzInfo = (signal.killzone != KZ_NONE) ? + StringFormat(" [%s]", g_currentKillzoneName) : ""; + string pairInfo = ""; + if(PAIR_OptimizationEnabled && g_gates.computed) { + pairInfo = StringFormat(" (%s)", g_autoOptParams.pair_category); + } + string costInfo = ""; + if(EnableCostAnalysis && g_lastTradeCosts.totalCost > 0) { + costInfo = StringFormat(" | Cost: $%.2f (%.1f%%)", + g_lastTradeCosts.totalCost, + g_lastTradeCosts.costAsPercentOfSL); + } + string alertMsg = StringFormat("[VISUAL] %s Signal: %s%s%s @ %.5f | SL: %.5f | TP: %.5f | RR: %.2f | Quality: %.0f%%%s", + isBullish ? "BUY" : "SELL", + strategy, kzInfo, pairInfo, + entryPrice, sl, tp, + signal.riskReward, quality, costInfo); + if(g_verboseLog) Print(alertMsg); + // Note: Alert() suppressed — CreateSignal is visual tracking only. + // Real trade alerts come from EA_CheckSignals → SelectBestCandidate path. + } + // =============================================================== + // WRITE TO JOURNAL + // =============================================================== + if(EnableTradeJournal && JournalLogAllSignals) + { + WriteToJournal("SIGNAL", signal, "New signal generated"); + } + // =============================================================== + // ADD TO BACKTEST IF ACTIVE + // =============================================================== + if(g_isBacktesting) + { + AddBacktestTrade(signal, signalTime, entryPrice); + } +} +//+------------------------------------------------------------------+ +//| Calculate Entry Quality (v5.0 Enhanced) | +//+------------------------------------------------------------------+ +double CalculateEntryQuality(double price, bool isBullish, string strategy, double confluence) +{ + double quality = 50.0; // Base quality + // =============================================================== + // STRUCTURE ALIGNMENT (+15) + // =============================================================== + if((isBullish && g_isBullishStructure) || (!isBullish && !g_isBullishStructure)) + { + quality += 15.0; + } + // =============================================================== + // PREMIUM/DISCOUNT ZONE (+10) + // =============================================================== + if((isBullish && g_currentPDZone == "DISCOUNT") || + (!isBullish && g_currentPDZone == "PREMIUM")) + { + quality += 10.0; + } + // =============================================================== + // RSI CONFIRMATION (+10) + // =============================================================== + if(g_workingUseRSIFilter) + { + if(isBullish && g_cachedRSI < 40) + { + quality += 10.0; + } + else if(!isBullish && g_cachedRSI > 60) + { + quality += 10.0; + } + } + // =============================================================== + // CONFLUENCE BONUS (+0-15) + // =============================================================== + quality += confluence * 15.0; + // =============================================================== + // KILLZONE BONUS (v5.0) (+0-10) + // =============================================================== + if(g_isInKillzone && g_currentKillzoneIndex >= 0) + { + double kzBonus = GetKillzoneQualityBonus(g_killzoneDefinitions[g_currentKillzoneIndex].type); + quality += kzBonus * 50.0; // Scale to quality points + // Extra bonus for Silver Bullet + if(g_killzoneDefinitions[g_currentKillzoneIndex].type == KZ_SILVER_BULLET_LDN || + g_killzoneDefinitions[g_currentKillzoneIndex].type == KZ_SILVER_BULLET_NY_AM || + g_killzoneDefinitions[g_currentKillzoneIndex].type == KZ_SILVER_BULLET_NY_PM) + { + quality += 5.0; + } + } + // =============================================================== + // ML CONFIRMATION (v5.0) (+0-10) + // =============================================================== + if(g_nnTrained && ArraySize(ML_Predictions) > 0) + { + ML_Prediction lastPred = ML_Predictions[ArraySize(ML_Predictions) - 1]; + if((isBullish && lastPred.category == "BULLISH") || + (!isBullish && lastPred.category == "BEARISH")) + { + quality += lastPred.confidence * 10.0; + } + else if((isBullish && lastPred.category == "BEARISH") || + (!isBullish && lastPred.category == "BULLISH")) + { + quality -= 10.0; // Penalty for conflicting ML + } + } + // =============================================================== + // HIGHER TIMEFRAME CONFIRMATION (+10) + // =============================================================== + if(EnableHTFConfirmation) + { + ENUM_TIMEFRAMES htf = GetHigherTimeframe(_Period); + bool htfBias = GetTimeframeBias(htf); + if(htfBias == isBullish) + { + quality += 10.0; + } + } + // =============================================================== + // * v10.29 FIX#316: ENHANCED per-strategy per-regime quality adjustment + // OLD: global WR ±5 pts (ignored which regime we're in) + // NEW: regime-specific WR ±12 pts (BOS_RETEST 100% WR in trending = +12!) + // =============================================================== + int stratIdx = GetStrategyIndex(strategy); + if(stratIdx >= 0 && g_strategyPerf[stratIdx].totalTrades >= 10) + { + // First check regime-specific WR (more accurate — needs 5+ trades in this regime) + int rIdx316 = -1; + if(g_regimeValid) + { + ENUM_MARKET_REGIME reg316 = g_regimeData.regime; + if(reg316==REGIME_STRONG_TREND_UP||reg316==REGIME_STRONG_TREND_DOWN|| + reg316==REGIME_TREND_UP||reg316==REGIME_TREND_DOWN) rIdx316=0; + else if(reg316==REGIME_RANGING||reg316==REGIME_RANGING_TIGHT|| + reg316==REGIME_RANGING_WIDE) rIdx316=1; + else if(reg316==REGIME_CHOPPY) rIdx316=2; + else if(reg316==REGIME_WEAK_TREND_UP||reg316==REGIME_WEAK_TREND_DOWN) rIdx316=3; + else if(reg316==REGIME_VOLATILE) rIdx316=4; + else if(reg316==REGIME_BREAKOUT) rIdx316=5; + } + bool usedRegimeWR = false; + if(rIdx316 >= 0 && g_strategyPerf[stratIdx].total_regime[rIdx316] >= 5) + { + double regWR = g_strategyPerf[stratIdx].wr_regime[rIdx316]; + // Graduated scale: 80%+ WR = +12, 70%+ = +8, 60%+ = +4, 40%- = -4, 30%- = -8, 20%- = -12 + if (regWR >= 80) quality += 12.0; + else if(regWR >= 70) quality += 8.0; + else if(regWR >= 60) quality += 4.0; + else if(regWR <= 20) quality -= 12.0; + else if(regWR <= 30) quality -= 8.0; + else if(regWR <= 40) quality -= 4.0; + usedRegimeWR = true; + } + if(!usedRegimeWR) + { + // Fallback: global WR (less precise but still useful) + if(g_strategyPerf[stratIdx].winRate > 65) quality += 5.0; + else if(g_strategyPerf[stratIdx].winRate < 35) quality -= 5.0; + } + } + // =============================================================== + // KILLZONE HISTORICAL WIN RATE ADJUSTMENT (v5.0) + // =============================================================== + if(g_isInKillzone && g_currentKillzoneIndex >= 0) + { + double kzWinRate = GetKillzoneWinRate(g_currentKillzoneIndex); + if(kzWinRate > 0 && g_kzTradesCount[g_currentKillzoneIndex] >= 10) + { + if(kzWinRate > 60) quality += 5.0; + else if(kzWinRate < 40) quality -= 5.0; + } + } + // Clamp to 0-100 + quality = MathMax(0, MathMin(100, quality)); + return quality; +} +//+------------------------------------------------------------------+ +//| Perform Maintenance Tasks | +//+------------------------------------------------------------------+ +void PerformMaintenanceTasks(datetime currentTime) +{ + // =============================================================== + // CLEANUP OLD OBJECTS + // =============================================================== + if((currentTime - g_lastObjectCleanup) > OBJECT_CLEANUP_INTERVAL) + { + CleanupOldObjects(); + g_lastObjectCleanup = currentTime; + } + // =============================================================== + // COMPACT ARRAYS + // =============================================================== + if((currentTime - g_lastArrayCompact) > ARRAY_COMPACT_INTERVAL) + { + CompactArrays(); + g_lastArrayCompact = currentTime; + } + // =============================================================== + // SAVE PERFORMANCE DATA (v5.0) + // =============================================================== + if(EnableDataPersistence && AutoSaveInterval > 0) + { + if((currentTime - g_lastPerfSave) > PERSISTENCE_SAVE_INTERVAL) + { + SavePerformanceData(); + g_lastPerfSave = currentTime; + } + } + // =============================================================== + // DAILY RESET + // =============================================================== + // * FIX#411: Use TimeGMT() for day boundary — matches OnNewBar which also uses TimeGMT(). + // ROOT CAUSE: TimeCurrent() = broker local (GMT+2). OnNewBar uses TimeGMT(). + // Window 22:00-00:00 broker (= 20:00-22:00 UTC): PerformMaintenanceTasks reset g_tradesThisDay + // 2 hours before OnNewBar reset g_ea_stats.trades → counters async → trades miscounted. + static int lastDay = -1; + MqlDateTime dt; + TimeToStruct(TimeGMT(), dt); + if(dt.day != lastDay) + { + lastDay = dt.day; + g_tradesThisDay = 0; + g_dailyLoss = 0; + g_dailyProfit = 0; + // * FIX#404: Reset consecutive loss streak daily so FIX#398 HALT doesn't become permanent. + // BUG: FIX#398 comment says "resets at next daily DD reset" but g_currentStreak was + // never reset here. Result: 4 consecutive losses → HALT fires every bar forever. + // kavala v10.67 backtest: last trade Jan 16 2024, then 15 months = 0 trades. + // FIX: If g_currentStreak is negative (loss streak), reset to 0 at daily boundary. + // Win streaks (positive) are preserved — no reason to penalise winning days. + // g_lossStreakForMTF also reset: it feeds score penalty (-15/-25pts) and should + // not carry over across days (each day is a fresh session). + if(g_currentStreak < 0) + { + PrintFormat("[FIX#404] Daily reset: lossStreak %d → 0 | FIX#398 HALT cleared for new day", + g_currentStreak); + g_currentStreak = 0; + g_lossStreakForMTF = 0; + } + if(EnableKillzones) + { + InitializeDSTInfo(); + } + } + // =============================================================== + // MEMORY MONITORING + // =============================================================== + if(g_verboseLog && LogMemoryUsage && (currentTime - g_lastMemoryLog) > MemoryLogInterval) // [v6.42] + { + // [v6.42] ShowProcessingTime + if(LogScreenshots && EnableDebugMode) + ChartScreenShot(0, "VpowerEA_" + TimeToString(TimeCurrent(), TIME_DATE) + ".png", 1920, 1080); + if(ShowProcessingTime) + Print("[TIMER] Processing time: ", GetTickCount() - g_tickStart, " ms"); + double memUsage = CalculateMemoryUsage(); + PrintFormat("[CHART] Memory Usage: %.2f KB", memUsage); + g_lastMemoryLog = currentTime; + } + // [v6.42] HEATMAP_ShowLegend + // Legend drawing is handled in UpdateProbabilityHeatmap + // [v6.42] Array compact using ArrayCompactInterval input + static datetime s_lastArrayCompact = 0; + if(ArrayCompactInterval > 0 && (currentTime - s_lastArrayCompact) > ArrayCompactInterval) + { + // Cap FVG array + if(ArraySize(FVG_Array) > MaxFVGsToKeep) + { + ArrayResize(FVG_Array, MaxFVGsToKeep); + g_fvgCount = ArraySize(FVG_Array); // * v7.4 FIX: sync count after cap + } + // Cap OB array + if(ArraySize(OB_Array) > MaxOBsToKeep) + { + ArrayResize(OB_Array, MaxOBsToKeep); + g_obCount = ArraySize(OB_Array); // * v7.4 FIX: sync count after cap + } + // Cap Signal array + if(ArraySize(SIGNAL_Array) > MaxSignalsToKeep) + ArrayResize(SIGNAL_Array, MaxSignalsToKeep); + // Cap Liquidity array + if(ArraySize(LIQ_Array) > MaxLiquidityLevels) + ArrayResize(LIQ_Array, MaxLiquidityLevels); + s_lastArrayCompact = currentTime; + } +} +//+------------------------------------------------------------------+ +//| OnDeinit - Cleanup v5.1 WITH COMPLETE OBJECT CLEANUP | +//| [OK] Ολοκληρωμένος καθαρισμός ΟΛΩΝ των objects και arrays | +//| [OK] Διαγραφή FVGs, OBs, Trendlines, Silver Bullets | +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) +{ + // FIX#503b: release cached ADX handle + if(g_adxHandleExhaust != INVALID_HANDLE) + { + IndicatorRelease(g_adxHandleExhaust); + g_adxHandleExhaust = INVALID_HANDLE; + } + // [ML] Cleanup Auto-Optimization + DeinitAutoOptimization(); + Print("==========================================================="); + Print("[STOP] EA ANGEL " + EA_VERSION + " - DEINITIALIZING..."); + Print("==========================================================="); + // [TIMER] KILL TIMER FIRST + EventKillTimer(); + // [v6.42] Clean up prediction and heatmap objects + ObjectDelete(0, "ICT_ML_PredLine"); + ObjectDelete(0, "ICT_ML_ConfLabel"); + ObjectDelete(0, "ICT_ML_UpperBand"); + ObjectDelete(0, "ICT_ML_LowerBand"); + ObjectDelete(0, "ICT_ML_Arrow"); + ObjectDelete(0, "ICT_ML_Target"); + ObjectDelete(0, "ICT_ML_TargetLabel"); + for(int hm = 0; hm < 50; hm++) + ObjectDelete(0, "ICT_Heatmap_" + IntegerToString(hm)); + // SAVE FINAL DATA + if(EnableDataPersistence && g_perfDataLoaded) + { + Print("[SAVE] Saving final performance data..."); + SavePerformanceData(); + ExportTradeHistory(); + if(EnableML && g_nnTrained) + { + ExportMLHistory(); + } + } + // * v9.31 FIX#116: Flush open entry groups before deinit + // Without this, last trades are never counted -> g_totalHistoricalTrades=0 + ForceFlushAllEntryGroups(); + // FINALIZE BACKTEST + if(g_isBacktesting) + { + FinalizeBacktest(); + } + // SAVE NEURAL NETWORK + if(EnableML && g_nnTrained && ML_SaveModel) + { + Print("[BOT] Saving Neural Network model..."); + SaveNeuralNetwork(); + } + // CLOSE FILE HANDLES + if(g_journalFileHandle != INVALID_HANDLE) + { + FileClose(g_journalFileHandle); + g_journalFileHandle = INVALID_HANDLE; + } + if(g_perfFileHandle != INVALID_HANDLE) + { + FileClose(g_perfFileHandle); + g_perfFileHandle = INVALID_HANDLE; + } + // RELEASE INDICATOR HANDLES + if(g_maHandle != INVALID_HANDLE) + { + IndicatorRelease(g_maHandle); + g_maHandle = INVALID_HANDLE; + } + if(g_atrHandle != INVALID_HANDLE) + { + IndicatorRelease(g_atrHandle); + g_atrHandle = INVALID_HANDLE; + } + if(g_rsiHandle != INVALID_HANDLE) + { + IndicatorRelease(g_rsiHandle); + g_rsiHandle = INVALID_HANDLE; + } + // * v7.8: Release TC EMA handles + if(g_tcEmaFastHandle != INVALID_HANDLE) { IndicatorRelease(g_tcEmaFastHandle); g_tcEmaFastHandle = INVALID_HANDLE; } + if(g_tcEmaSlowHandle != INVALID_HANDLE) { IndicatorRelease(g_tcEmaSlowHandle); g_tcEmaSlowHandle = INVALID_HANDLE; } + if(g_tcRSIHandle != INVALID_HANDLE) { IndicatorRelease(g_tcRSIHandle); g_tcRSIHandle = INVALID_HANDLE; } // * v9.16 FIX#48 + // Release Divergence RSI handle + if(g_rsiDivergenceHandle != INVALID_HANDLE) + { + IndicatorRelease(g_rsiDivergenceHandle); + g_rsiDivergenceHandle = INVALID_HANDLE; + } + // DESTROY NEURAL NETWORK + if(g_nnInitialized) + { + DestroyNeuralNetwork(); + } + // =============================================================== + // [CLEAN] PHASE 1: COMPREHENSIVE CLEANUP OF ALL ICT STRUCTURES + // Διαγραφή ΟΛΩΝ των FVGs, OBs, Trendlines, Silver Bullets + // =============================================================== + Print("[CLEAN] Phase 1: Cleaning up all ICT structures..."); + // * v8.07: delete standalone objects not covered by prefix-based cleanup + ObjectDelete(0, "SessionSL_Label"); + ObjectDelete(0, "AMD_Phase_Label"); + ObjectDelete(0, "AMD_RANGE"); + CleanupAllObjects(); + // =============================================================== + // [CLEAN] PHASE 2: CLEANUP ADVANCED ICT MODULES + // =============================================================== + Print("[CLEAN] Phase 2: Cleaning up advanced modules..."); + // Cleanup Advanced ICT Modules + CleanupCRT(); + CleanupTBS(); + CleanupJudasSwings(); + CleanupDivergences(); + CleanupTrendlines(); + // Cleanup VSA & MTF + CleanupVSA(); + CleanupMTF(); + // Cleanup Killzones & Equity Curve + if(EnableKillzones) + { + DeleteKillzoneObjects(); + } + if(ShowEquityCurve) + { + DeleteEquityCurveObjects(); + } + // * v8.06: Cleanup any remaining equity curve objects by prefix + ObjectsDeleteAll(0, "ICT_BT_Equity_"); + ObjectsDeleteAll(0, "ICT_BT_Peak"); + // Cleanup Dashboard + CleanupDashboard(); + // DELETE CONTROL BUTTONS + if(g_buttonsCreated) + { + DeleteControlButtons(); + g_buttonsCreated = false; + } + // Cleanup Multi-TP Module + if(InpEnableMultiTP) + { + DeinitializeMultiTP(); + } + // Cleanup Entry Scoring Module + if(InpEnableScoring) + { + DeinitializeEntryScoring(); + } + // =============================================================== + // [CLEAN] PHASE 3: CLEANUP REMAINING ICT OBJECTS (Safe deletion) + // Αντί για ObjectsDeleteAll που είναι επικίνδυνο + // =============================================================== + Print("[CLEAN] Phase 3: Cleaning up remaining objects..."); + CleanAllRemainingICTObjects(); + // =============================================================== + // [CLEAN] PHASE 4: FREE ALL ARRAYS (After objects are deleted) + // =============================================================== + Print("[CLEAN] Phase 4: Freeing all arrays..."); + FreeAllArrays(); + // =============================================================== + // [CHART] PRINT FINAL SUMMARY + // =============================================================== + // * v7.4 FIX: Only show summary if THIS session actually traded. + // Was: g_perfData.totalTrades > 0 -> showed STALE data from persisted files + if(g_totalHistoricalTrades > 0) + { + Print(GetPerformanceSummary()); + PrintStrategyRanking(); + } + else + { + Print("==============================================="); + if(g_perfData.totalTrades > 0) + Print("[WARN] NO TRADES THIS SESSION (persisted history: ", g_perfData.totalTrades, " trades)"); + else + Print("[WARN] NO TRADES - Check EA configuration"); + Print(" Working Spread Limit: ", DoubleToString(g_workingMaxSpreadPips, 1), " pips"); + Print("==============================================="); + } + ChartRedraw(0); + Print("==========================================================="); + Print("[OK] EA ANGEL " + EA_VERSION + " - DEINITIALIZATION COMPLETE"); + Print(" Reason: ", GetDeinitReasonText(reason)); + Print("==========================================================="); + g_initSuccess = false; +} +//+------------------------------------------------------------------+ +//| * v9.31 FIX#113: OnTester() -- Custom Optimization Criterion | +//| Called by MT5 at end of each optimization pass. MT5 maximizes | +//| the returned value. Score=0 filters useless/invalid passes. | +//+------------------------------------------------------------------+ +double OnTester() +{ + // * v9.31 FIX#116: Flush any un-closed entry groups + ForceFlushAllEntryGroups(); + int totalTrades = (int)TesterStatistics(STAT_TRADES); + if(totalTrades < 5) return 0.0; + double netProfit = TesterStatistics(STAT_PROFIT); + double grossProfit = TesterStatistics(STAT_GROSS_PROFIT); + double grossLoss = MathAbs(TesterStatistics(STAT_GROSS_LOSS)); + double maxDD_pct = TesterStatistics(STAT_EQUITY_DD_RELATIVE); + double recovFactor = TesterStatistics(STAT_RECOVERY_FACTOR); + double sharpeRatio = TesterStatistics(STAT_SHARPE_RATIO); + double profitFactor = (grossLoss > 0.0) ? (grossProfit / grossLoss) : grossProfit; + int wonTrades = (int)TesterStatistics(STAT_PROFIT_TRADES); + double winRate = (totalTrades > 0) ? (100.0 * wonTrades / totalTrades) : 0.0; + double expPayoff = TesterStatistics(STAT_EXPECTED_PAYOFF); + if(netProfit <= 0.0) return 0.0; + if(profitFactor < 1.0) return 0.0; + if(maxDD_pct > 20.0) return 0.0; + double pfComp = MathMax(0.0, profitFactor - 1.0); + double sharpeComp = MathMax(0.0, sharpeRatio); + double recovComp = MathMax(0.0, recovFactor); + double ddMult = MathMax(0.01, 1.0 - (maxDD_pct / 100.0)); + double tradeBonus = MathLog(MathMax(1, totalTrades)); + double winBonus = winRate / 100.0; + double score = (pfComp * 3.0) + + (sharpeComp * 2.0) + + (recovComp * 1.5) + + (winBonus * 1.0) + + (tradeBonus * 0.5); + score *= ddMult; + if(expPayoff > 0.0) score += MathMin(1.0, expPayoff / 10.0); + PrintFormat("[FIX#113] OnTester %s %s: Trades=%d PF=%.2f Sharpe=%.2f RecF=%.2f DD=%.1f%% WR=%.0f%% -> Score=%.4f", + _Symbol, EnumToString(_Period), totalTrades, + profitFactor, sharpeRatio, recovFactor, maxDD_pct, winRate, score); + return score; +} +//+------------------------------------------------------------------+ +//| * v9.31 FIX#114: OnTesterPass() -- Save best params per pass | +//+------------------------------------------------------------------+ +void OnTesterPass() +{ + double score = TesterStatistics(STAT_CUSTOM_ONTESTER); + if(score <= 0.0) return; + if((int)TesterStatistics(STAT_TRADES) < 5) return; + SaveOptimizedProfile(_Symbol, _Period, score); + PrintFormat("[FIX#114] New best: %s %s Score=%.4f Trades=%d", + _Symbol, EnumToString((ENUM_TIMEFRAMES)_Period), + score, (int)TesterStatistics(STAT_TRADES)); +} +//+------------------------------------------------------------------+ +//| FIX#114: Save current best params to OptProfiles.csv | +//+------------------------------------------------------------------+ +void SaveOptimizedProfile(string symbol, int tf, double score) +{ + string fname = DataFolder + "/OptProfiles.csv"; + bool newFile = !FileIsExist(fname); + int handle = FileOpen(fname, FILE_WRITE | FILE_READ | FILE_CSV | FILE_ANSI | FILE_SHARE_WRITE, ','); + if(handle == INVALID_HANDLE) return; + if(newFile) + FileWrite(handle, + "Symbol","TF","Score","Trades", + "RiskPct","SL_ATR","TP1_ATR","TP2_ATR","TP3_ATR","MinRR","MinScore", + "W_FVG","W_OB","W_Breaker","W_TC","W_Judas", + "Sess_London","Sess_NY","Sess_Asian", + "TrailAct_RR","BE_RR","SmartExit_RR"); + else + FileSeek(handle, 0, SEEK_END); + FileWrite(handle, + symbol, (string)tf, + DoubleToString(score, 4), + (string)(int)TesterStatistics(STAT_TRADES), + DoubleToString(g_autoOptParams.risk_pct, 3), + DoubleToString(g_autoOptParams.sl_atr_mult, 3), + DoubleToString(g_autoOptParams.tp_atr_mult, 3), + DoubleToString(g_autoOptParams.tp_atr_mult * 1.43, 3), + DoubleToString(g_autoOptParams.tp_atr_mult * 1.96, 3), + DoubleToString(g_autoOptParams.min_rr, 3), + (string)g_autoOptParams.min_entry_score, + g_autoOptParams.allow_fvg_entry ? "1" : "0", + g_autoOptParams.allow_ob_entry ? "1" : "0", + g_autoOptParams.allow_breaker_entry ? "1" : "0", + "1","1", + g_autoOptParams.session_london ? "1" : "0", + g_autoOptParams.session_ny ? "1" : "0", + g_autoOptParams.session_asian ? "1" : "0", + DoubleToString(EA_Trail_Activation_RR, 3), // trail_act_rr (direct input) + DoubleToString(g_workingBE_RR, 3), + DoubleToString(0.0, 3)); // smartexit_rr: undeclared, placeholder + FileClose(handle); +} +//+------------------------------------------------------------------+ +//| FIX#114: Load optimized profiles from CSV on startup | +//+------------------------------------------------------------------+ +void LoadOptimizedProfiles() +{ + g_optProfileCount = 0; + g_optProfilesLoaded = false; + string fname = DataFolder + "/OptProfiles.csv"; + if(!FileIsExist(fname)) return; + int handle = FileOpen(fname, FILE_READ | FILE_CSV | FILE_ANSI | FILE_SHARE_READ, ','); + if(handle == INVALID_HANDLE) return; + // Skip header + if(!FileIsEnding(handle)) + for(int col = 0; col < 22; col++) FileReadString(handle); + int loaded = 0; + while(!FileIsEnding(handle) && g_optProfileCount < 50) + { + string sym = FileReadString(handle); + int tf = (int)StringToInteger(FileReadString(handle)); + double score = StringToDouble(FileReadString(handle)); + int trades = (int)StringToInteger(FileReadString(handle)); + double risk = StringToDouble(FileReadString(handle)); + double sl = StringToDouble(FileReadString(handle)); + double tp1 = StringToDouble(FileReadString(handle)); + double tp2 = StringToDouble(FileReadString(handle)); + double tp3 = StringToDouble(FileReadString(handle)); + double minrr = StringToDouble(FileReadString(handle)); + int minsco = (int)StringToInteger(FileReadString(handle)); + int wfvg = (int)StringToInteger(FileReadString(handle)); + int wob = (int)StringToInteger(FileReadString(handle)); + int wbrk = (int)StringToInteger(FileReadString(handle)); + int wtc = (int)StringToInteger(FileReadString(handle)); + int wjud = (int)StringToInteger(FileReadString(handle)); + bool sLon = FileReadString(handle) == "1"; + bool sNY = FileReadString(handle) == "1"; + bool sAsi = FileReadString(handle) == "1"; + double trRR = StringToDouble(FileReadString(handle)); + double beRR = StringToDouble(FileReadString(handle)); + double seRR = StringToDouble(FileReadString(handle)); + if(sym == "" || score <= 0 || trades < 5) continue; + int idx = FindOptimizedProfile(sym, tf); + if(idx >= 0) + { + if(score <= g_optProfiles[idx].score) continue; + } + else { idx = g_optProfileCount; g_optProfileCount++; } + g_optProfiles[idx].symbol = sym; + g_optProfiles[idx].tf = tf; + g_optProfiles[idx].score = score; + g_optProfiles[idx].trades = trades; + g_optProfiles[idx].risk_pct = risk; + g_optProfiles[idx].sl_atr = sl; + g_optProfiles[idx].tp1_atr = tp1; + g_optProfiles[idx].tp2_atr = tp2; + g_optProfiles[idx].tp3_atr = tp3; + g_optProfiles[idx].min_rr = minrr; + g_optProfiles[idx].min_score = minsco; + g_optProfiles[idx].w_fvg = wfvg; + g_optProfiles[idx].w_ob = wob; + g_optProfiles[idx].w_breaker = wbrk; + g_optProfiles[idx].w_tc = wtc; + g_optProfiles[idx].w_judas = wjud; + g_optProfiles[idx].sess_london = sLon; + g_optProfiles[idx].sess_ny = sNY; + g_optProfiles[idx].sess_asian = sAsi; + g_optProfiles[idx].trail_act_rr = trRR; + g_optProfiles[idx].be_rr = beRR; + g_optProfiles[idx].smartexit_rr = seRR; + g_optProfiles[idx].loaded = true; + loaded++; + } + FileClose(handle); + g_optProfilesLoaded = (loaded > 0); + if(g_optProfilesLoaded) + PrintFormat("[FIX#114] Loaded %d optimized profiles from %s", loaded, fname); +} +//+------------------------------------------------------------------+ +//| FIX#114: Find profile index (-1 if not found) | +//+------------------------------------------------------------------+ +int FindOptimizedProfile(string symbol, int tf) +{ + string sym = symbol; + if(StringLen(sym) > 6) sym = StringSubstr(sym, 0, 6); + StringToUpper(sym); + for(int i = 0; i < g_optProfileCount; i++) + { + string ps = g_optProfiles[i].symbol; + if(StringLen(ps) > 6) ps = StringSubstr(ps, 0, 6); + StringToUpper(ps); + if(ps == sym && g_optProfiles[i].tf == tf) return i; + } + return -1; +} +//+------------------------------------------------------------------+ +//| FIX#114: Apply optimized profile to g_autoOptParams if available | +//+------------------------------------------------------------------+ +void ApplyOptimizedProfileToAutoOpt(string symbol, int tf) +{ + if(!g_optProfilesLoaded) return; + int idx = FindOptimizedProfile(symbol, tf); + if(idx < 0) return; + if(!g_optProfiles[idx].loaded || g_optProfiles[idx].trades < 10) return; + // trust weight: score>=3 = full, 1-3 = partial blend + double trust = MathMin(1.0, g_optProfiles[idx].score / 3.0); + if(g_optProfiles[idx].risk_pct > 0) g_autoOptParams.risk_pct = g_autoOptParams.risk_pct *(1-trust) + g_optProfiles[idx].risk_pct *trust; + if(g_optProfiles[idx].sl_atr > 0) g_autoOptParams.sl_atr_mult = g_autoOptParams.sl_atr_mult *(1-trust) + g_optProfiles[idx].sl_atr *trust; + if(g_optProfiles[idx].tp1_atr > 0) g_autoOptParams.tp_atr_mult = g_autoOptParams.tp_atr_mult *(1-trust) + g_optProfiles[idx].tp1_atr *trust; + if(g_optProfiles[idx].min_rr > 0) g_autoOptParams.min_rr = g_autoOptParams.min_rr *(1-trust) + g_optProfiles[idx].min_rr *trust; + if(g_optProfiles[idx].min_score > 0) g_autoOptParams.min_entry_score = (int)MathRound( + g_autoOptParams.min_entry_score*(1-trust) + g_optProfiles[idx].min_score*trust); + if(trust >= 0.7) + { + if(g_optProfiles[idx].w_fvg == 0) g_autoOptParams.allow_fvg_entry = false; + if(g_optProfiles[idx].w_ob == 0) g_autoOptParams.allow_ob_entry = false; + if(g_optProfiles[idx].w_breaker == 0) g_autoOptParams.allow_breaker_entry = false; + } + if(trust >= 0.8) + { + g_autoOptParams.session_london = g_optProfiles[idx].sess_london; + g_autoOptParams.session_ny = g_optProfiles[idx].sess_ny; + g_autoOptParams.session_asian = g_optProfiles[idx].sess_asian; + } + // trail_act_rr optProfile blend removed — ProfitGuard reads EA_Trail_Activation_RR directly + if(g_optProfiles[idx].be_rr > 0) g_workingBE_RR = g_workingBE_RR *(1-trust) + g_optProfiles[idx].be_rr *trust; + // * FIX#115 compile fix: g_workingSmartExitRR removed (undeclared) — smartexit_rr skipped safely + if(g_verboseLog) + PrintFormat("[FIX#114] Profile APPLIED %s %s: Score=%.2f Trust=%.0f%% Trades=%d Risk=%.2f%% SL=%.2f TP=%.2f", + symbol, EnumToString((ENUM_TIMEFRAMES)tf), + g_optProfiles[idx].score, trust*100, g_optProfiles[idx].trades, + g_autoOptParams.risk_pct, g_autoOptParams.sl_atr_mult, g_autoOptParams.tp_atr_mult); +} +//+------------------------------------------------------------------+ +//| [CLEAN] CLEANUP ALL OBJECTS - Master Cleanup Function | +//| Διαγράφει ΟΛΟΚΛΗΡΑ όλα τα FVGs, OBs, Trendlines, SBs | +//+------------------------------------------------------------------+ +void CleanupAllObjects() +{ + Print("[CLEAN] Starting complete cleanup of all indicator objects..."); + int deletedCount = 0; + // =============================================================== + // 1. CLEANUP ALL FVGs + // =============================================================== + if(ArraySize(FVG_Array) > 0) + { + Print(" [INFO] Cleaning ", ArraySize(FVG_Array), " FVGs..."); + for(int i = ArraySize(FVG_Array) - 1; i >= 0; i--) + { + DeleteFVGObjects(i); + deletedCount++; + } + ArrayResize(FVG_Array, 0); + g_fvgCount = 0; // Update count + Print(" [OK] FVGs cleaned"); + } + // =============================================================== + // 2. CLEANUP ALL ORDER BLOCKS + // =============================================================== + if(ArraySize(OB_Array) > 0) + { + Print(" [INFO] Cleaning ", ArraySize(OB_Array), " Order Blocks..."); + for(int i = ArraySize(OB_Array) - 1; i >= 0; i--) + { + DeleteOrderBlockObjects(i); + deletedCount++; + } + ArrayResize(OB_Array, 0); + g_obCount = 0; // Update count + Print(" [OK] Order Blocks cleaned"); + } + // =============================================================== + // 3. CLEANUP ALL TRENDLINES + // =============================================================== + if(g_trendlineCount > 0) + { + Print(" [INFO] Cleaning ", g_trendlineCount, " Trendlines..."); + for(int i = g_trendlineCount - 1; i >= 0; i--) + { + DeleteTrendlineObjectsEnhanced(g_trendlines[i]); + deletedCount++; + } + g_trendlineCount = 0; + Print(" [OK] Trendlines cleaned"); + } + // =============================================================== + // 4. CLEANUP ALL SILVER BULLETS + // =============================================================== + if(ArraySize(g_sbSetups) > 0) + { + Print(" [INFO] Cleaning ", ArraySize(g_sbSetups), " Silver Bullets..."); + for(int i = ArraySize(g_sbSetups) - 1; i >= 0; i--) + { + ObjectDelete(0, g_sbSetups[i].objName); + ObjectDelete(0, g_sbSetups[i].objName + "_L"); + deletedCount++; + } + ArrayResize(g_sbSetups, 0); + Print(" [OK] Silver Bullets cleaned"); + } + // =============================================================== + // 5. CLEANUP LIQUIDITY LEVELS + // =============================================================== + if(ArraySize(LIQ_Array) > 0) + { + Print(" [INFO] Cleaning ", ArraySize(LIQ_Array), " Liquidity Levels..."); + for(int i = ArraySize(LIQ_Array) - 1; i >= 0; i--) + { + string objName = "ICT_LIQ_" + IntegerToString(LIQ_Array[i].id); + ObjectDelete(0, objName); + ObjectDelete(0, objName + "_Label"); + ObjectDelete(0, objName + "_Sweep"); + } + ArrayResize(LIQ_Array, 0); + Print(" [OK] Liquidity Levels cleaned"); + } + // =============================================================== + // 6. CLEANUP BREAKER BLOCKS + // =============================================================== + if(ArraySize(BREAKER_Array) > 0) + { + Print(" [INFO] Cleaning ", ArraySize(BREAKER_Array), " Breaker Blocks..."); + for(int i = ArraySize(BREAKER_Array) - 1; i >= 0; i--) + { + string objName = "ICT_BREAKER_" + IntegerToString(BREAKER_Array[i].id); + ObjectDelete(0, objName); + ObjectDelete(0, objName + "_Label"); + } + ArrayResize(BREAKER_Array, 0); + Print(" [OK] Breaker Blocks cleaned"); + } + // =============================================================== + // 7. CLEANUP MITIGATION BLOCKS + // =============================================================== + if(ArraySize(MITIGATION_Array) > 0) + { + Print(" [INFO] Cleaning ", ArraySize(MITIGATION_Array), " Mitigation Blocks..."); + for(int i = ArraySize(MITIGATION_Array) - 1; i >= 0; i--) + { + string objName = "ICT_MIT_" + IntegerToString(MITIGATION_Array[i].id); + ObjectDelete(0, objName); + ObjectDelete(0, objName + "_Label"); + } + ArrayResize(MITIGATION_Array, 0); + Print(" [OK] Mitigation Blocks cleaned"); + } + // =============================================================== + // 8. CLEANUP OTE ZONES + // =============================================================== + if(ArraySize(OTE_Array) > 0) + { + Print(" [INFO] Cleaning ", ArraySize(OTE_Array), " OTE Zones..."); + for(int i = ArraySize(OTE_Array) - 1; i >= 0; i--) + { + // OTE doesn't have ID, use index-based naming + string objName = "ICT_OTE_" + IntegerToString(i); + ObjectDelete(0, objName); + ObjectDelete(0, objName + "_Label"); + ObjectDelete(0, objName + "_0.618"); + ObjectDelete(0, objName + "_0.705"); + ObjectDelete(0, objName + "_0.79"); + } + ArrayResize(OTE_Array, 0); + Print(" [OK] OTE Zones cleaned"); + } + // =============================================================== + // 9. CLEANUP SIGNALS + // =============================================================== + if(ArraySize(SIGNAL_Array) > 0) + { + Print(" [INFO] Cleaning ", ArraySize(SIGNAL_Array), " Signals..."); + for(int i = ArraySize(SIGNAL_Array) - 1; i >= 0; i--) + { + string objName = "ICT_SIGNAL_" + IntegerToString(SIGNAL_Array[i].id); + ObjectDelete(0, objName); + ObjectDelete(0, objName + "_Entry"); + ObjectDelete(0, objName + "_SL"); + ObjectDelete(0, objName + "_TP1"); + ObjectDelete(0, objName + "_TP2"); + ObjectDelete(0, objName + "_TP3"); + ObjectDelete(0, objName + "_Label"); + } + // [v6.42] AutoCleanExpiredSignals controls whether expired signals are cleaned + ArrayResize(SIGNAL_Array, 0); + Print(" [OK] Signals cleaned"); + } + // =============================================================== + // 10. CLEANUP STRUCTURE POINTS (BOS/CHoCH) + // =============================================================== + if(ArraySize(STRUCT_Array) > 0) + { + Print(" [INFO] Cleaning ", ArraySize(STRUCT_Array), " Structure Points..."); + for(int i = ArraySize(STRUCT_Array) - 1; i >= 0; i--) + { + // STRUCT doesn't have ID, use index-based naming + string objName = "ICT_STRUCT_" + IntegerToString(i); + ObjectDelete(0, objName); + ObjectDelete(0, objName + "_Label"); + } + ArrayResize(STRUCT_Array, 0); + Print(" [OK] Structure Points cleaned"); + } + Print("[OK] Cleanup complete! Deleted ", deletedCount, " structures and all related objects."); +} +//+------------------------------------------------------------------+ +//| [CLEAN] CLEAN ALL REMAINING ICT OBJECTS - Safe Deletion | +//| Διαγράφει οποιοδήποτε απομένει με ασφαλή τρόπο | +//+------------------------------------------------------------------+ +void CleanAllRemainingICTObjects() +{ + int totalObjects = ObjectsTotal(0, 0, -1); + int deletedCount = 0; + // Loop backwards για ασφαλή διαγραφή + for(int i = totalObjects - 1; i >= 0; i--) + { + string objName = ObjectName(0, i, 0, -1); + // * v8.06 FIX: expanded prefix list -- previously missing prefixes left ghost objects on chart + if(StringFind(objName, "ICT_") == 0 || // covers ICT_FVG_, ICT_OB_, ICT_LIQ_, ICT_Dash_, ICT_BT_, etc. + StringFind(objName, "DASH_") == 0 || // professional dashboard panels + StringFind(objName, "FVG_") == 0 || + StringFind(objName, "OB_") == 0 || + StringFind(objName, "TL_") == 0 || + StringFind(objName, "LIQ_") == 0 || + StringFind(objName, "BB_") == 0 || + StringFind(objName, "MB_") == 0 || + StringFind(objName, "OTE_") == 0 || + StringFind(objName, "SIG_") == 0 || + StringFind(objName, "SIGNAL_") == 0 || // * v8.06 added + StringFind(objName, "STRUCT_") == 0 || + StringFind(objName, "KZ_") == 0 || + StringFind(objName, "SB_") == 0 || + StringFind(objName, "CRT_") == 0 || + StringFind(objName, "TBS_") == 0 || + StringFind(objName, "AMD_") == 0 || + StringFind(objName, "JUDAS_") == 0 || + StringFind(objName, "JUDAS") == 0 || // * v8.06 added (no underscore variant) + StringFind(objName, "DIV_") == 0 || + StringFind(objName, "VSA_") == 0 || + StringFind(objName, "MTF_") == 0 || + StringFind(objName, "BTN_") == 0 || + StringFind(objName, "MTP_") == 0 || + StringFind(objName, "SCORE_") == 0 || + StringFind(objName, "CORR_") == 0 || // * v8.06 added: correlation panel objects + StringFind(objName, "CANDLE_") == 0 || // * v8.06 added: candle pattern objects + StringFind(objName, "TC_") == 0 || // * v8.06 added: trend channel objects + StringFind(objName, "PP_") == 0 || // * v8.06 added: pivot point objects + StringFind(objName, "ZONE_") == 0 || // * v8.06 added: PD zone objects + objName == "SessionSL_Label" || // * v8.07: Session SL label + StringFind(objName, "AMD_Phase") == 0 || // * v8.07: AMD phase labels + StringFind(objName, "AMD_RANGE") == 0 || // * v8.07: AMD range objects + StringFind(objName, "AMD_LABEL") == 0) // * v8.07: AMD labels + { + if(ObjectDelete(0, objName)) + deletedCount++; + } + } + if(deletedCount > 0) + Print(" [CLEAN] Deleted ", deletedCount, " remaining ICT objects"); +} +//+------------------------------------------------------------------+ +//| [DEL] FREE ALL ARRAYS - Memory Cleanup | +//| Καθαρίζει τη μνήμη από όλα τα arrays | +//+------------------------------------------------------------------+ +void FreeAllArrays() +{ + // Main ICT Arrays (using correct names from declarations) + ArrayFree(FVG_Array); + ArrayFree(OB_Array); + ArrayFree(g_trendlines); + ArrayFree(g_sbSetups); + ArrayFree(LIQ_Array); + ArrayFree(BREAKER_Array); + ArrayFree(MITIGATION_Array); + ArrayFree(OTE_Array); + ArrayFree(SIGNAL_Array); + ArrayFree(STRUCT_Array); + // Advanced Module Arrays + ArrayFree(g_crtSetups); + ArrayFree(g_tbsSetups); + // g_amdPhases doesn't exist as array - it's g_amdData (single struct) + ArrayFree(g_judasSwings); + ArrayFree(g_divergences); + // VSA & MTF Arrays + ArrayFree(g_vsaPatterns); + ArrayFree(g_tfBiases); // Correct name is g_tfBiases not g_mtfBias + // ML & Performance Arrays + ArrayFree(ML_Predictions); + ArrayFree(g_tradeHistory); + // Multi-TP Arrays + if(MultiTP_Enabled) + { + ArrayFree(g_multiTPEntries); // Correct name + } + // Entry Scoring Arrays + // Note: g_scoredEntries doesn't exist in declarations + // Removed to prevent compilation error + Print(" [DEL] All arrays freed from memory"); +} +//+------------------------------------------------------------------+ +//| [NOTE] GET DEINIT REASON TEXT | +//+------------------------------------------------------------------+ +string GetDeinitReasonText(int reason) +{ + switch(reason) + { + case REASON_PROGRAM: return "Expert removed from chart"; + case REASON_REMOVE: return "Expert removed manually"; + case REASON_RECOMPILE: return "Expert recompiled"; + case REASON_CHARTCHANGE: return "Symbol or timeframe changed"; + case REASON_CHARTCLOSE: return "Chart closed"; + case REASON_PARAMETERS: return "Input parameters changed"; + case REASON_ACCOUNT: return "Account changed"; + case REASON_TEMPLATE: return "New template applied"; + case REASON_INITFAILED: return "Initialization failed"; + case REASON_CLOSE: return "Terminal closed"; + default: return "Unknown reason (" + IntegerToString(reason) + ")"; + } +} +//+------------------------------------------------------------------+ +//| Cleanup Old Objects | +//+------------------------------------------------------------------+ +void CleanupOldObjects() +{ + datetime cutoffTime = TimeCurrent() - ObjectMaxAge * 86400; + int deleted = 0; + int totalObjects = ObjectsTotal(0, 0, -1); + for(int i = totalObjects - 1; i >= 0; i--) + { + string objName = ObjectName(0, i, 0, -1); + if(StringFind(objName, "ICT_") == 0) + { + datetime objTime = (datetime)ObjectGetInteger(0, objName, OBJPROP_TIME); + if(objTime > 0 && objTime < cutoffTime) + { + ObjectDelete(0, objName); + deleted++; + } + } + } + if(deleted > 0 && EnableDebugMode) + { + PrintFormat("[CLEAN] Cleaned up %d old objects", deleted); + } +} +//+------------------------------------------------------------------+ +//| Compact Arrays | +//+------------------------------------------------------------------+ +void CompactArrays() +{ + // Compact FVG array + int fvgCount = ArraySize(FVG_Array); + int validFVG = 0; + for(int i = 0; i < fvgCount; i++) + { + if(FVG_Array[i].status != FVG_STATUS_INVALID && + FVG_Array[i].age < g_workingFVG_MaxAge * 2) + { + if(i != validFVG) + { + FVG_Array[validFVG] = FVG_Array[i]; + } + validFVG++; + } + } + if(validFVG < fvgCount) + { + ArrayResize(FVG_Array, validFVG); + g_fvgCount = validFVG; // Update count + if(g_verboseLog) + { + PrintFormat("[COMPRESS] Compacted FVG array: %d -> %d", fvgCount, validFVG); + } + } + // Similar compaction for other arrays... + // (OB_Array, LIQ_Array, etc.) +} +//+------------------------------------------------------------------+ +//| Calculate Memory Usage | +//+------------------------------------------------------------------+ +double CalculateMemoryUsage() +{ + double totalKB = 0; + totalKB += ArraySize(FVG_Array) * sizeof(FVG_Struct) / 1024.0; + totalKB += ArraySize(OB_Array) * sizeof(OB_Struct) / 1024.0; + totalKB += ArraySize(LIQ_Array) * sizeof(LIQ_Struct) / 1024.0; + totalKB += ArraySize(SIGNAL_Array) * sizeof(SIGNAL_Struct) / 1024.0; + totalKB += ArraySize(ML_Predictions) * sizeof(ML_Prediction) / 1024.0; + totalKB += ArraySize(g_tradeHistory) * sizeof(TradeRecord) / 1024.0; + totalKB += ArraySize(g_backtestTrades) * sizeof(BacktestTrade) / 1024.0; + // Neural network memory + if(g_nnInitialized) + { + for(int l = 0; l < g_neuralNet.numLayers; l++) + { + for(int n = 0; n < g_neuralNet.layers[l].numNeurons; n++) + { + totalKB += ArraySize(g_neuralNet.layers[l].neurons[n].weights) * sizeof(double) / 1024.0; + totalKB += ArraySize(g_neuralNet.layers[l].neurons[n].m_weights) * sizeof(double) / 1024.0; + totalKB += ArraySize(g_neuralNet.layers[l].neurons[n].v_weights) * sizeof(double) / 1024.0; + } + } + } + return totalKB; +} +//+------------------------------------------------------------------+ +//| Print Initialization Summary | +//+------------------------------------------------------------------+ +void PrintInitializationSummary() +{ + Print("-----------------------------------------------------------"); + Print("[LIST] INITIALIZATION SUMMARY:"); + Print("-----------------------------------------------------------"); + PrintFormat(" Symbol: %s", _Symbol); + PrintFormat(" Timeframe: %s", EnumToString(_Period)); + PrintFormat(" Pip Value: %.5f", g_pipValue); + Print("-----------------------------------------------------------"); + Print("[FIX] ENABLED FEATURES:"); + PrintFormat(" FVG: %s | OB: %s | Liquidity: %s", + EnableFVG ? "[OK]" : "[X]", + EnableOB ? "[OK]" : "[X]", + EnableLiquidity ? "[OK]" : "[X]"); + PrintFormat(" Structure: %s | OTE: %s | Breakers: %s", + EnableStructure ? "[OK]" : "[X]", + EnableOTE ? "[OK]" : "[X]", + EnableBreakerBlocks ? "[OK]" : "[X]"); + PrintFormat(" Market Maker: %s | Volume Profile: %s", + EnableMarketMaker ? "[OK]" : "[X]", + EnableVolumeProfile ? "[OK]" : "[X]"); + Print("-----------------------------------------------------------"); + Print("[NEW] v5.0 FEATURES:"); + PrintFormat(" Neural Network ML: %s", EnableML ? "[OK]" : "[X]"); + PrintFormat(" ICT Killzones: %s (%d zones)", EnableKillzones ? "[OK]" : "[X]", g_numKillzones); + PrintFormat(" Data Persistence: %s", EnableDataPersistence ? "[OK]" : "[X]"); + PrintFormat(" Backtesting: %s", BacktestMode != BACKTEST_DISABLED ? "[OK]" : "[X]"); + PrintFormat(" Trade Journal: %s", EnableTradeJournal ? "[OK]" : "[X]"); + Print("-----------------------------------------------------------"); + Print("[CHART] ACTIVE STRATEGIES:"); + if(STRATEGY_FVG_Entry) Print(" [OK] FVG Entry"); + if(STRATEGY_OB_Entry) Print(" [OK] Order Block Entry"); + if(STRATEGY_BOS_Retest) Print(" [OK] BOS Retest"); + if(STRATEGY_LIQ_Grab) Print(" [OK] Liquidity Grab"); + if(STRATEGY_OTE_Entry) Print(" [OK] OTE Entry"); + if(STRATEGY_BB_Entry) Print(" [OK] Breaker Block Entry"); + if(STRATEGY_MB_Entry) Print(" [OK] Mitigation Block Entry"); + if(STRATEGY_MM_Model) Print(" [OK] Market Maker Model"); + if(STRATEGY_ML_Signal) Print(" [OK] ML Signal"); + if(STRATEGY_Killzone) Print(" [OK] Killzone Entry"); + Print("-----------------------------------------------------------"); + if(EnableKillzones) + { + Print("[PIN] ACTIVE KILLZONES:"); + for(int i = 0; i < g_numKillzones; i++) + { + int sh, sm, eh, em; + GetAdjustedKillzoneTimes(i, sh, sm, eh, em); + PrintFormat(" [OK] %s: %02d:%02d - %02d:%02d (Broker Time)", + g_killzoneDefinitions[i].name, sh, sm, eh, em); + } + Print("-----------------------------------------------------------"); + } +} +//+------------------------------------------------------------------+ +//| Initialize Prediction Buffers | +//+------------------------------------------------------------------+ +void InitializePredictionBuffers() +{ + // [v6.42] Use PRED_Bars input instead of hardcoded 20 + g_predictionBars = MathMax(5, MathMin(100, PRED_Bars)); + ArrayResize(g_predictionBuffer, g_predictionBars); + ArrayResize(g_upperBandBuffer, g_predictionBars); + ArrayResize(g_lowerBandBuffer, g_predictionBars); + ArrayResize(g_predictionTime, g_predictionBars); + ArrayInitialize(g_predictionBuffer, 0); + ArrayInitialize(g_upperBandBuffer, 0); + ArrayInitialize(g_lowerBandBuffer, 0); + g_predictionInitialized = true; +} +//+------------------------------------------------------------------+ +//| Initialize Heatmap | +//+------------------------------------------------------------------+ +void InitializeHeatmap() +{ + // [v6.42] Use HEATMAP_Rows input instead of hardcoded 20 + g_heatmapRows = MathMax(5, MathMin(50, HEATMAP_Rows)); + ArrayResize(g_heatmapLevels, g_heatmapRows); + for(int i = 0; i < g_heatmapRows; i++) + { + g_heatmapLevels[i].price = 0; + g_heatmapLevels[i].strength = 0; + g_heatmapLevels[i].type = "NONE"; + } +} +//+------------------------------------------------------------------+ +//| Update Prediction Visualization | +//+------------------------------------------------------------------+ +void UpdatePredictionVisualization(const datetime &time[], const double &close[]) +{ + if(!g_predictionInitialized || !g_nnTrained) return; + if(ArraySize(ML_Predictions) == 0) return; + // [v6.42] Throttle updates using PRED_UpdateInterval + if(PRED_UpdateInterval > 0 && (TimeCurrent() - g_lastPredictionUpdate) < PRED_UpdateInterval) + return; + g_lastPredictionUpdate = TimeCurrent(); + ML_Prediction lastPred = ML_Predictions[ArraySize(ML_Predictions) - 1]; + double currentPrice = close[0]; + double expectedMove = lastPred.expectedMove * g_pipValue; + double uncertainty = g_cachedATR; + int horizon = MathMax(1, g_predictionBars); + // [v6.42] Use PRED_BullColor / PRED_BearColor inputs + color predColor = (lastPred.category == "BULLISH") ? PRED_BullColor : + (lastPred.category == "BEARISH") ? PRED_BearColor : clrGray; + // -- Prediction Line -- + string predLineName = "ICT_ML_PredLine"; + datetime endTime = time[0] + PeriodSeconds() * horizon; + if(ObjectFind(0, predLineName) < 0) + ObjectCreate(0, predLineName, OBJ_TREND, 0, time[0], currentPrice, endTime, lastPred.targetPrice); + ObjectSetInteger(0, predLineName, OBJPROP_TIME, 0, time[0]); + ObjectSetDouble(0, predLineName, OBJPROP_PRICE, 0, currentPrice); + ObjectSetInteger(0, predLineName, OBJPROP_TIME, 1, endTime); + ObjectSetDouble(0, predLineName, OBJPROP_PRICE, 1, lastPred.targetPrice); + ObjectSetInteger(0, predLineName, OBJPROP_COLOR, predColor); + ObjectSetInteger(0, predLineName, OBJPROP_WIDTH, 2); + ObjectSetInteger(0, predLineName, OBJPROP_STYLE, STYLE_DASH); + ObjectSetInteger(0, predLineName, OBJPROP_RAY_RIGHT, false); + // -- Confidence Label -- + string confLabel = "ICT_ML_ConfLabel"; + datetime midTime = time[0] + PeriodSeconds() * (horizon / 2); + double midPrice = (currentPrice + lastPred.targetPrice) / 2; + if(ObjectFind(0, confLabel) < 0) + ObjectCreate(0, confLabel, OBJ_TEXT, 0, midTime, midPrice); + ObjectSetInteger(0, confLabel, OBJPROP_TIME, 0, midTime); + ObjectSetDouble(0, confLabel, OBJPROP_PRICE, 0, midPrice); + ObjectSetString(0, confLabel, OBJPROP_TEXT, + StringFormat("ML %.0f%%", lastPred.confidence * 100)); + ObjectSetInteger(0, confLabel, OBJPROP_COLOR, predColor); + ObjectSetInteger(0, confLabel, OBJPROP_FONTSIZE, 10); + // -- [v6.42] Prediction Bands (uncertainty envelope) -- + if(PRED_ShowBands && uncertainty > 0) + { + // Build prediction path with expanding uncertainty + for(int i = 0; i < g_predictionBars; i++) + { + double progress = (double)(i + 1) / g_predictionBars; + double predPrice = currentPrice + (lastPred.targetPrice - currentPrice) * progress; + double bandWidth = uncertainty * progress * 1.5; // Expanding uncertainty cone + g_predictionBuffer[i] = predPrice; + g_upperBandBuffer[i] = predPrice + bandWidth; + g_lowerBandBuffer[i] = predPrice - bandWidth; + g_predictionTime[i] = time[0] + PeriodSeconds() * (i + 1); + } + // Draw upper band + string upperName = "ICT_ML_UpperBand"; + if(ObjectFind(0, upperName) < 0) + ObjectCreate(0, upperName, OBJ_TREND, 0, time[0], currentPrice, endTime, g_upperBandBuffer[g_predictionBars-1]); + ObjectSetInteger(0, upperName, OBJPROP_TIME, 0, time[0]); + ObjectSetDouble(0, upperName, OBJPROP_PRICE, 0, currentPrice); + ObjectSetInteger(0, upperName, OBJPROP_TIME, 1, endTime); + ObjectSetDouble(0, upperName, OBJPROP_PRICE, 1, g_upperBandBuffer[g_predictionBars-1]); + ObjectSetInteger(0, upperName, OBJPROP_COLOR, predColor); + ObjectSetInteger(0, upperName, OBJPROP_WIDTH, 1); + ObjectSetInteger(0, upperName, OBJPROP_STYLE, STYLE_DOT); + ObjectSetInteger(0, upperName, OBJPROP_RAY_RIGHT, false); + // Draw lower band + string lowerName = "ICT_ML_LowerBand"; + if(ObjectFind(0, lowerName) < 0) + ObjectCreate(0, lowerName, OBJ_TREND, 0, time[0], currentPrice, endTime, g_lowerBandBuffer[g_predictionBars-1]); + ObjectSetInteger(0, lowerName, OBJPROP_TIME, 0, time[0]); + ObjectSetDouble(0, lowerName, OBJPROP_PRICE, 0, currentPrice); + ObjectSetInteger(0, lowerName, OBJPROP_TIME, 1, endTime); + ObjectSetDouble(0, lowerName, OBJPROP_PRICE, 1, g_lowerBandBuffer[g_predictionBars-1]); + ObjectSetInteger(0, lowerName, OBJPROP_COLOR, predColor); + ObjectSetInteger(0, lowerName, OBJPROP_WIDTH, 1); + ObjectSetInteger(0, lowerName, OBJPROP_STYLE, STYLE_DOT); + ObjectSetInteger(0, lowerName, OBJPROP_RAY_RIGHT, false); + } + // -- [v6.42] Direction Arrow -- + if(PRED_ShowArrow) + { + string arrowName = "ICT_ML_Arrow"; + ObjectDelete(0, arrowName); + int arrowCode = (lastPred.category == "BULLISH") ? 233 : // Arrow up + (lastPred.category == "BEARISH") ? 234 : 0; // Arrow down + if(arrowCode > 0) + { + ObjectCreate(0, arrowName, OBJ_ARROW, 0, time[0], + (arrowCode == 233) ? currentPrice - uncertainty * 0.3 : + currentPrice + uncertainty * 0.3); + ObjectSetInteger(0, arrowName, OBJPROP_ARROWCODE, arrowCode); + ObjectSetInteger(0, arrowName, OBJPROP_COLOR, predColor); + ObjectSetInteger(0, arrowName, OBJPROP_WIDTH, 3); + } + } + // -- [v6.42] Target Price Marker -- + if(PRED_ShowTargets && lastPred.targetPrice > 0) + { + string targetName = "ICT_ML_Target"; + if(ObjectFind(0, targetName) < 0) + ObjectCreate(0, targetName, OBJ_ARROW, 0, endTime, lastPred.targetPrice); + ObjectSetInteger(0, targetName, OBJPROP_TIME, 0, endTime); + ObjectSetDouble(0, targetName, OBJPROP_PRICE, 0, lastPred.targetPrice); + ObjectSetInteger(0, targetName, OBJPROP_ARROWCODE, 161); // Diamond + ObjectSetInteger(0, targetName, OBJPROP_COLOR, predColor); + ObjectSetInteger(0, targetName, OBJPROP_WIDTH, 2); + // Target price label + string tpLabel = "ICT_ML_TargetLabel"; + if(ObjectFind(0, tpLabel) < 0) + ObjectCreate(0, tpLabel, OBJ_TEXT, 0, endTime, lastPred.targetPrice); + ObjectSetInteger(0, tpLabel, OBJPROP_TIME, 0, endTime); + ObjectSetDouble(0, tpLabel, OBJPROP_PRICE, 0, lastPred.targetPrice); + ObjectSetString(0, tpLabel, OBJPROP_TEXT, + StringFormat(" TP: %s", DoubleToString(lastPred.targetPrice, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS)))); + ObjectSetInteger(0, tpLabel, OBJPROP_COLOR, predColor); + ObjectSetInteger(0, tpLabel, OBJPROP_FONTSIZE, 9); + } +} +//+------------------------------------------------------------------+ +//| Update Probability Heatmap | +//+------------------------------------------------------------------+ +void UpdateProbabilityHeatmap(const datetime &time[], const double &high[], + const double &low[], const double &close[]) +{ + if(!g_nnTrained) return; + double rangeHigh = high[0]; + double rangeLow = low[0]; + for(int i = 0; i < HEATMAP_Bars && i < g_totalRates; i++) // [v6.42] was hardcoded 20 + { + if(high[i] > rangeHigh) rangeHigh = high[i]; + if(low[i] < rangeLow) rangeLow = low[i]; + } + double levelStep = (rangeHigh - rangeLow) / g_heatmapRows; + for(int i = 0; i < g_heatmapRows; i++) + { + double levelPrice = rangeLow + levelStep * (i + 0.5); + g_heatmapLevels[i].price = levelPrice; + // Calculate probability based on various factors + double prob = 0.5; + // Check if level has FVG + int fvgIdx; + if(IsPriceInFVG(levelPrice, fvgIdx, true) || IsPriceInFVG(levelPrice, fvgIdx, false)) + { + prob += 0.15; + } + // Check if level has OB + int obIdx; + if(IsPriceNearOB(levelPrice, obIdx, true) || IsPriceNearOB(levelPrice, obIdx, false)) + { + prob += 0.15; + } + // Check liquidity + for(int l = 0; l < ArraySize(LIQ_Array); l++) + { + if(MathAbs(LIQ_Array[l].price - levelPrice) < g_cachedATR * 0.2) + { + prob += 0.1; + break; + } + } + g_heatmapLevels[i].strength = MathMin(1.0, prob); + g_heatmapLevels[i].type = (prob > 0.7) ? "HIGH" : (prob > 0.5) ? "MEDIUM" : "LOW"; + // Draw heatmap rectangle + DrawHeatmapLevel(i, time[0], levelPrice, levelStep, g_heatmapLevels[i].strength); + } +} +//+------------------------------------------------------------------+ +//| Draw Heatmap Level | +//+------------------------------------------------------------------+ +void DrawHeatmapLevel(int index, datetime time, double price, double height, double strength) +{ + string objName = "ICT_Heatmap_" + IntegerToString(index); + // [v6.42] Use HEATMAP_Bars input for width (was hardcoded 5) + datetime endTime = time + PeriodSeconds() * HEATMAP_Bars; + if(ObjectFind(0, objName) < 0) + { + ObjectCreate(0, objName, OBJ_RECTANGLE, 0, + time, price - height/2, + endTime, price + height/2); + } + ObjectSetInteger(0, objName, OBJPROP_TIME, 0, time); + ObjectSetDouble(0, objName, OBJPROP_PRICE, 0, price - height/2); + ObjectSetInteger(0, objName, OBJPROP_TIME, 1, endTime); + ObjectSetDouble(0, objName, OBJPROP_PRICE, 1, price + height/2); + // [v6.42] Color based on strength using HEAT_* input colors + color heatColor; + if(strength > 0.8) + heatColor = HEAT_VeryHigh; + else if(strength > 0.65) + heatColor = HEAT_High; + else if(strength > 0.5) + heatColor = HEAT_Medium; + else if(strength > 0.35) + heatColor = HEAT_Low; + else + heatColor = HEAT_VeryLow; + ObjectSetInteger(0, objName, OBJPROP_COLOR, heatColor); + ObjectSetInteger(0, objName, OBJPROP_FILL, true); + ObjectSetInteger(0, objName, OBJPROP_BACK, true); + // [v6.42] Apply HEATMAP_Transparency + // MQL5 uses alpha channel: 0=opaque, 255=fully transparent + // Input HEATMAP_Transparency: 0=opaque, 100=fully transparent + int alpha = (int)(HEATMAP_Transparency * 255 / 100); + // Combine color with alpha: ARGB format + uchar r = (uchar)((heatColor >> 0) & 0xFF); + uchar g = (uchar)((heatColor >> 8) & 0xFF); + uchar b = (uchar)((heatColor >> 16) & 0xFF); + color colorWithAlpha = (color)((alpha << 24) | (b << 16) | (g << 8) | r); + ObjectSetInteger(0, objName, OBJPROP_COLOR, colorWithAlpha); + // * v9.16 FIX#48: HEATMAP_ShowLegend draws color legend on chart (was dead input) + if(HEATMAP_ShowLegend && index == 0) // Draw once (first level) + { + string legendNames[] = {"ICT_HM_Leg_VH","ICT_HM_Leg_H","ICT_HM_Leg_M","ICT_HM_Leg_L","ICT_HM_Leg_VL"}; + string legendTexts[] = {"[*] Very High","[*] High","[*] Medium","[*] Low","[*] Very Low"}; + color legendColors[] = {HEAT_VeryHigh, HEAT_High, HEAT_Medium, HEAT_Low, HEAT_VeryLow}; + for(int lg = 0; lg < 5; lg++) + { + if(ObjectFind(0, legendNames[lg]) < 0) + ObjectCreate(0, legendNames[lg], OBJ_LABEL, 0, 0, 0); + ObjectSetInteger(0, legendNames[lg], OBJPROP_XDISTANCE, 10); + ObjectSetInteger(0, legendNames[lg], OBJPROP_YDISTANCE, 60 + lg * 16); + ObjectSetInteger(0, legendNames[lg], OBJPROP_CORNER, CORNER_RIGHT_UPPER); + ObjectSetString(0, legendNames[lg], OBJPROP_TEXT, legendTexts[lg]); + ObjectSetInteger(0, legendNames[lg], OBJPROP_COLOR, legendColors[lg]); + ObjectSetInteger(0, legendNames[lg], OBJPROP_FONTSIZE, 8); + } + } +} +//+------------------------------------------------------------------+ +//| Update Dashboard | +//+------------------------------------------------------------------+ +void UpdateDashboard() +{ + if(!g_workingShowDashboard) return; + int x = 10; + int y = 30; + int lineHeight = 18; + // Title + CreateLabel("ICT_Dash_Title", x, y, "ICT Professional v5.0", clrGold, 12); + y += lineHeight + 5; + // Market Structure + color structColor = g_isBullishStructure ? clrLime : clrRed; + CreateLabel("ICT_Dash_Struct", x, y, + "Structure: " + (g_isBullishStructure ? "BULLISH" : "BEARISH"), + structColor, 10); + y += lineHeight; + // PD Zone + color pdColor = (g_currentPDZone == "PREMIUM") ? clrRed : + (g_currentPDZone == "DISCOUNT") ? clrLime : clrGray; + CreateLabel("ICT_Dash_PD", x, y, "PD Zone: " + g_currentPDZone, pdColor, 10); + y += lineHeight; + // Killzone (v5.0) + if(EnableKillzones) + { + color kzColor = g_isInKillzone ? clrGold : clrGray; + string kzText = "Killzone: " + (g_isInKillzone ? g_currentKillzoneName : "NONE"); + CreateLabel("ICT_Dash_KZ", x, y, kzText, kzColor, 10); + y += lineHeight; + } + // ML Prediction (v5.0) + if(EnableML && g_nnTrained && ArraySize(ML_Predictions) > 0) + { + ML_Prediction lastPred = ML_Predictions[ArraySize(ML_Predictions) - 1]; + color mlColor = (lastPred.category == "BULLISH") ? clrLime : + (lastPred.category == "BEARISH") ? clrRed : clrGray; + string mlText = StringFormat("ML: %s (%.0f%%)", lastPred.category, lastPred.confidence * 100); + CreateLabel("ICT_Dash_ML", x, y, mlText, mlColor, 10); + y += lineHeight; + // ML Accuracy + if(g_perfData.mlAccuracy > 0) + { + string accText = StringFormat("ML Accuracy: %.1f%%", g_perfData.mlAccuracy); + CreateLabel("ICT_Dash_MLAcc", x, y, accText, clrWhite, 10); + y += lineHeight; + } + } + // Active Signals + CreateLabel("ICT_Dash_Signals", x, y, + StringFormat("Active Signals: %d", g_activeSignalCount), clrWhite, 10); + y += lineHeight; + // Performance (v5.0) + if((EnableDataPersistence || EnablePerformanceTracking || AutoSavePerformance) && g_perfData.totalTrades > 0) // [v6.42] + { + y += 5; + CreateLabel("ICT_Dash_PerfTitle", x, y, "- Performance -", clrGold, 10); + y += lineHeight; + CreateLabel("ICT_Dash_Trades", x, y, + StringFormat("Trades: %d", g_perfData.totalTrades), clrWhite, 10); + y += lineHeight; + // * v9.59 FIX#229: Use grouped win rate from multiTP stats when available. + // g_perfData.overallWinRate counted each TP tranche close as a separate trade + // (3 lots open = 3 deal closes = 3 "trades") producing artificially low WR (2.4%). + // g_multiTPStats.winRate tracks outcomes at the ENTRY GROUP level (correct). + double displayWinRate = g_perfData.overallWinRate; + string wrLabel = "Win Rate: %.1f%%"; + if(InpEnableMultiTP && g_multiTPStats.totalEntries > 0) + { + CalculateMultiTPStats(); + displayWinRate = g_multiTPStats.winRate; + wrLabel = "Win Rate(G): %.1f%%"; // (G) = grouped + } + color wrColor = (displayWinRate >= 50) ? clrLime : clrRed; + CreateLabel("ICT_Dash_WR", x, y, + StringFormat(wrLabel, displayWinRate), wrColor, 10); + y += lineHeight; + double netPips = g_perfData.totalProfitPips - g_perfData.totalLossPips; + color pipsColor = (netPips >= 0) ? clrLime : clrRed; + CreateLabel("ICT_Dash_Pips", x, y, + StringFormat("Net Pips: %.1f", netPips), pipsColor, 10); + y += lineHeight; + CreateLabel("ICT_Dash_PF", x, y, + StringFormat("Profit Factor: %.2f", g_perfData.profitFactor), clrWhite, 10); + y += lineHeight; + } + // =============================================================== + // [NEW] MULTI-TP STATISTICS (NEW) + // =============================================================== + if(InpEnableMultiTP && g_multiTPStats.totalEntries > 0) + { + y += 5; + CreateLabel("ICT_Dash_MTPTitle", x, y, "- Multi-TP -", clrGold, 10); + y += lineHeight; + CreateLabel("ICT_Dash_MTPEntries", x, y, + StringFormat("Entries: %d Active: %d", + g_multiTPStats.totalEntries, GetActiveMultiTPCount()), clrWhite, 10); + y += lineHeight; + CreateLabel("ICT_Dash_MTPTP1", x, y, + StringFormat("TP1: %d TP2: %d TP3: %d", + g_multiTPStats.tp1HitCount, g_multiTPStats.tp2HitCount, + g_multiTPStats.tp3HitCount), clrLime, 10); + y += lineHeight; + CreateLabel("ICT_Dash_MTPSL", x, y, + StringFormat("SL: %d BE: %d", + g_multiTPStats.slHitCount, g_multiTPStats.beHitCount), clrRed, 10); + y += lineHeight; + // Calculate net profit + double totalProfit = g_multiTPStats.totalProfitTP1 + + g_multiTPStats.totalProfitTP2 + + g_multiTPStats.totalProfitTP3; + double netProfit = totalProfit - g_multiTPStats.totalLoss; + color netColor = (netProfit >= 0) ? clrLime : clrRed; + CreateLabel("ICT_Dash_MTPNet", x, y, + StringFormat("Net: %s%.1fR", (netProfit >= 0 ? "+" : ""), netProfit), + netColor, 10); + y += lineHeight; + // Win Rate + if(g_multiTPStats.totalEntries > 0) + { + double winRate = ((double)g_multiTPStats.tp1HitCount / g_multiTPStats.totalEntries) * 100.0; + color wrColor = (winRate >= 50) ? clrLime : clrRed; + CreateLabel("ICT_Dash_MTPWR", x, y, + StringFormat("Win Rate: %.1f%%", winRate), wrColor, 10); + y += lineHeight; + } + } + // ATR + y += 5; + CreateLabel("ICT_Dash_ATR", x, y, + StringFormat("ATR: %.1f pips", g_cachedATR / g_pipValue), clrWhite, 10); + y += lineHeight; + // RSI + color rsiColor = (g_cachedRSI > 70) ? clrRed : (g_cachedRSI < 30) ? clrLime : clrWhite; + CreateLabel("ICT_Dash_RSI", x, y, + StringFormat("RSI: %.1f", g_cachedRSI), rsiColor, 10); +} +//+------------------------------------------------------------------+ +//| Create Label Helper | +//+------------------------------------------------------------------+ +void CreateLabel(string name, int x, int y, string text, color clr, int fontSize) +{ + if(ObjectFind(0, name) < 0) + { + ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0); + } + ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x); + ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y); + ObjectSetInteger(0, name, OBJPROP_CORNER, DashboardCorner); // [v6.42] + ObjectSetString(0, name, OBJPROP_TEXT, text); + ObjectSetInteger(0, name, OBJPROP_COLOR, clr); + ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize); + ObjectSetString(0, name, OBJPROP_FONT, "Consolas"); +} +//+------------------------------------------------------------------+ +//| Create Label Helper - Extended with Font Name | +//+------------------------------------------------------------------+ +void CreateLabelEx(string name, int x, int y, string text, color clr, int fontSize, string fontName) +{ + if(ObjectFind(0, name) < 0) + { + ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0); + } + ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x); + ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y); + ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER); + ObjectSetString(0, name, OBJPROP_TEXT, text); + ObjectSetInteger(0, name, OBJPROP_COLOR, clr); + ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize); + ObjectSetString(0, name, OBJPROP_FONT, fontName); +} +//+------------------------------------------------------------------+ +//| Main Dashboard Update - PROFESSIONAL | +//+------------------------------------------------------------------+ +void UpdateProfessionalDashboard() +{ + if(!Dash_Enabled) return; + int x = g_dashStartX; + int y = g_dashStartY; + // Objects are updated in-place via CreateLabelEx (no delete/recreate = no flicker) + // Full cleanup only on chart close/EA remove (OnDeinit) + //=== TITLE BAR === + DrawDashboardTitleBar(x, y); + y += 42; // * v9.52b: was 28 — title bar is now 38px (added subtitle row), gap=4px + if(g_dashMinimized) + { + return; + } + // [v6.42] Display toggles + bool showScoring = Scoring_ShowOnChart; + bool showWinProb = WinProb_ShowOnChart; + bool showEV = EV_ShowOnChart; + bool showCorr = Corr_ShowOnChart; + bool showTime = Time_ShowOnChart; + bool showChecklist = ShowTradeChecklist; + bool showAge = ShowSignalAge; + //=== RISK & DRAWDOWN PANEL * v8.06: always shown when Dash_Enabled === + y = DrawRiskDDPanel(x, y); + y += g_dashPanelSpacing; + //=== MARKET OVERVIEW PANEL === + if(Dash_ShowMarketPanel) + { + y = DrawMarketOverviewPanel(x, y); + y += g_dashPanelSpacing; + } + //=== ICT CONCEPTS PANEL === + if(Dash_ShowICTPanel) + { + y = DrawICTConceptsPanel(x, y); + y += g_dashPanelSpacing; + } + //=== ENTRY SCORE PANEL === + if(Dash_ShowEntryScorePanel && InpEnableScoring) + { + y = DrawEntryScorePanel(x, y); + y += g_dashPanelSpacing; + } + //=== SIGNALS PANEL === + if(Dash_ShowSignalPanel) + { + y = DrawSignalsPanel(x, y); + y += g_dashPanelSpacing; + } + //=== MULTI-TP PANEL === + if(Dash_ShowMultiTPPanel && InpEnableMultiTP) + { + y = DrawMultiTPPanel(x, y); + y += g_dashPanelSpacing; + } + //=== PERFORMANCE PANEL === + if(Dash_ShowPerformancePanel) + { + y = DrawPerformancePanel(x, y); + y += g_dashPanelSpacing; + } + //=== NEW FEATURES PANEL === + if(Dash_ShowNewFeaturesPanel) + { + y = DrawNewFeaturesPanel(x, y); + y += g_dashPanelSpacing; + } + //=== BUILD INFO PANEL (always shown) — eaEURUSD version, fixes, last backtest === + DrawBuildPanel(x, y); +} +//+------------------------------------------------------------------+ +//| Draw Title Bar | +//+------------------------------------------------------------------+ +void DrawDashboardTitleBar(int x, int y) +{ + // * v9.52b: Title bar expanded to 38px (was 24px) to fit version + fix subtitle row + // Background — taller to accommodate subtitle + CreateDashRect("DASH_TitleBG", x, y, g_dashWidth, 38, C'40,40,60', Dash_BorderColor); + + // ── Row 1: EA name + version ────────────────────────────────────────── + // * v9.30: dynamic title from EA_NAME + EA_VERSION defines + // * v9.52b: EA_NAME changed from "eaEURUSD" to "EA ANGEL" (matches product branding) + string title = EA_TITLE; // "EA ANGEL v9.52b" -- auto-updates on every release + CreateDashLabel("DASH_Title", x + 10, y + 3, title, Dash_TitleColor, 11, "Arial Bold"); + + // Minimize + Status (top-right, row 1) + string minText = g_dashMinimized ? "[+]" : "[-]"; + CreateDashLabel("DASH_MinBtn", x + g_dashWidth - 25, y + 3, minText, clrWhite, 12, "Arial Bold"); + color statusColor = g_initSuccess ? clrLime : clrRed; + CreateDashLabel("DASH_Status", x + g_dashWidth - 50, y + 6, "[*]", statusColor, 10, "Arial"); + + // ── Row 2: Last fix + date (subtitle) ──────────────────────────────── + // * v9.52b: Shows current fix number and date so we always know which build is running + // Update EA_LAST_FIX and EA_LAST_DATE defines on every release — appears here automatically + string subtitle = EA_LAST_FIX + " " + EA_LAST_DATE; + CreateDashLabel("DASH_Subtitle", x + 10, y + 22, subtitle, clrLightSteelBlue, 8, "Consolas"); + + // Symbol + TF (right side of subtitle row) + string tfStr = ""; + switch(_Period) + { + case PERIOD_M5: tfStr = "M5"; break; + case PERIOD_M15: tfStr = "M15"; break; + case PERIOD_H1: tfStr = "H1"; break; + case PERIOD_H4: tfStr = "H4"; break; + case PERIOD_D1: tfStr = "D1"; break; + default: tfStr = EnumToString((ENUM_TIMEFRAMES)_Period); break; + } + string symTF = _Symbol + " " + tfStr; + CreateDashLabel("DASH_SymTF", x + g_dashWidth - 80, y + 22, symTF, clrGold, 8, "Consolas"); +} +//+------------------------------------------------------------------+ +//| Draw Market Overview Panel | +//+------------------------------------------------------------------+ +int DrawMarketOverviewPanel(int x, int y) +{ + // * v8.06: panelHeight 95->145 (added MTF + Regime rows) + int panelHeight = 145; + int innerY = y; + // Panel background + CreateDashRect("DASH_MarketBG", x, y, g_dashWidth, panelHeight, Dash_PanelColor, Dash_BorderColor); + // Panel title + CreateDashLabel("DASH_MarketTitle", x + 10, innerY + 3, "[CHART] MARKET OVERVIEW", Dash_TitleColor, 9, "Arial Bold"); + innerY += 20; + //=== STRUCTURE === + color structColor = g_isBullishStructure ? Dash_BullColor : Dash_BearColor; + string structText = g_isBullishStructure ? "[^] BULLISH" : "[v] BEARISH"; + CreateDashLabel("DASH_Struct", x + 10, innerY, "Structure:", Dash_TextColor, 9, "Consolas"); + CreateDashLabel("DASH_StructVal", x + 100, innerY, structText, structColor, 9, "Consolas"); + innerY += g_dashLineHeight; + //=== MTF DIRECTION * v8.06 NEW === + { + string mtfStr = "NEUTRAL"; + color mtfColor = Dash_NeutralColor; + if(MTF_Enabled) + { + switch(g_mtfAnalysis.overallDirection) + { + case MTF_STRONG_BULLISH: mtfStr = "[^][^] STRONG BULL"; mtfColor = Dash_BullColor; break; + case MTF_BULLISH: mtfStr = "[^] BULL"; mtfColor = Dash_BullColor; break; + case MTF_NEUTRAL: mtfStr = "- NEUTRAL"; mtfColor = Dash_NeutralColor; break; + case MTF_BEARISH: mtfStr = "[v] BEAR"; mtfColor = Dash_BearColor; break; + case MTF_STRONG_BEARISH: mtfStr = "[v][v] STRONG BEAR"; mtfColor = Dash_BearColor; break; + } + } + // * v9.36 FIX#160b: Dynamic HTF label based on current TF (was hardcoded "MTF H4:") + string _mtfLbl = "MTF H4:"; + if(_Period >= PERIOD_H4) _mtfLbl = "MTF D1:"; + else if(_Period >= PERIOD_H1) _mtfLbl = "MTF H4:"; + else if(_Period >= PERIOD_M15) _mtfLbl = "MTF H1:"; + else _mtfLbl = "MTF M15:"; + CreateDashLabel("DASH_MTFLbl", x + 10, innerY, _mtfLbl, Dash_TextColor, 9, "Consolas"); + CreateDashLabel("DASH_MTFVal", x + 100, innerY, mtfStr, mtfColor, 9, "Consolas"); + innerY += g_dashLineHeight; + } + //=== REGIME * v8.06 NEW === + { + string regStr = g_regimeValid ? GetRegimeString(g_regimeData.regime) : "---"; + color regColor = Dash_NeutralColor; + if(g_regimeValid) + { + if(g_regimeData.regime == REGIME_STRONG_TREND_UP || g_regimeData.regime == REGIME_TREND_UP) + regColor = Dash_BullColor; + else if(g_regimeData.regime == REGIME_WEAK_TREND_UP) + regColor = clrYellowGreen; + else if(g_regimeData.regime == REGIME_STRONG_TREND_DOWN || g_regimeData.regime == REGIME_TREND_DOWN) + regColor = Dash_BearColor; + else if(g_regimeData.regime == REGIME_WEAK_TREND_DOWN) + regColor = clrOrange; + else if(g_regimeData.regime == REGIME_VOLATILE || g_regimeData.regime == REGIME_BREAKOUT) + regColor = clrYellow; + } + CreateDashLabel("DASH_RegLbl", x + 10, innerY, "Regime:", Dash_TextColor, 9, "Consolas"); + CreateDashLabel("DASH_RegVal", x + 100, innerY, regStr, regColor, 9, "Consolas"); + innerY += g_dashLineHeight; + } + //=== PD ZONE === + color pdColor = (g_currentPDZone == "PREMIUM") ? Dash_BearColor : + (g_currentPDZone == "DISCOUNT") ? Dash_BullColor : Dash_NeutralColor; + CreateDashLabel("DASH_PDZone", x + 10, innerY, "PD Zone:", Dash_TextColor, 9, "Consolas"); + CreateDashLabel("DASH_PDZoneVal", x + 100, innerY, g_currentPDZone, pdColor, 9, "Consolas"); + DrawPDZoneBar(x + 170, innerY + 2, 100, 10); + innerY += g_dashLineHeight; + //=== KILLZONE === + if(EnableKillzones) + { + color kzColor = g_isInKillzone ? clrGold : Dash_NeutralColor; + string kzText = g_isInKillzone ? g_currentKillzoneName : "None"; + CreateDashLabel("DASH_KZ", x + 10, innerY, "Killzone:", Dash_TextColor, 9, "Consolas"); + CreateDashLabel("DASH_KZVal", x + 100, innerY, kzText, kzColor, 9, "Consolas"); + innerY += g_dashLineHeight; + } + //=== ATR & RSI === + CreateDashLabel("DASH_ATR", x + 10, innerY, + StringFormat("ATR: %.1f pips", g_cachedATR / g_pipValue), Dash_TextColor, 9, "Consolas"); + color rsiColor = (g_cachedRSI > 70) ? Dash_BearColor : (g_cachedRSI < 30) ? Dash_BullColor : Dash_TextColor; + CreateDashLabel("DASH_RSI", x + 140, innerY, + StringFormat("RSI: %.0f", g_cachedRSI), rsiColor, 9, "Consolas"); + return y + panelHeight; +} +//+------------------------------------------------------------------+ +//| Draw ICT Concepts Panel | +//+------------------------------------------------------------------+ +int DrawICTConceptsPanel(int x, int y) +{ + int panelHeight = 115; + int innerY = y; + // Panel background + CreateDashRect("DASH_ICTBG", x, y, g_dashWidth, panelHeight, Dash_PanelColor, Dash_BorderColor); + // Panel title + CreateDashLabel("DASH_ICTTitle", x + 10, innerY + 3, "[TARGET] ICT CONCEPTS", Dash_TitleColor, 9, "Arial Bold"); + innerY += 20; + int colWidth = 85; + //=== ROW 1: FVG, OB, BB === + int fvgCount = CountActiveFVGs(); + color fvgColor = (fvgCount > 0) ? Dash_BullColor : Dash_NeutralColor; + CreateDashLabel("DASH_FVG", x + 10, innerY, StringFormat("FVG: %d", fvgCount), fvgColor, 9, "Consolas"); + int obCount = CountActiveOBs(); + color obColor = (obCount > 0) ? Dash_BullColor : Dash_NeutralColor; + CreateDashLabel("DASH_OB", x + 10 + colWidth, innerY, StringFormat("OB: %d", obCount), obColor, 9, "Consolas"); + int bbCount = CountActiveBBs(); + color bbColor = (bbCount > 0) ? clrOrange : Dash_NeutralColor; + CreateDashLabel("DASH_BB", x + 10 + colWidth * 2, innerY, StringFormat("BB: %d", bbCount), bbColor, 9, "Consolas"); + innerY += g_dashLineHeight; + //=== ROW 2: LIQ, OTE === + int liqCount = CountActiveLiquidity(); + color liqColor = (liqCount > 0) ? Dash_BullColor : Dash_NeutralColor; + CreateDashLabel("DASH_LIQ", x + 10, innerY, StringFormat("LIQ: %d", liqCount), liqColor, 9, "Consolas"); + bool oteActive = IsOTEZoneActive(); + color oteColor = oteActive ? Dash_BullColor : Dash_NeutralColor; + CreateDashLabel("DASH_OTE", x + 10 + colWidth, innerY, oteActive ? "OTE: [OK]" : "OTE: -", oteColor, 9, "Consolas"); + innerY += g_dashLineHeight; + //=== ROW 3: DIVERGENCE & TRENDLINE === + string divText = "DIV: -"; + color divColor = Dash_NeutralColor; + if(Divergence_Enabled) + { + if(g_bullishDivergence) + { + divText = "DIV: BULL"; + divColor = Dash_BullColor; + } + else if(g_bearishDivergence) + { + divText = "DIV: BEAR"; + divColor = Dash_BearColor; + } + } + CreateDashLabel("DASH_DIV", x + 10, innerY, divText, divColor, 9, "Consolas"); + // Trendline + string tlText = "TL: -"; + color tlColor = Dash_NeutralColor; + if(Trendline_Enabled) + { + if(g_trendlineBreakBull) + { + tlText = "TL: BRK[^]"; + tlColor = Dash_BullColor; + } + else if(g_trendlineBreakBear) + { + tlText = "TL: BRK[v]"; + tlColor = Dash_BearColor; + } + else if(g_bullTrendlineActive) + { + tlText = "TL: Supp"; + tlColor = clrDodgerBlue; + } + else if(g_bearTrendlineActive) + { + tlText = "TL: Res"; + tlColor = clrCrimson; + } + } + CreateDashLabel("DASH_TL", x + 10 + colWidth, innerY, tlText, tlColor, 9, "Consolas"); + innerY += g_dashLineHeight; + //=== ROW 4: CRT, TBS, AMD, JUDAS === + bool crtActive = HasActiveCRTSetup(); + color crtColor = crtActive ? clrMagenta : Dash_NeutralColor; + CreateDashLabel("DASH_CRT", x + 10, innerY, crtActive ? "CRT:[OK]" : "CRT:-", crtColor, 8, "Consolas"); + bool tbsActive = HasActiveTBSSetup(); + color tbsColor = tbsActive ? clrOrange : Dash_NeutralColor; + CreateDashLabel("DASH_TBS", x + 65, innerY, tbsActive ? "TBS:[OK]" : "TBS:-", tbsColor, 8, "Consolas"); + string amdPhase = GetAMDPhaseString(); + color amdColor = (amdPhase != "-") ? clrGold : Dash_NeutralColor; + CreateDashLabel("DASH_AMD", x + 120, innerY, "AMD:" + amdPhase, amdColor, 8, "Consolas"); + bool judasActive = HasActiveJudas(); + color judasColor = judasActive ? clrDeepPink : Dash_NeutralColor; + CreateDashLabel("DASH_Judas", x + 185, innerY, judasActive ? "JDS:[OK]" : "JDS:-", judasColor, 8, "Consolas"); + return y + panelHeight; +} +//+------------------------------------------------------------------+ +//| Draw Entry Score Panel | +//+------------------------------------------------------------------+ +int DrawEntryScorePanel(int x, int y) +{ + int panelHeight = 75; + int innerY = y; + // Panel background + CreateDashRect("DASH_ScoreBG", x, y, g_dashWidth, panelHeight, Dash_PanelColor, Dash_BorderColor); + // Panel title + CreateDashLabel("DASH_ScoreTitle", x + 10, innerY + 3, "[UP] SIGNAL SCORE (chart display)", Dash_TitleColor, 9, "Arial Bold"); + innerY += 20; + int totalScore = g_lastEntryScore.totalScore; + string grade = g_lastEntryScore.grade; + if(grade == "") grade = "-"; + // Score bar + DrawScoreBar(x + 10, innerY, g_dashWidth - 20, 18, totalScore, 140); + innerY += 24; + // Score details + color gradeColor = GetScoreGradeColor(grade); + CreateDashLabel("DASH_ScoreGrade", x + 10, innerY, + StringFormat("Score: %d/85 [%s]", totalScore, grade), gradeColor, 10, "Consolas"); + // * v10.06 FIX#275: Dashboard reads g_gates.minScore (same as all other gates). + double _dash_minScore = g_gates.computed ? g_gates.minScore : (double)EA_MinEntryScore; + color minColor = (totalScore >= (int)_dash_minScore) ? Dash_BullColor : Dash_BearColor; + string minText = (totalScore >= (int)_dash_minScore) ? "[OK] PASS" : "[X] FAIL"; + CreateDashLabel("DASH_ScoreMin", x + 130, innerY, + StringFormat("Min:%d %s", (int)_dash_minScore, minText), minColor, 9, "Consolas"); + return y + panelHeight; +} +//+------------------------------------------------------------------+ +//| Draw Signals Panel | +//+------------------------------------------------------------------+ +int DrawSignalsPanel(int x, int y) +{ + int panelHeight = 55; + int innerY = y; + // Panel background + CreateDashRect("DASH_SigBG", x, y, g_dashWidth, panelHeight, Dash_PanelColor, Dash_BorderColor); + // Panel title + CreateDashLabel("DASH_SigTitle", x + 10, innerY + 3, "[ALERT] SIGNALS", Dash_TitleColor, 9, "Arial Bold"); + innerY += 20; + // Active signals + CreateDashLabel("DASH_SigActive", x + 10, innerY, + StringFormat("Active: %d", g_activeSignalCount), Dash_TextColor, 9, "Consolas"); + // ML Prediction (if enabled) + if(EnableML && g_nnTrained && ArraySize(ML_Predictions) > 0) + { + ML_Prediction lastPred = ML_Predictions[ArraySize(ML_Predictions) - 1]; + color mlColor = (lastPred.category == "BULLISH") ? Dash_BullColor : + (lastPred.category == "BEARISH") ? Dash_BearColor : Dash_NeutralColor; + string mlText = StringFormat("ML: %s %.0f%%", lastPred.category, lastPred.confidence * 100); + CreateDashLabel("DASH_ML", x + 120, innerY, mlText, mlColor, 9, "Consolas"); + } + return y + panelHeight; +} +//+------------------------------------------------------------------+ +//| Draw Multi-TP Panel | +//+------------------------------------------------------------------+ +int DrawMultiTPPanel(int x, int y) +{ + int panelHeight = 85; + int innerY = y; + // Panel background + CreateDashRect("DASH_MTPBG", x, y, g_dashWidth, panelHeight, Dash_PanelColor, Dash_BorderColor); + // Panel title + CreateDashLabel("DASH_MTPTitle", x + 10, innerY + 3, "[TARGET] MULTI-TP", Dash_TitleColor, 9, "Arial Bold"); + innerY += 20; + // Entries + CreateDashLabel("DASH_MTPEntries", x + 10, innerY, + StringFormat("Entries: %d Active: %d", + g_multiTPStats.totalEntries, GetActiveMultiTPCount()), Dash_TextColor, 9, "Consolas"); + innerY += g_dashLineHeight; + // TP hits + CreateDashLabel("DASH_TP1", x + 10, innerY, StringFormat("TP1: %d", g_multiTPStats.tp1HitCount), Dash_BullColor, 8, "Consolas"); + CreateDashLabel("DASH_TP2", x + 80, innerY, StringFormat("TP2: %d", g_multiTPStats.tp2HitCount), clrYellow, 8, "Consolas"); + CreateDashLabel("DASH_TP3", x + 150, innerY, StringFormat("TP3: %d", g_multiTPStats.tp3HitCount), clrAqua, 8, "Consolas"); + CreateDashLabel("DASH_SL", x + 220, innerY, StringFormat("SL: %d", g_multiTPStats.slHitCount), Dash_BearColor, 8, "Consolas"); + innerY += g_dashLineHeight; + // Net profit + double totalProfit = g_multiTPStats.totalProfitTP1 + g_multiTPStats.totalProfitTP2 + g_multiTPStats.totalProfitTP3; + double netProfit = totalProfit - g_multiTPStats.totalLoss; + color netColor = (netProfit >= 0) ? Dash_BullColor : Dash_BearColor; + string netSign = (netProfit >= 0) ? "+" : ""; + CreateDashLabel("DASH_MTPNet", x + 10, innerY, + StringFormat("Net: %s%.1fR", netSign, netProfit), netColor, 9, "Consolas"); + // Win rate + if(g_multiTPStats.totalEntries > 0) + { + double wr = ((double)g_multiTPStats.tp1HitCount / g_multiTPStats.totalEntries) * 100.0; + color wrColor = (wr >= 50) ? Dash_BullColor : Dash_BearColor; + CreateDashLabel("DASH_MTPWR", x + 150, innerY, StringFormat("WR: %.0f%%", wr), wrColor, 9, "Consolas"); + } + return y + panelHeight; +} +//+------------------------------------------------------------------+ +//| Draw Performance Panel | +//+------------------------------------------------------------------+ +int DrawPerformancePanel(int x, int y) +{ + int panelHeight = 95; + int innerY = y; + // Panel background + CreateDashRect("DASH_PerfBG", x, y, g_dashWidth, panelHeight, Dash_PanelColor, Dash_BorderColor); + // Panel title + CreateDashLabel("DASH_PerfTitle", x + 10, innerY + 3, "[CHART] PERFORMANCE", Dash_TitleColor, 9, "Arial Bold"); + innerY += 20; + // Total trades + CreateDashLabel("DASH_Trades", x + 10, innerY, + StringFormat("Trades: %d", g_perfData.totalTrades), Dash_TextColor, 9, "Consolas"); + // Wins / Losses + CreateDashLabel("DASH_WL", x + 120, innerY, + StringFormat("W: %d L: %d", g_perfData.totalWins, g_perfData.totalLosses), + Dash_TextColor, 9, "Consolas"); + innerY += g_dashLineHeight; + // Win Rate + // * v9.59 FIX#229: Use grouped win rate from multiTP stats when available + double winRate = g_perfData.overallWinRate; + string wrDashLabel = "Win Rate: %.1f%%"; + if(InpEnableMultiTP && g_multiTPStats.totalEntries > 0) + { + CalculateMultiTPStats(); + winRate = g_multiTPStats.winRate; + wrDashLabel = "Win Rate(G): %.1f%%"; + } + color wrColor = (winRate >= 60) ? Dash_BullColor : (winRate >= 50) ? clrYellow : Dash_BearColor; + CreateDashLabel("DASH_WR", x + 10, innerY, StringFormat(wrDashLabel, winRate), wrColor, 9, "Consolas"); + DrawProgressBar(x + 140, innerY + 2, 130, 10, winRate, 100, wrColor, "_WR"); + innerY += g_dashLineHeight; + // Net Pips + double netPips = g_perfData.totalProfitPips - g_perfData.totalLossPips; + color pipsColor = (netPips >= 0) ? Dash_BullColor : Dash_BearColor; + CreateDashLabel("DASH_Pips", x + 10, innerY, + StringFormat("Net Pips: %s%.1f", (netPips >= 0 ? "+" : ""), netPips), pipsColor, 9, "Consolas"); + innerY += g_dashLineHeight; + // Profit Factor + color pfColor = (g_perfData.profitFactor >= 1.5) ? Dash_BullColor : + (g_perfData.profitFactor >= 1.0) ? clrYellow : Dash_BearColor; + CreateDashLabel("DASH_PF", x + 10, innerY, + StringFormat("PF: %.2f", g_perfData.profitFactor), pfColor, 9, "Consolas"); + // Expectancy + double expectancy = (g_perfData.totalTrades > 0) ? netPips / g_perfData.totalTrades : 0; + color expColor = (expectancy >= 0) ? Dash_BullColor : Dash_BearColor; + CreateDashLabel("DASH_Exp", x + 120, innerY, + StringFormat("Exp: %.1f pips", expectancy), expColor, 9, "Consolas"); + return y + panelHeight; +} +//+------------------------------------------------------------------+ +//| Draw Risk & Drawdown Panel * v8.06 | +//+------------------------------------------------------------------+ +int DrawRiskDDPanel(int x, int y) +{ + int panelHeight = 105; + int innerY = y; + CreateDashRect("DASH_RiskBG", x, y, g_dashWidth, panelHeight, Dash_PanelColor, Dash_BorderColor); + CreateDashLabel("DASH_RiskTitle", x + 10, innerY + 3, "[MONEY] RISK & DRAWDOWN", Dash_TitleColor, 9, "Arial Bold"); + innerY += 20; + //=== ACTUAL RISK % (what the EA is actually using) === + double actualRisk = EA_RiskPercent; + if(AutoOpt_Enabled && g_autoOptInitialized) + actualRisk = g_autoOptParams.risk_pct; + // FIX#164: Manual mode shows EA_RiskPercent directly (not profile) + // else if(g_gates.computed) block removed -- manual = EA input + bool riskMatch = (MathAbs(actualRisk - EA_RiskPercent) < 0.05); + color riskColor = riskMatch ? Dash_BullColor : clrOrange; + string riskText = riskMatch + ? StringFormat("%.1f%%", actualRisk) + : StringFormat("%.1f%% (set: %.1f%%)", actualRisk, EA_RiskPercent); + CreateDashLabel("DASH_RiskLbl", x + 10, innerY, "Risk/Trade:", Dash_TextColor, 9, "Consolas"); + CreateDashLabel("DASH_RiskVal", x + 110, innerY, riskText, riskColor, 9, "Consolas"); + innerY += g_dashLineHeight; + //=== DAILY DRAWDOWN === + double dailyMax = EA_MaxDailyDrawdownPercent; + double dailyUsed = g_currentDailyDD; + color dailyColor = (dailyMax > 0 && (dailyUsed / dailyMax) > 0.80) ? Dash_BearColor : + (dailyMax > 0 && (dailyUsed / dailyMax) > 0.50) ? clrOrange : Dash_BullColor; + CreateDashLabel("DASH_DDDayLbl", x + 10, innerY, + StringFormat("Daily DD: %.2f%%", dailyUsed), Dash_TextColor, 9, "Consolas"); + CreateDashLabel("DASH_DDDayMax", x + 170, innerY, + StringFormat("/ %.1f%%", dailyMax), dailyColor, 9, "Consolas"); + DrawProgressBar(x + 10, innerY + 12, g_dashWidth - 20, 6, dailyUsed, dailyMax, dailyColor, "_DDd"); + innerY += g_dashLineHeight + 8; + //=== WEEKLY DRAWDOWN === + double weeklyMax = EA_MaxWeeklyDrawdownPercent; + double weeklyUsed = g_currentWeeklyDD; + color weeklyColor = (weeklyMax > 0 && (weeklyUsed / weeklyMax) > 0.80) ? Dash_BearColor : + (weeklyMax > 0 && (weeklyUsed / weeklyMax) > 0.50) ? clrOrange : Dash_BullColor; + CreateDashLabel("DASH_DDWeekLbl", x + 10, innerY, + StringFormat("Weekly DD: %.2f%%", weeklyUsed), Dash_TextColor, 9, "Consolas"); + CreateDashLabel("DASH_DDWeekMax", x + 170, innerY, + StringFormat("/ %.1f%%", weeklyMax), weeklyColor, 9, "Consolas"); + DrawProgressBar(x + 10, innerY + 12, g_dashWidth - 20, 6, weeklyUsed, weeklyMax, weeklyColor, "_DDw"); + innerY += g_dashLineHeight + 8; + //=== TOTAL DRAWDOWN === + double totalMax = EA_MaxTotalDrawdownPercent; + double totalUsed = g_currentTotalDD; + color totalColor = (totalMax > 0 && (totalUsed / totalMax) > 0.80) ? Dash_BearColor : + (totalMax > 0 && (totalUsed / totalMax) > 0.50) ? clrOrange : Dash_BullColor; + CreateDashLabel("DASH_DDTotLbl", x + 10, innerY, + StringFormat("Total DD: %.2f%%", totalUsed), Dash_TextColor, 9, "Consolas"); + CreateDashLabel("DASH_DDTotMax", x + 170, innerY, + StringFormat("/ %.1f%%", totalMax), totalColor, 9, "Consolas"); + DrawProgressBar(x + 10, innerY + 12, g_dashWidth - 20, 6, totalUsed, totalMax, totalColor, "_DDt"); + return y + panelHeight; +} +//+------------------------------------------------------------------+ +//| Draw New Features Panel | +//+------------------------------------------------------------------+ +int DrawNewFeaturesPanel(int x, int y) +{ + int panelHeight = 55; + int innerY = y; + // Panel background + CreateDashRect("DASH_NewBG", x, y, g_dashWidth, panelHeight, Dash_PanelColor, Dash_BorderColor); + // Panel title + CreateDashLabel("DASH_NewTitle", x + 10, innerY + 3, "[NEW] MODULES", Dash_TitleColor, 9, "Arial Bold"); + innerY += 20; + // Row 1 + color vsaColor = VSA_Enabled ? Dash_BullColor : Dash_NeutralColor; + CreateDashLabel("DASH_VSA", x + 10, innerY, "VSA", vsaColor, 8, "Consolas"); + color mtfColor = MTF_Enabled ? Dash_BullColor : Dash_NeutralColor; + CreateDashLabel("DASH_MTF", x + 50, innerY, "MTF", mtfColor, 8, "Consolas"); + color divOnColor = Divergence_Enabled ? Dash_BullColor : Dash_NeutralColor; + CreateDashLabel("DASH_DivOn", x + 90, innerY, "DIV", divOnColor, 8, "Consolas"); + color tlOnColor = Trendline_Enabled ? Dash_BullColor : Dash_NeutralColor; + CreateDashLabel("DASH_TLOn", x + 130, innerY, "TL", tlOnColor, 8, "Consolas"); + color crtOnColor = CRT_Enabled ? Dash_BullColor : Dash_NeutralColor; + CreateDashLabel("DASH_CRTOn", x + 165, innerY, "CRT", crtOnColor, 8, "Consolas"); + color tbsOnColor = TBS_Enabled ? Dash_BullColor : Dash_NeutralColor; + CreateDashLabel("DASH_TBSOn", x + 205, innerY, "TBS", tbsOnColor, 8, "Consolas"); + color smartColor = SmartEntry_Enabled ? Dash_BullColor : Dash_NeutralColor; + CreateDashLabel("DASH_Smart", x + 245, innerY, "SE", smartColor, 8, "Consolas"); + return y + panelHeight; +} +//+------------------------------------------------------------------+ +//| Draw Build Info Panel | +//| Shows: version, total fixes, last 2 fixes, last backtest result | +//| Updated every version bump — eaEURUSD v9.43 | +//+------------------------------------------------------------------+ +int DrawBuildPanel(int x, int y) +{ + // * v9.44: LiveStatusPanel — 3 sections: + // 1. OPEN TRADE — technique, RR, bars, zone armed/suppressed, BE/Trail status + // 2. FILTERS — MTF, Regime, KZ, Spread, CT-block, CHOPPY-block + // 3. SESSION — Today P&L, Daily DD bar + int panelHeight = 162; // +14 for version row + int innerY = y; + + CreateDashRect("DASH_LiveBG", x, y, g_dashWidth, panelHeight, C'18,18,35', Dash_BorderColor); + + // ── VERSION ────────────────────────────────────────────────────────── + string _verStr = EA_VERSION + " | " + _Symbol + " " + EnumToString(_Period); + CreateDashLabel("DASH_Version", x + 10, innerY + 3, _verStr, clrGold, 8, "Arial Bold"); + innerY += 14; + + // ── SECTION 1: OPEN TRADE ───────────────────────────────────────────── + CreateDashLabel("DASH_LiveTrade", x + 10, innerY + 3, + "OPEN TRADE", Dash_NeutralColor, 9, "Arial Bold"); + innerY += 18; + + // Find active MultiTPEntry for this symbol + int mIdx = -1; + double liveRR = 0; + int barsAlive = 0; + string techLabel = "---"; + string dirLabel = "---"; + string zoneLabel = "---"; + string beLabel = "---"; + string trailLabel= "---"; + color rrColor = Dash_NeutralColor; + bool anyOpen = false; + + for(int m = 0; m < ArraySize(g_multiTPEntries); m++) + { + if(!g_multiTPEntries[m].active) continue; + if(g_multiTPEntries[m].ticket <= 0) continue; + if(!PositionSelectByTicket((ulong)g_multiTPEntries[m].ticket)) continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; + mIdx = m; + anyOpen = true; + break; + } + + if(anyOpen && mIdx >= 0) + { + // --- Live RR --- + double entry = g_multiTPEntries[mIdx].entryPrice; + double sl = g_multiTPEntries[mIdx].stopLoss; + double price = (g_multiTPEntries[mIdx].direction == 1) + ? SymbolInfoDouble(_Symbol, SYMBOL_BID) + : SymbolInfoDouble(_Symbol, SYMBOL_ASK); + double slDist = MathAbs(entry - sl); + if(slDist > 0) + liveRR = (g_multiTPEntries[mIdx].direction == 1) + ? (price - entry) / slDist + : (entry - price) / slDist; + rrColor = (liveRR >= 0) ? Dash_BullColor : Dash_BearColor; + + // --- Bars alive --- + datetime openTime = (datetime)PositionGetInteger(POSITION_TIME); + barsAlive = Bars(_Symbol, PERIOD_CURRENT, openTime, TimeCurrent()); + + // --- Labels --- + string zt = g_multiTPEntries[mIdx].zoneType; + techLabel = (zt != "") ? zt : "---"; + dirLabel = (g_multiTPEntries[mIdx].direction == 1) ? "BUY" : "SELL"; + + // Zone status: armed / suppressed (TC guard) / invalidated + if(g_multiTPEntries[mIdx].zoneInvalidated) + zoneLabel = "INVALIDATED"; + else if(zt == "TC" && _Period >= PERIOD_H4 && MathAbs(liveRR) < 0.25 && barsAlive < 3) + zoneLabel = "GUARD (TC)"; // FIX#185 would suppress right now + else if(g_multiTPEntries[mIdx].zoneTop > 0) + zoneLabel = StringFormat("ARMED %.5f-%.5f", + g_multiTPEntries[mIdx].zoneBottom, + g_multiTPEntries[mIdx].zoneTop); + else + zoneLabel = "NO ZONE"; + + // BE status + double beRR = g_multiTPEntries[mIdx].perTrade_BE_RR; + if(g_multiTPEntries[mIdx].slMovedToBE) + beLabel = "ACTIVE (SL=BE)"; + else if(beRR > 0) + beLabel = StringFormat("ARM @%.2fR (%.2fR)", beRR, liveRR); + else + beLabel = "---"; + + // Trail status (FIX#184: per-trade threshold) + double trailRR = g_multiTPEntries[mIdx].perTrade_Trail_RR; + if(trailRR <= 0) trailRR = EA_Trail_Activation_RR; + if(liveRR >= trailRR) + trailLabel = StringFormat("ACTIVE (%.2fR)", liveRR); + else + trailLabel = StringFormat("PENDING @%.2fR", trailRR); + } + + // Row: Tech + Dir + RR + Bars + string tradeRow = anyOpen + ? StringFormat("%s %s RR:%+.2f Bar:%d", techLabel, dirLabel, liveRR, barsAlive) + : "No open trade"; + color tradeRowColor = anyOpen ? rrColor : Dash_NeutralColor; + CreateDashLabel("DASH_LiveRow1", x + 10, innerY, tradeRow, tradeRowColor, 9, "Consolas"); + innerY += g_dashLineHeight; + + // Row: Zone + string zoneRow = "Zone: " + zoneLabel; + color zoneColor = (!anyOpen) ? Dash_NeutralColor : + (zoneLabel == "INVALIDATED") ? Dash_BearColor : + (zoneLabel == "GUARD (TC)") ? clrOrange : + (zoneLabel == "NO ZONE") ? Dash_NeutralColor : clrLightSteelBlue; + CreateDashLabel("DASH_LiveZone", x + 10, innerY, zoneRow, zoneColor, 8, "Consolas"); + innerY += g_dashLineHeight; + + // Row: BE + Trail + string exitRow = anyOpen + ? StringFormat("BE:%s Tr:%s", g_multiTPEntries[mIdx].slMovedToBE ? "ON" : "pend", + liveRR >= (g_multiTPEntries[mIdx].perTrade_Trail_RR > 0 + ? g_multiTPEntries[mIdx].perTrade_Trail_RR + : EA_Trail_Activation_RR) ? "ON" : "pend") + : "BE:--- Trail:---"; + CreateDashLabel("DASH_LiveExit", x + 10, innerY, exitRow, Dash_TextColor, 8, "Consolas"); + innerY += g_dashLineHeight + 4; + + // ── SECTION 2: FILTERS ──────────────────────────────────────────────── + CreateDashLabel("DASH_LiveFilt", x + 10, innerY, + "FILTERS", clrGold, 9, "Arial Bold"); + innerY += 14; + + // MTF string + string mtfStr = "---"; + color mtfCol = Dash_NeutralColor; + if(MTF_Enabled) + { + switch(g_mtfAnalysis.overallDirection) + { + case MTF_STRONG_BULLISH: mtfStr="STR BULL"; mtfCol=Dash_BullColor; break; + case MTF_BULLISH: mtfStr="BULL"; mtfCol=Dash_BullColor; break; + case MTF_NEUTRAL: mtfStr="NEUTRAL"; mtfCol=Dash_NeutralColor;break; + case MTF_BEARISH: mtfStr="BEAR"; mtfCol=Dash_BearColor; break; + case MTF_STRONG_BEARISH: mtfStr="STR BEAR"; mtfCol=Dash_BearColor; break; + default: mtfStr="---"; mtfCol=Dash_NeutralColor;break; + } + } + + // Regime string + color (reuse existing GetRegimeString) + string regStr = g_regimeValid ? GetRegimeString(g_regimeData.regime) : "---"; + color regCol = Dash_NeutralColor; + if(g_regimeValid) + { + if(g_regimeData.regime==REGIME_STRONG_TREND_UP||g_regimeData.regime==REGIME_TREND_UP) + regCol = Dash_BullColor; + else if(g_regimeData.regime==REGIME_STRONG_TREND_DOWN||g_regimeData.regime==REGIME_TREND_DOWN) + regCol = Dash_BearColor; + else if(g_regimeData.regime==REGIME_CHOPPY||g_regimeData.regime==REGIME_VOLATILE) + regCol = clrOrange; + else if(g_regimeData.regime==REGIME_WEAK_TREND_UP||g_regimeData.regime==REGIME_WEAK_TREND_DOWN) + regCol = clrYellow; + } + + // Spread live + double spreadPips = (double)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * g_point / g_pipValue; + color sprdCol = (spreadPips > g_autoOptParams.max_spread_pips * 0.8) ? Dash_BearColor : + (spreadPips > g_autoOptParams.max_spread_pips * 0.5) ? clrOrange : Dash_BullColor; + + // CT-block: regime opposes MTF direction (FIX#176 logic mirror) + bool ctBlocked = false; + if(_Period >= PERIOD_H4 && g_regimeValid) + { + bool regBull = (g_regimeData.regime==REGIME_STRONG_TREND_UP||g_regimeData.regime==REGIME_TREND_UP|| + g_regimeData.regime==REGIME_WEAK_TREND_UP); + bool regBear = (g_regimeData.regime==REGIME_STRONG_TREND_DOWN||g_regimeData.regime==REGIME_TREND_DOWN|| + g_regimeData.regime==REGIME_WEAK_TREND_DOWN); + bool mtfBull = (g_mtfAnalysis.overallDirection==MTF_STRONG_BULLISH||g_mtfAnalysis.overallDirection==MTF_BULLISH); + bool mtfBear = (g_mtfAnalysis.overallDirection==MTF_STRONG_BEARISH||g_mtfAnalysis.overallDirection==MTF_BEARISH); + ctBlocked = (mtfBull && regBear) || (mtfBear && regBull); + } + bool choppyBlocked = g_regimeValid && + (g_regimeData.regime==REGIME_CHOPPY || g_regimeData.regime==REGIME_VOLATILE); + + // Row: MTF + Regime + CreateDashLabel("DASH_FMtf", x + 10, innerY, "MTF:", Dash_TextColor, 8, "Consolas"); + CreateDashLabel("DASH_FMtfV", x + 45, innerY, mtfStr, mtfCol, 8, "Consolas"); + CreateDashLabel("DASH_FReg", x + 110, innerY, "Regime:", Dash_TextColor, 8, "Consolas"); + CreateDashLabel("DASH_FRegV", x + 160, innerY, regStr, regCol, 8, "Consolas"); + innerY += g_dashLineHeight; + + // Row: KZ + Spread + string kzStr = g_isInKillzone ? g_currentKillzoneName : "None"; + color kzCol = g_isInKillzone ? clrGold : Dash_NeutralColor; + CreateDashLabel("DASH_FKZ", x + 10, innerY, "KZ:", Dash_TextColor, 8, "Consolas"); + CreateDashLabel("DASH_FKZV", x + 45, innerY, kzStr, kzCol, 8, "Consolas"); + CreateDashLabel("DASH_FSpr", x + 110, innerY, "Sprd:", Dash_TextColor, 8, "Consolas"); + CreateDashLabel("DASH_FSprV", x + 145, innerY, + StringFormat("%.1fp", spreadPips), sprdCol, 8, "Consolas"); + innerY += g_dashLineHeight; + + // Row: CT-block + CHOPPY-block + color ctCol = ctBlocked ? Dash_BearColor : Dash_BullColor; + color choppyCol = choppyBlocked ? Dash_BearColor : Dash_BullColor; + CreateDashLabel("DASH_FCT", x + 10, innerY, "CT-blk:", Dash_TextColor, 8, "Consolas"); + CreateDashLabel("DASH_FCTV", x + 60, innerY, ctBlocked ? "ON" : "off", ctCol, 8, "Consolas"); + CreateDashLabel("DASH_FChop", x + 100, innerY, "CHOP-blk:", Dash_TextColor, 8, "Consolas"); + CreateDashLabel("DASH_FChopV", x + 165, innerY, choppyBlocked ? "ON" : "off", choppyCol, 8, "Consolas"); + innerY += g_dashLineHeight + 4; + + // ── SECTION 3: SESSION P&L ──────────────────────────────────────────── + CreateDashLabel("DASH_LiveSess", x + 10, innerY, + "SESSION", clrGold, 9, "Arial Bold"); + innerY += 14; + + // Today P&L = (current balance - daily start) + open floating profit + double floatingPnL = AccountInfoDouble(ACCOUNT_PROFIT); + double closedPnL = (g_dailyStartBalance > 0) + ? AccountInfoDouble(ACCOUNT_BALANCE) - g_dailyStartBalance + : 0; + double todayTotal = closedPnL + floatingPnL; + color pnlColor = (todayTotal > 0) ? Dash_BullColor : + (todayTotal < 0) ? Dash_BearColor : Dash_NeutralColor; + + CreateDashLabel("DASH_PnLLbl", x + 10, innerY, "Today P&L:", Dash_TextColor, 8, "Consolas"); + CreateDashLabel("DASH_PnLVal", x + 95, innerY, + StringFormat("%+.2f$", todayTotal), pnlColor, 9, "Consolas"); + + // DD bar (reuse daily DD vars — already computed elsewhere) + double ddUsed = g_currentDailyDD; + double ddMax = EA_MaxDailyDrawdownPercent; + color ddCol = (ddMax > 0 && ddUsed / ddMax > 0.80) ? Dash_BearColor : + (ddMax > 0 && ddUsed / ddMax > 0.50) ? clrOrange : Dash_BullColor; + CreateDashLabel("DASH_DDLbl", x + 160, innerY, StringFormat("DD:%.1f%%", ddUsed), ddCol, 8, "Consolas"); + innerY += 12; + DrawProgressBar(x + 10, innerY, g_dashWidth - 20, 5, ddUsed, ddMax, ddCol, "_Live"); + + return y + panelHeight; +} +//+------------------------------------------------------------------+ +//| Create Dashboard Rectangle | +//+------------------------------------------------------------------+ +void CreateDashRect(string name, int x, int y, int width, int height, color bgColor, color borderColor) +{ + if(ObjectFind(0, name) < 0) + ObjectCreate(0, name, OBJ_RECTANGLE_LABEL, 0, 0, 0); + ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x); + ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y); + ObjectSetInteger(0, name, OBJPROP_XSIZE, width); + ObjectSetInteger(0, name, OBJPROP_YSIZE, height); + // * v9.16 FIX#48: Apply Dash_Transparency (was dead input -- comment said "Apply" but never did) + uint argbBg = ColorToARGB(bgColor, (uchar)Dash_Transparency); + ObjectSetInteger(0, name, OBJPROP_BGCOLOR, argbBg); + ObjectSetInteger(0, name, OBJPROP_BORDER_COLOR, borderColor); + ObjectSetInteger(0, name, OBJPROP_BORDER_TYPE, BORDER_FLAT); + ObjectSetInteger(0, name, OBJPROP_CORNER, DashboardCorner); // [v6.42] + ObjectSetInteger(0, name, OBJPROP_BACK, false); +} +//+------------------------------------------------------------------+ +//| Create Dashboard Label | +//+------------------------------------------------------------------+ +void CreateDashLabel(string name, int x, int y, string text, color clr, int fontSize, string fontName) +{ + // * v8.07 FIX: use DashboardCorner (was hardcoded CORNER_LEFT_UPPER, mismatching CreateDashRect) + if(fontSize == 9 && DashboardFontSize != 9) fontSize = DashboardFontSize; + if(ObjectFind(0, name) < 0) + ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0); + ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x); + ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y); + ObjectSetInteger(0, name, OBJPROP_CORNER, DashboardCorner); // * v8.07: match rect corner + ObjectSetString(0, name, OBJPROP_TEXT, text); + ObjectSetInteger(0, name, OBJPROP_COLOR, clr); + ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize); + ObjectSetString(0, name, OBJPROP_FONT, fontName); +} +//+------------------------------------------------------------------+ +//| Draw Progress Bar | +//+------------------------------------------------------------------+ +// * v8.07 FIX: added id parameter so multiple bars don't share the same object name +// (previously all bars used DASH_PBar_BG/Fill -> only the last drawn was visible) +void DrawProgressBar(int x, int y, int width, int height, double value, double maxValue, color fillColor, string id = "") +{ + string bgName = "DASH_PBar_BG" + id; + string fillName = "DASH_PBar_Fill" + id; + CreateDashRect(bgName, x, y, width, height, C'40,40,40', C'60,60,60'); + int fillWidth = (maxValue > 0) ? (int)(width * MathMin(value / maxValue, 1.0)) : 0; + if(fillWidth > 0) + CreateDashRect(fillName, x, y, fillWidth, height, fillColor, fillColor); + else + { + // Ensure stale fill rect is removed when value drops to 0 + if(ObjectFind(0, fillName) >= 0) ObjectDelete(0, fillName); + } +} +//+------------------------------------------------------------------+ +//| Draw Score Bar | +//+------------------------------------------------------------------+ +void DrawScoreBar(int x, int y, int width, int height, int score, int maxScore) +{ + // Background + CreateDashRect("DASH_Score_BG", x, y, width, height, C'30,30,30', C'50,50,50'); + // Color based on score -- * FIX#17: Scaled for new max=85 + color fillColor; + if(score >= 60) fillColor = clrGold; + else if(score >= 52) fillColor = Dash_BullColor; + else if(score >= 44) fillColor = clrYellowGreen; + else if(score >= 36) fillColor = clrYellow; + else if(score >= 27) fillColor = clrOrange; + else fillColor = Dash_BearColor; + // Fill + int fillWidth = (maxScore > 0) ? (int)(width * MathMin((double)score / maxScore, 1.0)) : 0; + if(fillWidth > 0) + CreateDashRect("DASH_Score_Fill", x, y, fillWidth, height, fillColor, fillColor); + // Score text + CreateDashLabel("DASH_Score_Text", x + width/2 - 10, y + 2, IntegerToString(score), clrWhite, 9, "Arial Bold"); +} +//+------------------------------------------------------------------+ +//| Draw PD Zone Bar | +//+------------------------------------------------------------------+ +void DrawPDZoneBar(int x, int y, int width, int height) +{ + // Premium (red) + CreateDashRect("DASH_PD_Premium", x, y, width/3, height, C'100,30,30', C'100,30,30'); + // Equilibrium (gray) + CreateDashRect("DASH_PD_Equil", x + width/3, y, width/3, height, C'60,60,60', C'60,60,60'); + // Discount (green) + CreateDashRect("DASH_PD_Discount", x + width*2/3, y, width/3, height, C'30,100,30', C'30,100,30'); + // Marker + int markerX = x + width/2; + if(g_currentPDZone == "PREMIUM") markerX = x + width/6; + else if(g_currentPDZone == "DISCOUNT") markerX = x + width*5/6; + CreateDashLabel("DASH_PD_Marker", markerX - 3, y - 2, "[v]", clrWhite, 8, "Arial"); +} +//+------------------------------------------------------------------+ +//| Helper Functions for Dashboard | +//+------------------------------------------------------------------+ +int CountActiveFVGs() +{ + // Safety check + if(ArraySize(FVG_Array) == 0) + { + return 0; + } + int count = 0; + for(int i = 0; i < ArraySize(FVG_Array); i++) + { + if(FVG_Array[i].status == FVG_STATUS_ACTIVE) + { + count++; + } + } + return count; +} +//+------------------------------------------------------------------+ +//| Count Active Order Blocks | +//+------------------------------------------------------------------+ +int CountActiveOBs() +{ + // Safety check + if(ArraySize(OB_Array) == 0) + { + return 0; + } + int count = 0; + for(int i = 0; i < ArraySize(OB_Array); i++) + { + // Check if status is properly set (not NULL or empty) + if(OB_Array[i].status == NULL || OB_Array[i].status == "") + { + continue; + } + // Count only ACTIVE OBs that are NOT Breaker Blocks + if(OB_Array[i].status == "ACTIVE" && !OB_Array[i].isBreakerBlock) + { + count++; + } + } + return count; +} +//+------------------------------------------------------------------+ +//| Count Active Breaker Blocks | +//+------------------------------------------------------------------+ +int CountActiveBBs() +{ + int count = 0; + // Count breakers from OB_Array + if(ArraySize(OB_Array) > 0) + { + for(int i = 0; i < ArraySize(OB_Array); i++) + { + // Check if status is properly set + if(OB_Array[i].status == NULL || OB_Array[i].status == "") + { + continue; + } + // CRITICAL: Check for BOTH conditions + // 1. isBreakerBlock flag must be true + // 2. Status must be "BREAKER" (NOT "ACTIVE") + if(OB_Array[i].isBreakerBlock && OB_Array[i].status == "BREAKER") + { + count++; + } + } + } + // Also count from dedicated BREAKER_Array + if(ArraySize(BREAKER_Array) > 0) + { + for(int i = 0; i < ArraySize(BREAKER_Array); i++) + { + if(BREAKER_Array[i].active && !BREAKER_Array[i].mitigated) + { + count++; + } + } + } + return count; +} +//+------------------------------------------------------------------+ +//| Count Active Liquidity | +//+------------------------------------------------------------------+ +int CountActiveLiquidity() +{ + // Safety check + if(ArraySize(LIQ_Array) == 0) + { + return 0; + } + int count = 0; + for(int i = 0; i < ArraySize(LIQ_Array); i++) + { + if(!LIQ_Array[i].swept) + { + count++; + } + } + return count; +} +//+------------------------------------------------------------------+ +//| Check if OTE Zone is Active | +//+------------------------------------------------------------------+ +bool IsOTEZoneActive() +{ + // Safety check + if(ArraySize(OTE_Array) == 0) + { + return false; + } + for(int i = 0; i < ArraySize(OTE_Array); i++) + { + if(OTE_Array[i].active) + { + return true; + } + } + return false; +} +//+------------------------------------------------------------------+ +//| Check for Active CRT Setup | +//+------------------------------------------------------------------+ +bool HasActiveCRTSetup() +{ + // Feature check + if(!CRT_Enabled) + { + return false; + } + // Safety check + if(ArraySize(g_crtSetups) == 0) + { + return false; + } + // Search for active setup + for(int i = 0; i < ArraySize(g_crtSetups); i++) + { + if(g_crtSetups[i].active) + { + return true; + } + } + return false; +} +//+------------------------------------------------------------------+ +//| Check for Active TBS Setup | +//+------------------------------------------------------------------+ +bool HasActiveTBSSetup() +{ + // Feature check + if(!TBS_Enabled) + { + return false; + } + // Safety check + if(ArraySize(g_tbsSetups) == 0) + { + return false; + } + // Search for active setup + for(int i = 0; i < ArraySize(g_tbsSetups); i++) + { + if(g_tbsSetups[i].active) + { + return true; + } + } + return false; +} +//+------------------------------------------------------------------+ +//| Check for Active Judas Swing | +//+------------------------------------------------------------------+ +bool HasActiveJudas() +{ + // Feature check + if(!Judas_Enabled) + { + return false; + } + // Safety check + if(ArraySize(g_judasSwings) == 0) + { + return false; + } + // Search for active swing + for(int i = 0; i < ArraySize(g_judasSwings); i++) + { + if(g_judasSwings[i].active) + { + return true; + } + } + return false; +} +//+------------------------------------------------------------------+ +//| Get AMD Phase String | +//+------------------------------------------------------------------+ +string GetAMDPhaseString() +{ + // Feature check + if(!AMD_Enabled) + { + return "-"; + } + // Data check + if(!g_amdData.active) + { + return "-"; + } + // Return phase + switch(g_amdData.phase) + { + case AMD_ACCUMULATION: + return "A"; + case AMD_MANIPULATION: + return "M"; + case AMD_DISTRIBUTION: + return "D"; + default: + return "-"; + } +} +//+------------------------------------------------------------------+ +//| Get Score Grade Color | +//+------------------------------------------------------------------+ +color GetScoreGradeColor(string grade) +{ + if(grade == "A+" || grade == "A") return clrGold; + if(grade == "B+" || grade == "B") return Dash_BullColor; + if(grade == "C") return clrYellow; + if(grade == "D") return clrOrange; + return Dash_BearColor; +} +//+------------------------------------------------------------------+ +//| DEBUGGING: Dashboard Debug Information | +//+------------------------------------------------------------------+ +void PrintDashboardDebugInfo() +{ + Print("+=======================================================+"); + Print("| DASHBOARD DEBUG INFORMATION |"); + Print("+=======================================================+"); + // FVG Info + int fvgCount = CountActiveFVGs(); + Print("| FVG: ", fvgCount, " active (Total: ", ArraySize(FVG_Array), ")"); + // OB Info + int obCount = CountActiveOBs(); + int totalOBs = ArraySize(OB_Array); + int obBreakers = 0; + for(int i = 0; i < totalOBs; i++) + if(OB_Array[i].isBreakerBlock) obBreakers++; + Print("| OB: ", obCount, " active (Total: ", totalOBs, ", Breakers: ", obBreakers, ")"); + // BB Info + int bbCount = CountActiveBBs(); + Print("| BB: ", bbCount, " active (BREAKER_Array size: ", ArraySize(BREAKER_Array), ")"); + // Liquidity + int liqCount = CountActiveLiquidity(); + Print("| LIQ: ", liqCount, " unswept (Total: ", ArraySize(LIQ_Array), ")"); + // OTE + bool oteActive = IsOTEZoneActive(); + Print("| OTE: ", oteActive ? "ACTIVE" : "INACTIVE", " (Total zones: ", ArraySize(OTE_Array), ")"); + // CRT + bool crtActive = HasActiveCRTSetup(); + Print("| CRT: ", crtActive ? "ACTIVE" : "INACTIVE", + " (Enabled: ", CRT_Enabled ? "YES" : "NO", + ", Count: ", ArraySize(g_crtSetups), ")"); + // TBS + bool tbsActive = HasActiveTBSSetup(); + Print("| TBS: ", tbsActive ? "ACTIVE" : "INACTIVE", + " (Enabled: ", TBS_Enabled ? "YES" : "NO", + ", Count: ", ArraySize(g_tbsSetups), ")"); + // Judas + bool judasActive = HasActiveJudas(); + Print("| JUDAS: ", judasActive ? "ACTIVE" : "INACTIVE", + " (Enabled: ", Judas_Enabled ? "YES" : "NO", + ", Count: ", ArraySize(g_judasSwings), ")"); + // AMD + string amdPhase = GetAMDPhaseString(); + Print("| AMD: Phase = ", amdPhase, + " (Enabled: ", AMD_Enabled ? "YES" : "NO", + ", Active: ", g_amdData.active ? "YES" : "NO", ")"); + Print("+=======================================================+"); +} +//+------------------------------------------------------------------+ +//| DEBUGGING: Detailed OB/BB Status Check | +//+------------------------------------------------------------------+ +void PrintOBBBStatus() +{ + Print("+=======================================================+"); + Print("| ORDER BLOCKS & BREAKER BLOCKS STATUS |"); + Print("+=======================================================+"); + int totalOBs = ArraySize(OB_Array); + Print("| Total OB_Array entries: ", totalOBs); + if(totalOBs > 0) + { + int activeOBs = 0; + int activeBBs = 0; + int mitigatedOBs = 0; + int brokenOBs = 0; + int otherStatus = 0; + for(int i = 0; i < totalOBs; i++) + { + string status = OB_Array[i].status; + bool isBreaker = OB_Array[i].isBreakerBlock; + if(status == "ACTIVE" && !isBreaker) + activeOBs++; + else if(status == "BREAKER" && isBreaker) + activeBBs++; + else if(status == "MITIGATED") + mitigatedOBs++; + else if(status == "BROKEN") + brokenOBs++; + else + otherStatus++; + } + Print("| - ACTIVE OBs: ", activeOBs); + Print("| - BREAKER BBs: ", activeBBs); + Print("| - MITIGATED: ", mitigatedOBs); + Print("| - BROKEN: ", brokenOBs); + Print("| - OTHER: ", otherStatus); + } + int totalBBs = ArraySize(BREAKER_Array); + Print("| Total BREAKER_Array entries: ", totalBBs); + if(totalBBs > 0) + { + int activeBBs = 0; + int mitigatedBBs = 0; + for(int i = 0; i < totalBBs; i++) + { + if(BREAKER_Array[i].active && !BREAKER_Array[i].mitigated) + activeBBs++; + else if(BREAKER_Array[i].mitigated) + mitigatedBBs++; + } + Print("| - ACTIVE BBs: ", activeBBs); + Print("| - MITIGATED BBs: ", mitigatedBBs); + } + Print("+=======================================================+"); +} +//+------------------------------------------------------------------+ +//| Cleanup Dashboard | +//+------------------------------------------------------------------+ +void CleanupDashboard() +{ + ObjectsDeleteAll(0, "DASH_"); + ObjectsDeleteAll(0, "ICT_Dash_"); // * v8.06: legacy dashboard labels (UpdateDashboard / CreateLabel) + ObjectsDeleteAll(0, "ICT_DASH_"); // * v8.06: MTF overlay labels (CreateLabelEx "ICT_DASH_MTF_") +} +//+------------------------------------------------------------------+ +//| Update Signal Visual | +//+------------------------------------------------------------------+ +void UpdateSignalVisual(int index, string status) +{ + if(index < 0 || index >= ArraySize(SIGNAL_Array)) return; + SIGNAL_Array[index].status = status; + string objName = "ICT_Signal_" + IntegerToString(SIGNAL_Array[index].id); + if(ObjectFind(0, objName) >= 0) + { + color signalColor; + if(status == "WIN") + signalColor = clrLime; + else if(status == "LOSS") + signalColor = clrRed; + else if(status == "EXPIRED") + signalColor = ExpiredSignalColor; + else + signalColor = SIGNAL_Array[index].isBullish ? SIGNAL_BuyColor : SIGNAL_SellColor; + ObjectSetInteger(0, objName, OBJPROP_COLOR, signalColor); + } +} +//+------------------------------------------------------------------+ +//| Update Performance Stats from Signal | +//+------------------------------------------------------------------+ +void UpdatePerformanceStats(SIGNAL_Struct &signal, bool isWin) +{ + if(isWin) + { + g_totalWins++; + g_totalProfitPips += signal.pnlPips; + if(signal.entryQuality >= 80) + g_highQualityWins++; + else if(signal.entryQuality >= 60) + g_mediumQualityWins++; + else + g_lowQualityWins++; + } + else + { + g_totalLosses++; + g_totalLossPips += MathAbs(signal.pnlPips); + if(signal.entryQuality >= 80) + g_highQualityLosses++; + else if(signal.entryQuality >= 60) + g_mediumQualityLosses++; + else + g_lowQualityLosses++; + } + int stratIdx = GetStrategyIndex(signal.strategy); + if(stratIdx >= 0) + { + g_strategyPerf[stratIdx].totalTrades++; + if(isWin) + { + g_strategyPerf[stratIdx].wins++; + g_strategyPerf[stratIdx].totalProfitPips += signal.pnlPips; + } + else + { + g_strategyPerf[stratIdx].losses++; + g_strategyPerf[stratIdx].totalLossPips += MathAbs(signal.pnlPips); + } + int total = g_strategyPerf[stratIdx].wins + g_strategyPerf[stratIdx].losses; + if(total > 0) + { + g_strategyPerf[stratIdx].winRate = (double)g_strategyPerf[stratIdx].wins / total * 100.0; + } + // * v10.29 FIX#316: per-regime tracking + // Map current regime to index: 0=trending, 1=ranging, 2=choppy, 3=weak, 4=volatile, 5=breakout + int rIdx = -1; + if(g_regimeValid) + { + ENUM_MARKET_REGIME reg = g_regimeData.regime; + if(reg == REGIME_STRONG_TREND_UP || reg == REGIME_STRONG_TREND_DOWN || + reg == REGIME_TREND_UP || reg == REGIME_TREND_DOWN) rIdx = 0; + else if(reg == REGIME_RANGING || reg == REGIME_RANGING_TIGHT || + reg == REGIME_RANGING_WIDE) rIdx = 1; + else if(reg == REGIME_CHOPPY) rIdx = 2; + else if(reg == REGIME_WEAK_TREND_UP || reg == REGIME_WEAK_TREND_DOWN) rIdx = 3; + else if(reg == REGIME_VOLATILE) rIdx = 4; + else if(reg == REGIME_BREAKOUT) rIdx = 5; + } + if(rIdx >= 0 && rIdx < 6) + { + g_strategyPerf[stratIdx].total_regime[rIdx]++; + if(isWin) g_strategyPerf[stratIdx].wins_regime[rIdx]++; + int rtotal = g_strategyPerf[stratIdx].total_regime[rIdx]; + g_strategyPerf[stratIdx].wr_regime[rIdx] = (rtotal > 0) + ? (double)g_strategyPerf[stratIdx].wins_regime[rIdx] / rtotal * 100.0 + : 50.0; + } + } + g_tradesThisDay++; +} +//+------------------------------------------------------------------+ +//| Chart Event Handler | +//+------------------------------------------------------------------+ +void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) +{ + // Handle click events for interactive elements + if(id == CHARTEVENT_OBJECT_CLICK) + { + // * v8.07 FIX: Minimize/restore button was never handled -- dashboard couldn't be collapsed + if(sparam == "DASH_MinBtn") + { + g_dashMinimized = !g_dashMinimized; + if(Dash_Enabled) UpdateProfessionalDashboard(); + ChartRedraw(0); + } + else if(StringFind(sparam, "ICT_Signal_") >= 0) + { + HandleSignalClick(sparam); + } + else if(StringFind(sparam, "ICT_BTN_") >= 0) + { + HandleButtonClick(sparam); + } + } + // Handle keyboard shortcuts + if(id == CHARTEVENT_KEYDOWN) + { + HandleKeyPress((int)lparam); + } +} +//+------------------------------------------------------------------+ +//| Handle Signal Click | +//+------------------------------------------------------------------+ +void HandleSignalClick(string objName) +{ + // Extract signal ID from object name + string idStr = StringSubstr(objName, 11); // After "ICT_Signal_" + long signalId = StringToInteger(idStr); + // Find signal + for(int i = 0; i < ArraySize(SIGNAL_Array); i++) + { + if(SIGNAL_Array[i].id == signalId) + { + // Show signal details + DisplaySignalDetails(i); + break; + } + } +} +//+------------------------------------------------------------------+ +//| Show Signal Details | +//+------------------------------------------------------------------+ +void DisplaySignalDetails(int signalIndex) +{ + if(!ShowSignalDetails) return; // [v6.42] + if(signalIndex < 0 || signalIndex >= ArraySize(SIGNAL_Array)) return; + SIGNAL_Struct signal = SIGNAL_Array[signalIndex]; + string kzName = "NONE"; + if(signal.killzone != KZ_NONE) + { + for(int k = 0; k < g_numKillzones; k++) + { + if(g_killzoneDefinitions[k].type == signal.killzone) + { + kzName = g_killzoneDefinitions[k].name; + break; + } + } + } + string details = ""; + details += "=======================================\n"; + details += StringFormat("SIGNAL DETAILS (ID: %I64d)\n", signal.id); + details += "=======================================\n"; + details += StringFormat("Strategy: %s\n", signal.strategy); + details += StringFormat("Direction: %s\n", signal.isBullish ? "BUY" : "SELL"); + details += StringFormat("Entry: %.5f\n", signal.entryPrice); + details += StringFormat("Stop Loss: %.5f\n", signal.stopLoss); + details += StringFormat("Take Profit: %.5f\n", signal.takeProfit); + details += StringFormat("Risk/Reward: %.2f\n", signal.riskReward); + details += StringFormat("Quality: %.0f%%\n", signal.entryQuality); + details += StringFormat("Confluence: %.2f\n", signal.confluence); + details += StringFormat("Status: %s\n", signal.status); + details += "---------------------------------------\n"; + details += StringFormat("Killzone: %s\n", kzName); + details += StringFormat("ML Confidence: %.1f%%\n", signal.mlConfidence * 100); + details += StringFormat("Structure Aligned: %s\n", signal.structureAligned ? "YES" : "NO"); + details += StringFormat("PD Zone Aligned: %s\n", signal.pdZoneAligned ? "YES" : "NO"); + details += StringFormat("Market Phase: %s\n", signal.marketPhase); + details += "======================================="; + Print(details); + Comment(details); +} +//+------------------------------------------------------------------+ +//| Handle Button Click - ENHANCED VERSION | +//+------------------------------------------------------------------+ +void HandleButtonClick(string objName) +{ + Print("[MOUSE] Button clicked: ", objName); + if(objName == "ICT_BTN_SaveData") + { + Print("[SAVE] Saving performance data..."); + if(EnableDataPersistence) + { + if(SavePerformanceData()) + { + Alert("[OK] Performance data saved successfully!"); + Comment("Data saved at: ", TimeToString(TimeCurrent(), TIME_DATE|TIME_MINUTES)); + } + else + { + Alert("[X] Failed to save performance data!"); + } + } + else + { + Alert("[WARN] Data persistence is disabled!"); + } + } + else if(objName == "ICT_BTN_ExportCSV") + { + Print("[CHART] Exporting trade history..."); + if(EnableTradeJournal) + { + if(ExportTradeHistory()) + { + Alert("[OK] Trade history exported to CSV!"); + Comment("CSV exported: ", g_tradeHistoryCount, " trades"); + } + else + { + Alert("[X] Failed to export trade history!"); + } + } + else + { + Alert("[WARN] Trade journal is disabled!"); + } + } + else if(objName == "ICT_BTN_TrainML") + { + if(EnableML && g_nnInitialized) + { + Print("[BOT] Starting manual ML training..."); + Alert("Training Neural Network... Please wait..."); + // Get current price arrays for training + datetime time[]; + double open[], high[], low[], close[]; + long volume[]; + ArraySetAsSeries(time, true); + ArraySetAsSeries(open, true); + ArraySetAsSeries(high, true); + ArraySetAsSeries(low, true); + ArraySetAsSeries(close, true); + ArraySetAsSeries(volume, true); + int copied = CopyTime(_Symbol, _Period, 0, 1000, time); + if(copied > 0) + { + CopyOpen(_Symbol, _Period, 0, copied, open); + CopyHigh(_Symbol, _Period, 0, copied, high); + CopyLow(_Symbol, _Period, 0, copied, low); + CopyClose(_Symbol, _Period, 0, copied, close); + CopyTickVolume(_Symbol, _Period, 0, copied, volume); + if(TrainNeuralNetwork(time, open, high, low, close, volume)) + { + Alert("[OK] Neural Network training complete!"); + PrintFormat("Training Loss: %.6f", g_nnTrainingLoss); + } + else + { + Alert("[X] Neural Network training failed!"); + } + } + else + { + Alert("[X] Failed to get price data for training!"); + } + } + else + { + Alert("[WARN] ML is not enabled or not initialized!"); + } + } + else if(objName == "ICT_BTN_PrintStats") + { + Print("[UP] Printing performance statistics..."); + if(g_perfData.totalTrades > 0) + { + Print(GetPerformanceSummary()); + PrintStrategyRanking(); + Comment(GetPerformanceSummary()); + } + else + { + Alert("[WARN] No trade statistics available yet!"); + Comment("No trades recorded yet"); + } + } + ChartRedraw(0); +} +//+------------------------------------------------------------------+ +//| Handle Key Press - ENHANCED VERSION | +//+------------------------------------------------------------------+ +void HandleKeyPress(int key) +{ + Print("[KEY] Key pressed: ", key); + // 'D' (68) - Toggle Dashboard + if(key == 68 || key == 100) + { + g_workingShowDashboard = !g_workingShowDashboard; + if(g_workingShowDashboard) + { + UpdateDashboard(); + Comment("Dashboard: ON"); + } + else + { + ObjectsDeleteAll(0, "ICT_Dash_"); + Comment("Dashboard: OFF"); + } + ChartRedraw(0); + } + // 'K' (75) - Toggle Killzones + else if(key == 75 || key == 107) + { + if(EnableKillzones) + { + g_workingKZ_ShowBoxes = !g_workingKZ_ShowBoxes; + if(g_workingKZ_ShowBoxes) + { + Comment("Killzones: ON"); + } + else + { + DeleteKillzoneObjects(); + Comment("Killzones: OFF"); + } + ChartRedraw(0); + } + else + { + Alert("[WARN] Killzones feature is disabled!"); + } + } + // 'S' (83) - Save Data + else if(key == 83 || key == 115) + { + if(EnableDataPersistence) + { + Print("[SAVE] Manual save triggered..."); + if(SavePerformanceData()) + { + Alert("[OK] Data saved successfully!"); + Comment("Data saved: ", TimeToString(TimeCurrent(), TIME_DATE|TIME_MINUTES)); + } + } + else + { + Alert("[WARN] Data persistence is disabled!"); + } + } + // 'P' (80) - Print Performance + else if(key == 80 || key == 112) + { + if(g_perfData.totalTrades > 0) + { + Print(GetPerformanceSummary()); + PrintStrategyRanking(); + Comment(GetPerformanceSummary()); + } + else + { + Comment("No performance data available"); + } + } + // 'B' (66) - Toggle Buttons + else if(key == 66 || key == 98) + { + if(!g_buttonsCreated) + { + CreateControlButtons(); + Comment("Control Buttons: ON"); + } + else + { + DeleteControlButtons(); + Comment("Control Buttons: OFF"); + } + ChartRedraw(0); + } + // 'F' (70) - Toggle FVG Display + else if(key == 70 || key == 102) + { + // [OK] ΔΙΟΡΘΩΣΗ - Χρησιμοποίησε την global variable + g_fvgDisplayEnabled = !g_fvgDisplayEnabled; + if(!g_fvgDisplayEnabled) + { + // Delete all FVG objects + for(int i = 0; i < ArraySize(FVG_Array); i++) + { + DeleteFVGObjects(i); + } + } + Comment("FVG Display: ", g_fvgDisplayEnabled ? "ON" : "OFF"); + ChartRedraw(0); + } + // 'O' (79) - Toggle OB Display + else if(key == 79 || key == 111) + { + // [OK] ΔΙΟΡΘΩΣΗ - Χρησιμοποίησε την global variable + g_obDisplayEnabled = !g_obDisplayEnabled; + if(!g_obDisplayEnabled) + { + ObjectsDeleteAll(0, "ICT_OB_"); + } + Comment("Order Blocks Display: ", g_obDisplayEnabled ? "ON" : "OFF"); + ChartRedraw(0); + } + // 'L' (76) - Toggle Liquidity Display + else if(key == 76 || key == 108) + { + // [OK] ΔΙΟΡΘΩΣΗ - Χρησιμοποίησε την global variable + g_liqDisplayEnabled = !g_liqDisplayEnabled; + if(!g_liqDisplayEnabled) + { + ObjectsDeleteAll(0, "ICT_LIQ_"); + } + Comment("Liquidity Display: ", g_liqDisplayEnabled ? "ON" : "OFF"); + ChartRedraw(0); + } + // 'H' (72) - Show Help + else if(key == 72 || key == 104) + { + string help = "===================================\n"; + help += "[LIST] KEYBOARD SHORTCUTS\n"; + help += "===================================\n"; + help += "D - Toggle Dashboard\n"; + help += "K - Toggle Killzones\n"; + help += "S - Save Data\n"; + help += "P - Print Performance\n"; + help += "B - Toggle Buttons\n"; + help += "F - Toggle FVG Display\n"; + help += "O - Toggle OB Display\n"; + help += "L - Toggle Liquidity Display\n"; + help += "H - Show this Help\n"; + help += "==================================="; + Print(help); + Comment(help); + } +} +//+------------------------------------------------------------------+ +//| [NEW] UI INTERACTION SYSTEM - COMPLETE IMPLEMENTATION | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| Create Control Buttons | +//+------------------------------------------------------------------+ +void CreateControlButtons() +{ + if(g_buttonsCreated) return; + int x = 10; + int y = 400; + int width = 120; + int height = 25; + int spacing = 30; + // Save Data Button + if(ObjectFind(0, "ICT_BTN_SaveData") < 0) + { + ObjectCreate(0, "ICT_BTN_SaveData", OBJ_BUTTON, 0, 0, 0); + ObjectSetInteger(0, "ICT_BTN_SaveData", OBJPROP_XDISTANCE, x); + ObjectSetInteger(0, "ICT_BTN_SaveData", OBJPROP_YDISTANCE, y); + ObjectSetInteger(0, "ICT_BTN_SaveData", OBJPROP_XSIZE, width); + ObjectSetInteger(0, "ICT_BTN_SaveData", OBJPROP_YSIZE, height); + ObjectSetString(0, "ICT_BTN_SaveData", OBJPROP_TEXT, "[SAVE] Save Data"); + ObjectSetInteger(0, "ICT_BTN_SaveData", OBJPROP_COLOR, clrWhite); + ObjectSetInteger(0, "ICT_BTN_SaveData", OBJPROP_BGCOLOR, clrDarkGreen); + ObjectSetInteger(0, "ICT_BTN_SaveData", OBJPROP_CORNER, CORNER_LEFT_LOWER); + } + y += spacing; + // Export CSV Button + if(ObjectFind(0, "ICT_BTN_ExportCSV") < 0) + { + ObjectCreate(0, "ICT_BTN_ExportCSV", OBJ_BUTTON, 0, 0, 0); + ObjectSetInteger(0, "ICT_BTN_ExportCSV", OBJPROP_XDISTANCE, x); + ObjectSetInteger(0, "ICT_BTN_ExportCSV", OBJPROP_YDISTANCE, y); + ObjectSetInteger(0, "ICT_BTN_ExportCSV", OBJPROP_XSIZE, width); + ObjectSetInteger(0, "ICT_BTN_ExportCSV", OBJPROP_YSIZE, height); + ObjectSetString(0, "ICT_BTN_ExportCSV", OBJPROP_TEXT, "[CHART] Export CSV"); + ObjectSetInteger(0, "ICT_BTN_ExportCSV", OBJPROP_COLOR, clrWhite); + ObjectSetInteger(0, "ICT_BTN_ExportCSV", OBJPROP_BGCOLOR, clrDarkBlue); + ObjectSetInteger(0, "ICT_BTN_ExportCSV", OBJPROP_CORNER, CORNER_LEFT_LOWER); + } + y += spacing; + // Train ML Button + if(EnableML && ObjectFind(0, "ICT_BTN_TrainML") < 0) + { + ObjectCreate(0, "ICT_BTN_TrainML", OBJ_BUTTON, 0, 0, 0); + ObjectSetInteger(0, "ICT_BTN_TrainML", OBJPROP_XDISTANCE, x); + ObjectSetInteger(0, "ICT_BTN_TrainML", OBJPROP_YDISTANCE, y); + ObjectSetInteger(0, "ICT_BTN_TrainML", OBJPROP_XSIZE, width); + ObjectSetInteger(0, "ICT_BTN_TrainML", OBJPROP_YSIZE, height); + ObjectSetString(0, "ICT_BTN_TrainML", OBJPROP_TEXT, "[BOT] Train ML"); + ObjectSetInteger(0, "ICT_BTN_TrainML", OBJPROP_COLOR, clrWhite); + ObjectSetInteger(0, "ICT_BTN_TrainML", OBJPROP_BGCOLOR, clrDarkOrange); + ObjectSetInteger(0, "ICT_BTN_TrainML", OBJPROP_CORNER, CORNER_LEFT_LOWER); + } + y += spacing; + // Print Stats Button + if(ObjectFind(0, "ICT_BTN_PrintStats") < 0) + { + ObjectCreate(0, "ICT_BTN_PrintStats", OBJ_BUTTON, 0, 0, 0); + ObjectSetInteger(0, "ICT_BTN_PrintStats", OBJPROP_XDISTANCE, x); + ObjectSetInteger(0, "ICT_BTN_PrintStats", OBJPROP_YDISTANCE, y); + ObjectSetInteger(0, "ICT_BTN_PrintStats", OBJPROP_XSIZE, width); + ObjectSetInteger(0, "ICT_BTN_PrintStats", OBJPROP_YSIZE, height); + ObjectSetString(0, "ICT_BTN_PrintStats", OBJPROP_TEXT, "[UP] Print Stats"); + ObjectSetInteger(0, "ICT_BTN_PrintStats", OBJPROP_COLOR, clrWhite); + ObjectSetInteger(0, "ICT_BTN_PrintStats", OBJPROP_BGCOLOR, clrDarkSlateGray); + ObjectSetInteger(0, "ICT_BTN_PrintStats", OBJPROP_CORNER, CORNER_LEFT_LOWER); + } + g_buttonsCreated = true; +} +//+------------------------------------------------------------------+ +//| Delete Control Buttons | +//+------------------------------------------------------------------+ +void DeleteControlButtons() +{ + ObjectDelete(0, "ICT_BTN_SaveData"); + ObjectDelete(0, "ICT_BTN_ExportCSV"); + ObjectDelete(0, "ICT_BTN_TrainML"); + ObjectDelete(0, "ICT_BTN_PrintStats"); + g_buttonsCreated = false; +} +//+------------------------------------------------------------------+ +//| =============================================================== | +//| [RULER] FULL FIBONACCI SYSTEM | +//| =============================================================== | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| Initialize Full Fibonacci | +//+------------------------------------------------------------------+ +void InitializeFullFibonacci() +{ + // Initialize Fibonacci data structure + g_fibData.swingHighTime = 0; + g_fibData.swingLowTime = 0; + g_fibData.swingHigh = 0; + g_fibData.swingLow = 0; + g_fibData.range = 0; + g_fibData.isUptrend = true; + g_fibData.isValid = false; + g_fibData.age = 0; + g_fibData.lastUpdate = 0; + // Calculate number of levels based on style + int levelCount = 0; + switch(FIB_Style) { + // [v6.42] FIB_Mode determines auto/manual/structure approach + case FIB_STYLE_FULL: + levelCount = ArraySize(g_fibStandardLevels); + if(FIB_Extensions != FIB_EXT_NONE) + levelCount += ArraySize(g_fibExtensionLevels); + break; + case FIB_STYLE_ICT: + levelCount = ArraySize(g_fibICTLevels); + break; + case FIB_STYLE_CUSTOM: + levelCount = 9; // Standard levels + break; + } + ArrayResize(g_fibData.levels, levelCount); + g_fibLevelCount = levelCount; + g_fibInitialized = true; + if(g_verboseLog) { + Print("[RULER] Fibonacci: ", levelCount, " levels configured"); + } +} +//+------------------------------------------------------------------+ +//| Detect Fibonacci Swing Points | +//+------------------------------------------------------------------+ +void DetectFibonacciSwings(const datetime &time[], const double &high[], + const double &low[], const double &close[], int limit) +{ + if(!FIB_Enabled || !g_fibInitialized) return; + // * v9.16 FIX#48: FIB_Mode was declared but never used + // FIB_MODE_MANUAL: don't auto-detect -- user sets Fib manually on chart + if(FIB_Mode == FIB_MODE_MANUAL && g_fibData.isValid) return; + // FIB_MODE_STRUCTURE: use market structure direction to set Fib orientation + // Instead of pure price swings, align Fib with BOS/CHoCH structural bias + // (FIB_MODE_AUTO falls through to standard swing detection below) + // FIB_MODE_AUTO: standard swing detection (existing code below) + int swingStrength = FIB_SwingStrength; + int lookback = MathMin(g_workingFIB_Lookback, limit); + double swingHigh = 0; + double swingLow = DBL_MAX; + datetime swingHighTime = 0; + datetime swingLowTime = 0; + int swingHighIndex = -1; + int swingLowIndex = -1; + // =========================================================== + // FIND SWING HIGH + // =========================================================== + for(int i = swingStrength; i < lookback - swingStrength; i++) + { + bool isSwingHigh = true; + for(int j = 1; j <= swingStrength; j++) + { + if(high[i] <= high[i-j] || high[i] <= high[i+j]) { + isSwingHigh = false; + break; + } + } + if(isSwingHigh && high[i] > swingHigh) { + swingHigh = high[i]; + swingHighTime = time[i]; + swingHighIndex = i; + } + } + // =========================================================== + // FIND SWING LOW + // =========================================================== + for(int i = swingStrength; i < lookback - swingStrength; i++) + { + bool isSwingLow = true; + for(int j = 1; j <= swingStrength; j++) + { + if(low[i] >= low[i-j] || low[i] >= low[i+j]) { + isSwingLow = false; + break; + } + } + if(isSwingLow && low[i] < swingLow) { + swingLow = low[i]; + swingLowTime = time[i]; + swingLowIndex = i; + } + } + // =========================================================== + // VALIDATE AND UPDATE + // =========================================================== + if(swingHigh > 0 && swingLow < DBL_MAX && swingHigh > swingLow) + { + // Check if significantly different from previous + bool shouldUpdate = false; + if(!g_fibData.isValid) { + shouldUpdate = true; + } + else if(FIB_AutoUpdate) { + double priceDiff = MathAbs(swingHigh - g_fibData.swingHigh) + + MathAbs(swingLow - g_fibData.swingLow); + if(priceDiff > g_cachedATR * 0.5) { + shouldUpdate = true; + } + } + if(shouldUpdate) { + g_fibData.swingHigh = swingHigh; + g_fibData.swingLow = swingLow; + g_fibData.swingHighTime = swingHighTime; + g_fibData.swingLowTime = swingLowTime; + g_fibData.range = swingHigh - swingLow; + g_fibData.isUptrend = (swingHighIndex < swingLowIndex); + // * v9.16 FIX#48: FIB_MODE_STRUCTURE -- override trend direction with BOS/CHoCH structure + if(FIB_Mode == FIB_MODE_STRUCTURE) + g_fibData.isUptrend = g_isBullishStructure; + g_fibData.isValid = true; + g_fibData.age = 0; + g_fibData.lastUpdate = TimeCurrent(); + // Calculate all Fibonacci levels + CalculateFibonacciLevels(); + // Draw on chart + DrawFullFibonacci(time); + if(g_verboseLog) { + Print("[RULER] Fibonacci updated: High=", swingHigh, + " Low=", swingLow, " Trend=", g_fibData.isUptrend ? "UP" : "DOWN"); + } + } + } + // Update current price Fib level + if(g_fibData.isValid && g_fibData.range > 0) + { + double currentPrice = close[0]; + if(g_fibData.isUptrend) { + g_fibCurrentLevel = (currentPrice - g_fibData.swingLow) / g_fibData.range; + } else { + g_fibCurrentLevel = (g_fibData.swingHigh - currentPrice) / g_fibData.range; + } + } + g_fibData.age++; +} +//+------------------------------------------------------------------+ +//| Calculate Fibonacci Levels | +//+------------------------------------------------------------------+ +void CalculateFibonacciLevels() +{ + if(!g_fibData.isValid) return; + int idx = 0; + double range = g_fibData.range; + bool isUptrend = g_fibData.isUptrend; + // =========================================================== + // STANDARD LEVELS + // =========================================================== + double levels[]; + switch(FIB_Style) { + case FIB_STYLE_FULL: + ArrayCopy(levels, g_fibStandardLevels); + break; + case FIB_STYLE_ICT: + ArrayCopy(levels, g_fibICTLevels); + break; + case FIB_STYLE_CUSTOM: + ArrayCopy(levels, g_fibStandardLevels); + break; + } + for(int i = 0; i < ArraySize(levels) && idx < g_fibLevelCount; i++) + { + double level = levels[i]; + double price; + if(isUptrend) { + price = g_fibData.swingLow + (range * level); + } else { + price = g_fibData.swingHigh - (range * level); + } + g_fibData.levels[idx].level = level; + g_fibData.levels[idx].price = price; + g_fibData.levels[idx].label = GetFibLevelLabel(level); + g_fibData.levels[idx].lineColor = GetFibLevelColor(level); + g_fibData.levels[idx].lineStyle = GetFibLevelStyle(level); + g_fibData.levels[idx].lineWidth = GetFibLevelWidth(level); + g_fibData.levels[idx].isICT = IsICTLevel(level); + g_fibData.levels[idx].isExtension = false; + idx++; + } + // =========================================================== + // EXTENSION LEVELS + // =========================================================== + if(FIB_Extensions != FIB_EXT_NONE && FIB_Style == FIB_STYLE_FULL) + { + int extCount = (FIB_Extensions == FIB_EXT_FULL) ? + ArraySize(g_fibExtensionLevels) : 2; + for(int i = 0; i < extCount && idx < g_fibLevelCount; i++) + { + double level = g_fibExtensionLevels[i]; + double price; + if(isUptrend) { + price = g_fibData.swingLow + (range * level); + } else { + price = g_fibData.swingHigh - (range * level); + } + g_fibData.levels[idx].level = level; + g_fibData.levels[idx].price = price; + g_fibData.levels[idx].label = GetFibLevelLabel(level); + g_fibData.levels[idx].lineColor = FIB_Color_Extensions; + g_fibData.levels[idx].lineStyle = STYLE_DASHDOT; + g_fibData.levels[idx].lineWidth = 1; + g_fibData.levels[idx].isICT = false; + g_fibData.levels[idx].isExtension = true; + idx++; + } + } +} +//+------------------------------------------------------------------+ +//| Get Fibonacci Level Label | +//+------------------------------------------------------------------+ +string GetFibLevelLabel(double level) +{ + string label = DoubleToString(level * 100, 1) + "%"; + // Add ICT labels + if(MathAbs(level - 0.5) < 0.001) label += " (EQ)"; + if(MathAbs(level - 0.618) < 0.001) label += " (OTE)"; + if(MathAbs(level - 0.705) < 0.001) label += " (OPTIMAL)"; + if(MathAbs(level - 0.786) < 0.001) label += " (OTE)"; + // Extension labels + if(MathAbs(level - 1.272) < 0.001) label += " (EXT)"; + if(MathAbs(level - 1.618) < 0.001) label += " (GOLDEN)"; + return label; +} +//+------------------------------------------------------------------+ +//| Get Fibonacci Level Color | +//+------------------------------------------------------------------+ +color GetFibLevelColor(double level) +{ + if(MathAbs(level - 0.0) < 0.001) return FIB_Color_0; + if(MathAbs(level - 0.236) < 0.001) return FIB_Color_236; + if(MathAbs(level - 0.382) < 0.001) return FIB_Color_382; + if(MathAbs(level - 0.5) < 0.001) return FIB_Color_500; + if(MathAbs(level - 0.618) < 0.001) return FIB_Color_618; + if(MathAbs(level - 0.705) < 0.001) return FIB_Color_705; + if(MathAbs(level - 0.786) < 0.001) return FIB_Color_786; + if(MathAbs(level - 1.0) < 0.001) return FIB_Color_100; + if(level > 1.0) return FIB_Color_Extensions; + return clrGray; +} +//+------------------------------------------------------------------+ +//| Get Fibonacci Level Style | +//+------------------------------------------------------------------+ +int GetFibLevelStyle(double level) +{ + // OTE levels get solid lines + if(MathAbs(level - 0.618) < 0.001 || + MathAbs(level - 0.705) < 0.001 || + MathAbs(level - 0.786) < 0.001) { + return STYLE_SOLID; + } + // 50% EQ gets dash + if(MathAbs(level - 0.5) < 0.001) { + return STYLE_DASH; + } + // 0% and 100% get solid + if(MathAbs(level) < 0.001 || MathAbs(level - 1.0) < 0.001) { + return STYLE_SOLID; + } + return STYLE_DOT; +} +//+------------------------------------------------------------------+ +//| Get Fibonacci Level Width | +//+------------------------------------------------------------------+ +int GetFibLevelWidth(double level) +{ + // OTE levels get thicker lines + if(MathAbs(level - 0.618) < 0.001 || + MathAbs(level - 0.705) < 0.001 || + MathAbs(level - 0.786) < 0.001) { + return 2; + } + // 50% EQ gets medium + if(MathAbs(level - 0.5) < 0.001) { + return 2; + } + // 0% and 100% get medium + if(MathAbs(level) < 0.001 || MathAbs(level - 1.0) < 0.001) { + return 2; + } + return 1; +} +//+------------------------------------------------------------------+ +//| Check if ICT Important Level | +//+------------------------------------------------------------------+ +bool IsICTLevel(double level) +{ + return (MathAbs(level - 0.5) < 0.001 || + MathAbs(level - 0.618) < 0.001 || + MathAbs(level - 0.705) < 0.001 || + MathAbs(level - 0.786) < 0.001); +} +//+------------------------------------------------------------------+ +//| Draw Full Fibonacci on Chart | +//+------------------------------------------------------------------+ +void DrawFullFibonacci(const datetime &time[]) +{ + if(!g_fibData.isValid) return; + // Delete old objects + DeleteFibonacciObjects(); + datetime startTime = g_fibData.isUptrend ? g_fibData.swingLowTime : g_fibData.swingHighTime; + datetime endTime = time[0] + PeriodSeconds(_Period) * FIB_ExtendRight; + // =========================================================== + // DRAW OTE ZONE BACKGROUND (if enabled) + // =========================================================== + if(FIB_HighlightOTE) + { + double oteHigh = 0, oteLow = 0; + for(int i = 0; i < g_fibLevelCount; i++) + { + if(MathAbs(g_fibData.levels[i].level - 0.618) < 0.001) { + oteHigh = g_fibData.levels[i].price; + } + if(MathAbs(g_fibData.levels[i].level - 0.786) < 0.001) { + oteLow = g_fibData.levels[i].price; + } + } + if(oteHigh > 0 && oteLow > 0) + { + if(oteHigh < oteLow) { + double temp = oteHigh; + oteHigh = oteLow; + oteLow = temp; + } + string oteName = "ICT_FIB_OTE_ZONE"; + ObjectCreate(0, oteName, OBJ_RECTANGLE, 0, startTime, oteHigh, endTime, oteLow); + ObjectSetInteger(0, oteName, OBJPROP_COLOR, FIB_OTE_ZoneColor); + ObjectSetInteger(0, oteName, OBJPROP_FILL, true); + ObjectSetInteger(0, oteName, OBJPROP_BACK, true); + ObjectSetInteger(0, oteName, OBJPROP_SELECTABLE, false); + ObjectSetInteger(0, oteName, OBJPROP_HIDDEN, true); + } + } + // =========================================================== + // DRAW FIBONACCI LEVELS + // =========================================================== + for(int i = 0; i < g_fibLevelCount; i++) + { + string lineName = "ICT_FIB_LINE_" + IntegerToString(i); + ObjectCreate(0, lineName, OBJ_TREND, 0, + startTime, g_fibData.levels[i].price, + endTime, g_fibData.levels[i].price); + ObjectSetInteger(0, lineName, OBJPROP_COLOR, g_fibData.levels[i].lineColor); + ObjectSetInteger(0, lineName, OBJPROP_STYLE, g_fibData.levels[i].lineStyle); + ObjectSetInteger(0, lineName, OBJPROP_WIDTH, g_fibData.levels[i].lineWidth); + ObjectSetInteger(0, lineName, OBJPROP_RAY_RIGHT, true); + ObjectSetInteger(0, lineName, OBJPROP_BACK, false); + ObjectSetInteger(0, lineName, OBJPROP_SELECTABLE, false); + ObjectSetInteger(0, lineName, OBJPROP_HIDDEN, true); + // Draw label + if(FIB_ShowLabels && FIB_ShowCurrentPrice) // [v6.42] + { + string labelName = "ICT_FIB_LABEL_" + IntegerToString(i); + string labelText = g_fibData.levels[i].label; + if(FIB_ShowPrices) { + labelText += " [" + DoubleToString(g_fibData.levels[i].price, _Digits) + "]"; + } + ObjectCreate(0, labelName, OBJ_TEXT, 0, endTime, g_fibData.levels[i].price); + ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); + ObjectSetInteger(0, labelName, OBJPROP_COLOR, g_fibData.levels[i].lineColor); + ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 8); + ObjectSetString(0, labelName, OBJPROP_FONT, "Arial"); + ObjectSetInteger(0, labelName, OBJPROP_ANCHOR, ANCHOR_LEFT); + ObjectSetInteger(0, labelName, OBJPROP_SELECTABLE, false); + ObjectSetInteger(0, labelName, OBJPROP_HIDDEN, true); + } + } + // =========================================================== + // DRAW SWING POINTS + // =========================================================== + string highName = "ICT_FIB_SWING_HIGH"; + ObjectCreate(0, highName, OBJ_ARROW, 0, g_fibData.swingHighTime, g_fibData.swingHigh); + ObjectSetInteger(0, highName, OBJPROP_ARROWCODE, 217); + ObjectSetInteger(0, highName, OBJPROP_COLOR, clrRed); + ObjectSetInteger(0, highName, OBJPROP_WIDTH, 2); + ObjectSetInteger(0, highName, OBJPROP_SELECTABLE, false); + string lowName = "ICT_FIB_SWING_LOW"; + ObjectCreate(0, lowName, OBJ_ARROW, 0, g_fibData.swingLowTime, g_fibData.swingLow); + ObjectSetInteger(0, lowName, OBJPROP_ARROWCODE, 218); + ObjectSetInteger(0, lowName, OBJPROP_COLOR, clrLime); + ObjectSetInteger(0, lowName, OBJPROP_WIDTH, 2); + ObjectSetInteger(0, lowName, OBJPROP_SELECTABLE, false); +} +//+------------------------------------------------------------------+ +//| Delete Fibonacci Objects | +//+------------------------------------------------------------------+ +void DeleteFibonacciObjects() +{ + for(int i = ObjectsTotal(0, 0, -1) - 1; i >= 0; i--) + { + string name = ObjectName(0, i); + if(StringFind(name, "ICT_FIB_") == 0) { + ObjectDelete(0, name); + } + } +} +//+------------------------------------------------------------------+ +//| Get Nearest Fibonacci Level | +//+------------------------------------------------------------------+ +double GetNearestFibLevel(double price, double &levelValue) +{ + if(!g_fibData.isValid) { + levelValue = 0.5; + return price; + } + double nearestPrice = g_fibData.levels[0].price; + levelValue = g_fibData.levels[0].level; + double minDistance = MathAbs(price - nearestPrice); + for(int i = 1; i < g_fibLevelCount; i++) + { + double distance = MathAbs(price - g_fibData.levels[i].price); + if(distance < minDistance) { + minDistance = distance; + nearestPrice = g_fibData.levels[i].price; + levelValue = g_fibData.levels[i].level; + } + } + return nearestPrice; +} +//+------------------------------------------------------------------+ +//| Check if Price is in OTE Zone | +//+------------------------------------------------------------------+ +bool IsPriceInFibOTE(double price) +{ + if(!g_fibData.isValid) return false; + double ote618 = 0, ote786 = 0; + for(int i = 0; i < g_fibLevelCount; i++) + { + if(MathAbs(g_fibData.levels[i].level - 0.618) < 0.001) ote618 = g_fibData.levels[i].price; + if(MathAbs(g_fibData.levels[i].level - 0.786) < 0.001) ote786 = g_fibData.levels[i].price; + } + if(ote618 == 0 || ote786 == 0) return false; + double high = MathMax(ote618, ote786); + double low = MathMin(ote618, ote786); + return (price >= low && price <= high); +} +//+------------------------------------------------------------------+ +//| =============================================================== | +//| [MONEY] COST ANALYSIS SYSTEM | +//| =============================================================== | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| Initialize Cost Analysis | +//+------------------------------------------------------------------+ +void InitializeCostAnalysis() +{ + // Initialize broker config + g_brokerConfig.brokerName = AccountInfoString(ACCOUNT_COMPANY); + g_brokerConfig.commissionPerLot = COST_CommissionPerLot; + g_brokerConfig.commissionPerSide = COST_CommissionRoundTrip ? 2.0 : 1.0; + g_brokerConfig.isCommissionInCurrency = true; + g_brokerConfig.avgSlippagePoints = COST_ExpectedSlippage; + g_brokerConfig.maxAcceptableSpread = COST_MaxSpreadPoints; + g_brokerConfig.maxAcceptableCost = COST_MaxCostPercent; + g_brokerConfig.swapLong = SymbolInfoDouble(_Symbol, SYMBOL_SWAP_LONG); + g_brokerConfig.swapShort = SymbolInfoDouble(_Symbol, SYMBOL_SWAP_SHORT); + g_brokerConfig.isECN = (g_brokerConfig.commissionPerLot > 0); + // Initialize spread analysis + g_spreadAnalysis.currentSpread = 0; + g_spreadAnalysis.avgSpread = 0; + g_spreadAnalysis.minSpread = DBL_MAX; + g_spreadAnalysis.maxSpread = 0; + g_spreadAnalysis.spreadVolatility = 0; + g_spreadAnalysis.isSpreadNormal = true; + g_spreadAnalysis.lastUpdate = 0; + // Initialize spread history + ArrayResize(g_spreadHistory, SPREAD_SampleSize); + ArrayInitialize(g_spreadHistory, 0); + g_spreadHistoryIndex = 0; + // Initialize last trade costs + ZeroMemory(g_lastTradeCosts); + g_costAnalysisEnabled = true; + if(g_verboseLog) { + Print("[MONEY] Cost Analysis initialized"); + Print(" Broker: ", g_brokerConfig.brokerName); + Print(" Commission: $", g_brokerConfig.commissionPerLot, " per lot"); + Print(" ECN: ", g_brokerConfig.isECN ? "Yes" : "No"); + } +} +//+------------------------------------------------------------------+ +//| Update Spread Analysis | +//+------------------------------------------------------------------+ +void UpdateSpreadAnalysis() +{ + if(!SPREAD_Monitor) return; + datetime currentTime = TimeCurrent(); + if(currentTime - g_lastSpreadUpdate < 1) return; // Update max once per second + double currentSpread = (double)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * g_point; + double spreadPoints = (double)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD); + // Store in history + g_spreadHistory[g_spreadHistoryIndex] = spreadPoints; + g_spreadHistoryIndex = (g_spreadHistoryIndex + 1) % SPREAD_SampleSize; + // Calculate statistics + double sum = 0; + double sumSq = 0; + int count = 0; + g_spreadAnalysis.minSpread = DBL_MAX; + g_spreadAnalysis.maxSpread = 0; + for(int i = 0; i < SPREAD_SampleSize; i++) + { + if(g_spreadHistory[i] > 0) { + sum += g_spreadHistory[i]; + sumSq += g_spreadHistory[i] * g_spreadHistory[i]; + count++; + if(g_spreadHistory[i] < g_spreadAnalysis.minSpread) + g_spreadAnalysis.minSpread = g_spreadHistory[i]; + if(g_spreadHistory[i] > g_spreadAnalysis.maxSpread) + g_spreadAnalysis.maxSpread = g_spreadHistory[i]; + } + } + if(count > 0) { + g_spreadAnalysis.avgSpread = sum / count; + double variance = (sumSq / count) - (g_spreadAnalysis.avgSpread * g_spreadAnalysis.avgSpread); + g_spreadAnalysis.spreadVolatility = MathSqrt(MathMax(0, variance)); + } + g_spreadAnalysis.currentSpread = spreadPoints; + g_spreadAnalysis.isSpreadNormal = (spreadPoints <= g_spreadAnalysis.avgSpread * SPREAD_AlertMultiplier); + g_spreadAnalysis.lastUpdate = currentTime; + // Alert on high spread + if(!g_spreadAnalysis.isSpreadNormal && EnableAlerts) { + static datetime lastAlert = 0; + if(currentTime - lastAlert > 300) { // Alert max every 5 min + Alert("[WARN] High Spread on ", _Symbol, ": ", spreadPoints, " points (Avg: ", + DoubleToString(g_spreadAnalysis.avgSpread, 1), ")"); + lastAlert = currentTime; + } + } +} +//+------------------------------------------------------------------+ +//| Calculate Full Trading Costs | +//+------------------------------------------------------------------+ +TradingCosts CalculateFullTradingCosts(double lotSize, double entryPrice, + double stopLoss, bool isBullish) +{ + TradingCosts costs; + ZeroMemory(costs); + if(!g_costAnalysisEnabled || lotSize <= 0) return costs; + // Get symbol info + double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); + double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); + double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); + double spread = (double)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * point; + double spreadPoints = (double)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD); + if(tickSize <= 0 || tickValue <= 0 || point <= 0) return costs; + // Calculate point value + double pointValue = tickValue * (point / tickSize) * lotSize; + costs.costPerPoint = pointValue; + // =========================================================== + // 1. SPREAD COST + // =========================================================== + costs.spreadCost = spreadPoints * pointValue; + // =========================================================== + // 2. COMMISSION COST + // =========================================================== + if(COST_Mode == COST_MODE_PER_LOT) + { + costs.commissionCost = g_brokerConfig.commissionPerLot * + g_brokerConfig.commissionPerSide * lotSize; + } + else if(COST_Mode == COST_MODE_FIXED) + { + costs.commissionCost = g_brokerConfig.commissionPerLot * g_brokerConfig.commissionPerSide; + } + else if(COST_Mode == COST_MODE_PERCENTAGE) + { + double tradeValue = entryPrice * lotSize * SymbolInfoDouble(_Symbol, SYMBOL_TRADE_CONTRACT_SIZE); + costs.commissionCost = tradeValue * (g_brokerConfig.commissionPerLot / 100.0) * g_brokerConfig.commissionPerSide; + } + // =========================================================== + // 3. SLIPPAGE COST + // =========================================================== + double slippagePoints = 0; + if(COST_SlippageMode == SLIP_MODE_FIXED) + { + slippagePoints = COST_ExpectedSlippage; + } + else if(COST_SlippageMode == SLIP_MODE_PERCENTAGE) + { + slippagePoints = g_cachedATR / point * (COST_ExpectedSlippage / 100.0); + } + else if(COST_SlippageMode == SLIP_MODE_DYNAMIC) + { + double volRatio = (g_cachedATR > 0) ? g_spreadAnalysis.spreadVolatility / g_cachedATR : 1.0; + slippagePoints = COST_ExpectedSlippage * (1.0 + volRatio); + } + costs.slippageCost = slippagePoints * pointValue * 2.0; // Entry + Exit + // =========================================================== + // 4. SWAP COST (if enabled) + // =========================================================== + if(COST_IncludeSwap) { + double swapRate = isBullish ? g_brokerConfig.swapLong : g_brokerConfig.swapShort; + costs.swapCostDaily = MathAbs(swapRate) * lotSize; + } + // =========================================================== + // 5. TOTAL COST CALCULATION + // =========================================================== + costs.totalCost = costs.spreadCost + costs.commissionCost + costs.slippageCost; + // Break-even points + if(pointValue > 0) { + costs.breakEvenPoints = costs.totalCost / pointValue; + } + // Cost as % of SL + double slDistance = MathAbs(entryPrice - stopLoss); + double slInPoints = slDistance / point; + double riskAmount = slInPoints * pointValue; + if(riskAmount > 0) { + costs.costAsPercentOfSL = (costs.totalCost / riskAmount) * 100.0; + } + // Check if acceptable + costs.isCostAcceptable = true; + costs.rejectReason = ""; + // * FIX#459: Spread execution gate now uses g_workingMaxSpreadPips (per pair+TF from spread[tf]) + // instead of COST_MaxSpreadPoints (global 500pt = ~50 pips, irrelevant for EURUSD). + // Flow: spread[tf] pair table → g_autoOptParams.max_spread_pips → g_workingMaxSpreadPips. + // Examples: EURUSD H4=20p, XAUUSD H4=80p, GBPJPY H4=40p. + // COST_MaxSpreadPoints kept as absolute fallback when g_workingMaxSpreadPips is not yet set (0). + // Convert pips → points: 1 pip = pipValue/point steps + // e.g. EURUSD 5-digit: 1 pip = 0.0001/0.00001 = 10 points + // Old formula (/ g_pipValue * g_point) was inverted: + // 10 / 0.0001 × 0.00001 = 1.0 pt instead of 100 pts → rejected all valid signals. + double _effectiveSpreadLimit = (g_workingMaxSpreadPips > 0) + ? g_workingMaxSpreadPips * (g_pipValue / g_point) // pips → points (correct) + : COST_MaxSpreadPoints; + if(spreadPoints > _effectiveSpreadLimit) { + costs.isCostAcceptable = false; + costs.rejectReason = StringFormat("Spread too high: %.1f > %.1f pts (%s %s)", + spreadPoints, _effectiveSpreadLimit, + _Symbol, EnumToString(_Period)); + } + else if(costs.costAsPercentOfSL > g_workingMaxCostPct) { + costs.isCostAcceptable = false; + costs.rejectReason = StringFormat("Cost too high: %.1f%% > %.1f%%", + costs.costAsPercentOfSL, g_workingMaxCostPct); + } + else if(SPREAD_BlockHighSpread && !g_spreadAnalysis.isSpreadNormal) { + costs.isCostAcceptable = false; + costs.rejectReason = "Spread abnormally high"; + } + // Store for reference + g_lastTradeCosts = costs; + return costs; +} +//+------------------------------------------------------------------+ +//| Validate and Adjust Trade for Costs | +//+------------------------------------------------------------------+ +bool ValidateTradeForCosts(double &entryPrice, double &stopLoss, double &takeProfit, + double lotSize, bool isBullish, TradingCosts &costs) +{ + if(!g_costAnalysisEnabled) return true; + costs = CalculateFullTradingCosts(lotSize, entryPrice, stopLoss, isBullish); + if(!costs.isCostAcceptable) { + if(g_verboseLog) { + Print("[X] Trade rejected: ", costs.rejectReason); + } + return !COST_RejectHighCost; + } + // Adjust TP to account for costs + if(COST_AdjustTP && takeProfit > 0) + { + double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); + double tpAdjustment = costs.breakEvenPoints * point; + if(isBullish) { + takeProfit += tpAdjustment; + } else { + takeProfit -= tpAdjustment; + } + // Recalculate effective R:R + double slDistance = MathAbs(entryPrice - stopLoss); + double tpDistance = MathAbs(takeProfit - entryPrice); + if(slDistance > 0) { + costs.effectiveRR = tpDistance / slDistance; + } + if(g_verboseLog) { + Print("[CHART] TP adjusted for costs. New TP: ", takeProfit, + " | Effective R:R: ", DoubleToString(costs.effectiveRR, 2)); + } + } + return true; +} +//+------------------------------------------------------------------+ +//| Get Cost Analysis Summary String | +//+------------------------------------------------------------------+ +string GetCostAnalysisSummary() +{ + if(!g_costAnalysisEnabled) return "Cost analysis disabled"; + return StringFormat( + "[MONEY] Costs: Spread=$%.2f | Comm=$%.2f | Slip=$%.2f | Total=$%.2f (%.1f%% of SL) | BE=%.1f pts", + g_lastTradeCosts.spreadCost, + g_lastTradeCosts.commissionCost, + g_lastTradeCosts.slippageCost, + g_lastTradeCosts.totalCost, + g_lastTradeCosts.costAsPercentOfSL, + g_lastTradeCosts.breakEvenPoints + ); +} +//+------------------------------------------------------------------+ +//| =============================================================== | +//| [TARGET] PAIR OPTIMIZATION SYSTEM | +//| =============================================================== | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| Initialize All Pair Profiles (COMPLETE) | +//+------------------------------------------------------------------+ +void InitializeAllPairProfiles() +{ + // Pre-allocate array for known pairs (45 = original 40 + 5 broker variations) + ArrayResize(g_pairProfiles, 45); + int idx = 0; + // =========================================================== + // MAJOR FOREX PAIRS + // =========================================================== + // EURUSD + g_pairProfiles[idx].symbol = "EURUSD"; + g_pairProfiles[idx].category = "Major"; + g_pairProfiles[idx].avgSpread = 1.2; + // * v10.00: Aligned to ApplyPairTFProfile H4 representative values (FIX#199/219/v9.59). + // Old fallback was M15-optimised (sl=1.1, tp=4.5, risk=1.0%) — caused H4 mismatch + // when ApplyPairTFProfile not called (first tick before pair detected). + // H4 values: sl=2.0 tp1=3.8 mrr=1.80 risk=2.0% (H4 is primary production TF). + g_pairProfiles[idx].optimalSL_Mult = 2.0; // H4 representative (was 1.1 M15-only) + g_pairProfiles[idx].optimalTP_Mult = 3.8; // H4 tp1 (was 4.5 — unreachable on H4) + g_pairProfiles[idx].optimalRR = 1.80; // H4 minRR (was 1.8 — unchanged but now intentional) + g_pairProfiles[idx].optimalTP = TP_BY_LIQUIDITY; + g_pairProfiles[idx].minConfluence = 0.60; + g_pairProfiles[idx].minEntryQuality = 44.0; // H1 calibrated: min_conf[2]=44 (OB/FVG score 44-49/85) + g_pairProfiles[idx].allowScalping = true; + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = true; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 5; + g_pairProfiles[idx].riskPercent = 2.0; // H4 risk 2.0% (was 1.0% — blocked dynamic boost) + g_pairProfiles[idx].chars.volatilityRank = 4; + g_pairProfiles[idx].chars.bestSession = "London/NY Overlap"; + idx++; + // GBPUSD + g_pairProfiles[idx].symbol = "GBPUSD"; + g_pairProfiles[idx].category = "Major"; + g_pairProfiles[idx].avgSpread = 1.8; + // * v10.00: Aligned to ApplyPairTFProfile H4 values (FIX#199: tp1 3.20→3.80, mrr 1.80→1.65). + // Old fallback: sl=1.8, tp=3.5, mrr=2.0, risk=0.75% — all wrong for H4. + // H4: sl=2.2, tp1=3.8, mrr=1.65 (R:R=3.8/2.2=1.73 > 1.65 ✓), risk=2.0%. + g_pairProfiles[idx].optimalSL_Mult = 2.2; // H4 sl (was 1.8) + g_pairProfiles[idx].optimalTP_Mult = 3.8; // H4 tp1 (was 3.5) + g_pairProfiles[idx].optimalRR = 1.65; // H4 mrr (was 2.0 — killed ALL GBPUSD H4 candidates) + g_pairProfiles[idx].optimalTP = TP_BY_SWING_STRUCTURE; + g_pairProfiles[idx].minConfluence = 0.60; + g_pairProfiles[idx].minEntryQuality = 65.0; + g_pairProfiles[idx].allowScalping = true; + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = false; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 4; + g_pairProfiles[idx].riskPercent = 2.0; // H4 risk (was 0.75%) + g_pairProfiles[idx].chars.volatilityRank = 7; + g_pairProfiles[idx].chars.bestSession = "London Open"; + idx++; + // USDJPY + g_pairProfiles[idx].symbol = "USDJPY"; + g_pairProfiles[idx].category = "Major"; + g_pairProfiles[idx].avgSpread = 1.5; + g_pairProfiles[idx].optimalSL_Mult = 1.5; + g_pairProfiles[idx].optimalTP_Mult = 3.0; + g_pairProfiles[idx].optimalRR = 1.80; // H4 mrr from ApplyPairTFProfile (was 2.5 — stale v7 value) + g_pairProfiles[idx].optimalTP = TP_BY_FIBONACCI; + g_pairProfiles[idx].minConfluence = 0.55; + g_pairProfiles[idx].minEntryQuality = 60.0; + g_pairProfiles[idx].allowScalping = true; + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = true; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 5; + g_pairProfiles[idx].riskPercent = 1.5; // H4 risk from ApplyPairTFProfile (was 1.0%) + g_pairProfiles[idx].chars.volatilityRank = 5; + g_pairProfiles[idx].chars.bestSession = "Asian/NY"; + idx++; + // USDCHF + g_pairProfiles[idx].symbol = "USDCHF"; + g_pairProfiles[idx].category = "Major"; + g_pairProfiles[idx].avgSpread = 1.8; + // * v10.00: Aligned to ApplyPairTFProfile H4 (FIX#199: sl=2.0, tp=3.60, mrr=1.80, risk=2.0%) + g_pairProfiles[idx].optimalSL_Mult = 2.0; // H4 sl (was 1.5) + g_pairProfiles[idx].optimalTP_Mult = 3.6; // H4 tp1 (was 2.5) + g_pairProfiles[idx].optimalRR = 1.80; // H4 mrr (was 2.0 — was blocking many candidates) + g_pairProfiles[idx].optimalTP = TP_BY_LIQUIDITY; + g_pairProfiles[idx].minConfluence = 0.55; + g_pairProfiles[idx].minEntryQuality = 60.0; + g_pairProfiles[idx].allowScalping = true; + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = true; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 4; + g_pairProfiles[idx].riskPercent = 2.0; // H4 risk (was 1.0%) + g_pairProfiles[idx].chars.volatilityRank = 4; + g_pairProfiles[idx].chars.bestSession = "London"; + idx++; + // AUDUSD + g_pairProfiles[idx].symbol = "AUDUSD"; + g_pairProfiles[idx].category = "Major"; + g_pairProfiles[idx].avgSpread = 1.4; + // * v10.00: Aligned to ApplyPairTFProfile H4 (sl=2.0, tp1=3.0, mrr=1.80, risk=1.5%) + g_pairProfiles[idx].optimalSL_Mult = 2.0; // H4 sl (was 1.5) + g_pairProfiles[idx].optimalTP_Mult = 3.0; // H4 tp1 (was 2.5) + g_pairProfiles[idx].optimalRR = 1.80; // H4 mrr (was 2.0) + g_pairProfiles[idx].optimalTP = TP_BY_LIQUIDITY; + g_pairProfiles[idx].minConfluence = 0.55; + g_pairProfiles[idx].minEntryQuality = 58.0; + g_pairProfiles[idx].allowScalping = true; + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = true; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 5; + g_pairProfiles[idx].riskPercent = 1.5; // H4 risk (was 1.0%) + g_pairProfiles[idx].chars.volatilityRank = 5; + g_pairProfiles[idx].chars.bestSession = "Asian/Sydney"; + idx++; + // USDCAD + g_pairProfiles[idx].symbol = "USDCAD"; + g_pairProfiles[idx].category = "Major"; + g_pairProfiles[idx].avgSpread = 1.8; + // * v10.00: Aligned to ApplyPairTFProfile H4 (sl=2.0, tp1=3.0, mrr=1.80, risk=1.5%) + g_pairProfiles[idx].optimalSL_Mult = 2.0; // H4 sl (was 1.5) + g_pairProfiles[idx].optimalTP_Mult = 3.0; // H4 tp1 (was 2.5) + g_pairProfiles[idx].optimalRR = 1.80; // H4 mrr (was 2.0) + g_pairProfiles[idx].optimalTP = TP_BY_LIQUIDITY; + g_pairProfiles[idx].minConfluence = 0.55; + g_pairProfiles[idx].minEntryQuality = 60.0; + g_pairProfiles[idx].allowScalping = true; + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = true; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 4; + g_pairProfiles[idx].riskPercent = 1.5; // H4 risk (was 1.0%) + g_pairProfiles[idx].chars.volatilityRank = 4; + g_pairProfiles[idx].chars.bestSession = "NY"; + idx++; + // NZDUSD + g_pairProfiles[idx].symbol = "NZDUSD"; + g_pairProfiles[idx].category = "Major"; + g_pairProfiles[idx].avgSpread = 1.8; + // * v10.00: Aligned to H4 representative (similar to AUDUSD — no specific NZDUSD block in ApplyPairTFProfile) + g_pairProfiles[idx].optimalSL_Mult = 2.0; // H4 sl (was 1.5) + g_pairProfiles[idx].optimalTP_Mult = 3.0; // H4 tp1 (was 2.5) + g_pairProfiles[idx].optimalRR = 1.80; // H4 mrr (was 2.0) + g_pairProfiles[idx].optimalTP = TP_BY_LIQUIDITY; + g_pairProfiles[idx].minConfluence = 0.55; + g_pairProfiles[idx].minEntryQuality = 58.0; + g_pairProfiles[idx].allowScalping = true; + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = true; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 4; + g_pairProfiles[idx].riskPercent = 1.5; // H4 risk (was 1.0%) + g_pairProfiles[idx].chars.volatilityRank = 5; + g_pairProfiles[idx].chars.bestSession = "Asian/Sydney"; + idx++; + // =========================================================== + // CROSS PAIRS + // =========================================================== + // EURGBP + g_pairProfiles[idx].symbol = "EURGBP"; + g_pairProfiles[idx].category = "Cross"; + g_pairProfiles[idx].avgSpread = 1.5; + g_pairProfiles[idx].optimalSL_Mult = 1.5; + g_pairProfiles[idx].optimalTP_Mult = 2.5; + g_pairProfiles[idx].optimalRR = 1.5; + g_pairProfiles[idx].optimalTP = TP_BY_ATR; + g_pairProfiles[idx].minConfluence = 0.50; + g_pairProfiles[idx].minEntryQuality = 55.0; + g_pairProfiles[idx].allowScalping = true; + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = false; + g_pairProfiles[idx].useBOSRetest = true; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 4; + g_pairProfiles[idx].riskPercent = 1.0; // * v7.5c FIX: was 1.5 -- too high for cross pair + // [v6.42] PAIR_ManualCategory override + if(PAIR_ManualCategory != "AUTO" && StringLen(PAIR_ManualCategory) > 0) + { + // Manual category overrides auto-detection + } + g_pairProfiles[idx].chars.volatilityRank = 3; + g_pairProfiles[idx].chars.bestSession = "London"; + idx++; + // EURJPY + g_pairProfiles[idx].symbol = "EURJPY"; + g_pairProfiles[idx].category = "Cross"; + g_pairProfiles[idx].avgSpread = 2.0; + // * v10.00: Aligned to ApplyPairTFProfile H4 (FIX#151: sl=2.3, tp1=3.5, mrr=1.80, risk=1.3%) + g_pairProfiles[idx].optimalSL_Mult = 2.3; // H4 sl (was 2.0) + g_pairProfiles[idx].optimalTP_Mult = 3.5; // H4 tp1 (unchanged) + g_pairProfiles[idx].optimalRR = 1.80; // H4 mrr (was 2.0 — stale v7) + g_pairProfiles[idx].optimalTP = TP_BY_SWING_STRUCTURE; + g_pairProfiles[idx].minConfluence = 0.60; + g_pairProfiles[idx].minEntryQuality = 65.0; + g_pairProfiles[idx].allowScalping = true; + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = false; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 3; + g_pairProfiles[idx].riskPercent = 1.3; // H4 risk (was 0.75%) + g_pairProfiles[idx].chars.volatilityRank = 7; + g_pairProfiles[idx].chars.bestSession = "Asian/London"; + idx++; + // GBPJPY + g_pairProfiles[idx].symbol = "GBPJPY"; + g_pairProfiles[idx].category = "Cross"; + g_pairProfiles[idx].avgSpread = 3.0; + // * v10.00: Aligned to ApplyPairTFProfile H4 (FIX#151: sl=2.5, tp1=3.8, mrr=1.80, risk=1.2%) + g_pairProfiles[idx].optimalSL_Mult = 2.5; // H4 sl (was 2.0) + g_pairProfiles[idx].optimalTP_Mult = 3.8; // H4 tp1 (was 3.0) + g_pairProfiles[idx].optimalRR = 1.80; // H4 mrr (was 1.5) + g_pairProfiles[idx].optimalTP = TP_BY_SWING_STRUCTURE; + g_pairProfiles[idx].minConfluence = 0.70; + g_pairProfiles[idx].minEntryQuality = 70.0; + g_pairProfiles[idx].allowScalping = true; + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = false; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 2; + g_pairProfiles[idx].riskPercent = 1.2; // H4 risk (was 0.5% — too conservative for GBPJPY vol) + g_pairProfiles[idx].chars.volatilityRank = 9; + g_pairProfiles[idx].chars.bestSession = "London Open"; + idx++; + // =========================================================== + // METALS + // =========================================================== + // XAUUSD (Gold) + g_pairProfiles[idx].symbol = "XAUUSD"; + g_pairProfiles[idx].category = "Metal"; + g_pairProfiles[idx].avgSpread = 30.0; + // * v10.00: Aligned to ApplyPairTFProfile H4 values (FIX#218: mrr 1.80→1.30 MAIN fix). + // OLD: sl=1.4, tp=2.0, mrr=1.3 (mrr happened to match but sl/tp were M5 values). + // H4: sl=2.5, tp1=3.5, mrr=1.30, risk=1.0% — mrr=1.30 critical (old 1.80 → 0 H4 trades). + g_pairProfiles[idx].optimalSL_Mult = 2.5; // H4 sl (was 1.4 — M5 value, too tight for H4) + g_pairProfiles[idx].optimalTP_Mult = 3.5; // H4 tp1 (was 2.0 — M5 value, too small for H4) + g_pairProfiles[idx].optimalRR = 1.30; // H4 mrr — CRITICAL: 1.80 was killing all H4 candidates + g_pairProfiles[idx].optimalTP = TP_BY_SWING_STRUCTURE; + g_pairProfiles[idx].minConfluence = 0.60; + g_pairProfiles[idx].minEntryQuality = 60.0; + g_pairProfiles[idx].allowScalping = true; + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = false; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 5; + g_pairProfiles[idx].riskPercent = 1.0; // H4 risk (unchanged — already correct) + g_pairProfiles[idx].chars.volatilityRank = 8; + g_pairProfiles[idx].chars.bestSession = "London+NY"; + idx++; + // XAGUSD (Silver) + g_pairProfiles[idx].symbol = "XAGUSD"; + g_pairProfiles[idx].category = "Metal"; + g_pairProfiles[idx].avgSpread = 25.0; + // * v10.00: Aligned to ApplyPairTFProfile H4 values (FIX#218/219). + // OLD: sl=1.5, tp=2.2, mrr=1.3, risk=0.5% — M5 values, wrong for H4. + // H4: sl=2.5, tp1=3.8, mrr=1.40, risk=1.0% — mrr=1.40 (old 1.30 too lenient for Silver spread). + g_pairProfiles[idx].optimalSL_Mult = 2.5; // H4 sl (was 1.5) + g_pairProfiles[idx].optimalTP_Mult = 3.8; // H4 tp1 (was 2.2) + g_pairProfiles[idx].optimalRR = 1.40; // H4 mrr (was 1.3 — Silver needs slightly higher floor due to spread) + g_pairProfiles[idx].optimalTP = TP_BY_SWING_STRUCTURE; + g_pairProfiles[idx].minConfluence = 0.60; + g_pairProfiles[idx].minEntryQuality = 60.0; + g_pairProfiles[idx].allowScalping = true; + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = false; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 4; + g_pairProfiles[idx].riskPercent = 1.0; // H4 risk (was 0.5% — too conservative) + g_pairProfiles[idx].chars.volatilityRank = 9; + g_pairProfiles[idx].chars.bestSession = "NY Open"; + idx++; + // =========================================================== + // INDICES + // =========================================================== + // US500 (S&P 500) + g_pairProfiles[idx].symbol = "US500"; + g_pairProfiles[idx].category = "Index"; + g_pairProfiles[idx].avgSpread = 50.0; // * v7.5c FIX: was 0.5 ($0.50 price) -- must be POINTS + g_pairProfiles[idx].optimalSL_Mult = 2.5; // * v7.5c: wider SL for indices + g_pairProfiles[idx].optimalTP_Mult = 5.0; // * v7.5c: wider TP for indices + g_pairProfiles[idx].optimalRR = 1.5; + g_pairProfiles[idx].optimalTP = TP_BY_SWING_STRUCTURE; + g_pairProfiles[idx].minConfluence = 0.65; + g_pairProfiles[idx].minEntryQuality = 65.0; // * v7.7: 55->65 (align with Nasdaq quality standard) + g_pairProfiles[idx].allowScalping = true; // * v7.5 FIX: Auto-Opt decides scalping viability (was hardcoded false) + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = true; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 8; // * v7.5c: 4->8 for indices + g_pairProfiles[idx].riskPercent = 0.5; // * v7.5c FIX: was 1.0 -> too high for indices (margin) + g_pairProfiles[idx].chars.volatilityRank = 6; + g_pairProfiles[idx].chars.bestSession = "NY Open/Power Hour"; + idx++; + // US100 (Nasdaq) + g_pairProfiles[idx].symbol = "US100"; + g_pairProfiles[idx].category = "Index"; + g_pairProfiles[idx].avgSpread = 150.0; + g_pairProfiles[idx].optimalSL_Mult = 1.2; // * v7.6: 2.5->1.2 (tight SL = better R:R, ATR on M5 already accounts for noise) + g_pairProfiles[idx].optimalTP_Mult = 2.5; // * v7.6: 5.0->2.5 (TP1 at 2.1R achievable, not 4R that never hits) + g_pairProfiles[idx].optimalRR = 2.0; + g_pairProfiles[idx].optimalTP = TP_BY_SWING_STRUCTURE; + g_pairProfiles[idx].minConfluence = 0.75; // * v7.6: 0.70->0.75 (higher bar = fewer but better trades) + g_pairProfiles[idx].minEntryQuality = 70.0; // * v7.6: 55->70 CRITICAL FIX (was allowing score 47-64 trades -> negative EV) + g_pairProfiles[idx].allowScalping = true; // * v7.7 FIX: false->true (minEntryQuality=70 is the quality gate, not scalping flag) + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = false; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 2000; // * v7.6b: No limit on pair profile -- EA_MaxDailyTrades input controls this globally + g_pairProfiles[idx].riskPercent = 0.5; + g_pairProfiles[idx].chars.volatilityRank = 8; + g_pairProfiles[idx].chars.bestSession = "NY Open"; + idx++; + // =========================================================== + // INDICES - BROKER VARIATIONS (FTMO, Vantage, Exness) + // =========================================================== + // NAS100 (NASDAQ - Vantage, Exness, many brokers) + g_pairProfiles[idx].symbol = "NAS100"; + g_pairProfiles[idx].category = "Index"; + g_pairProfiles[idx].avgSpread = 150.0; + g_pairProfiles[idx].optimalSL_Mult = 1.2; // * v7.6: 2.5->1.2 + g_pairProfiles[idx].optimalTP_Mult = 2.5; // * v7.6: 5.0->2.5 + g_pairProfiles[idx].optimalRR = 2.0; + g_pairProfiles[idx].optimalTP = TP_BY_SWING_STRUCTURE; + g_pairProfiles[idx].minConfluence = 0.75; // * v7.6: 0.70->0.75 + g_pairProfiles[idx].minEntryQuality = 70.0; // * v7.6: 55->70 CRITICAL FIX + g_pairProfiles[idx].allowScalping = true; // * v7.7 FIX: Enabled (minEntryQuality=70 is the quality gate) + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = false; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 2000; // * v7.6b: No limit on pair profile -- EA_MaxDailyTrades input controls this globally + g_pairProfiles[idx].riskPercent = 0.5; + g_pairProfiles[idx].chars.volatilityRank = 7; + g_pairProfiles[idx].chars.bestSession = "NY Open/Power Hour"; + idx++; + // US100.cash (NASDAQ - FTMO) + g_pairProfiles[idx].symbol = "US100.cash"; + g_pairProfiles[idx].category = "Index"; + g_pairProfiles[idx].avgSpread = 150.0; + g_pairProfiles[idx].optimalSL_Mult = 1.2; // * v7.6: 2.5->1.2 (tight SL = better R:R, matches EA_StopLossATR=1.2 input) + g_pairProfiles[idx].optimalTP_Mult = 2.5; // * v7.6: 5.0->2.5 (TP1=2.1R, TP2=2.9R -- actually achievable on M5) + g_pairProfiles[idx].optimalRR = 2.0; + g_pairProfiles[idx].optimalTP = TP_BY_SWING_STRUCTURE; + g_pairProfiles[idx].minConfluence = 0.75; // * v7.6: 0.70->0.75 (higher quality bar) + g_pairProfiles[idx].minEntryQuality = 70.0; // * v7.6: 55->70 CRITICAL FIX (log showed 92/181 trades were score<65 -> all negative EV) + g_pairProfiles[idx].allowScalping = true; // * v7.7 FIX: Enabled (quality score=70 gates entries) + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = false; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 2000; // * v7.6b: No limit on pair profile -- EA_MaxDailyTrades input controls this globally + g_pairProfiles[idx].riskPercent = 0.5; + g_pairProfiles[idx].chars.volatilityRank = 7; + g_pairProfiles[idx].chars.bestSession = "NY Open/Power Hour"; + idx++; + // SP500 (S&P 500 - Vantage, some brokers) + g_pairProfiles[idx].symbol = "SP500"; + g_pairProfiles[idx].category = "Index"; + g_pairProfiles[idx].avgSpread = 50.0; // * v7.5c FIX: was 0.5 -- must be POINTS + g_pairProfiles[idx].optimalSL_Mult = 2.5; // * v7.5c: wider SL for indices + g_pairProfiles[idx].optimalTP_Mult = 5.0; // * v7.5c: wider TP for indices + g_pairProfiles[idx].optimalRR = 1.5; + g_pairProfiles[idx].optimalTP = TP_BY_SWING_STRUCTURE; + g_pairProfiles[idx].minConfluence = 0.65; + g_pairProfiles[idx].minEntryQuality = 55.0; // * v7.5c: 65->55 for indices + g_pairProfiles[idx].allowScalping = true; // * v7.5 FIX: Auto-Opt decides scalping viability (was hardcoded false) + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = true; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 8; // * v7.5c: 4->8 for indices + g_pairProfiles[idx].riskPercent = 0.5; // * v7.5c FIX: was 1.0 -> too high for indices (margin) + g_pairProfiles[idx].chars.volatilityRank = 6; + g_pairProfiles[idx].chars.bestSession = "NY Open/Power Hour"; + idx++; + // US500.cash (S&P 500 - FTMO) + g_pairProfiles[idx].symbol = "US500.cash"; + g_pairProfiles[idx].category = "Index"; + g_pairProfiles[idx].avgSpread = 50.0; // * v7.5c FIX: was 0.5 -- must be POINTS + g_pairProfiles[idx].optimalSL_Mult = 2.5; // * v7.5c: wider SL for indices + g_pairProfiles[idx].optimalTP_Mult = 5.0; // * v7.5c: wider TP for indices + g_pairProfiles[idx].optimalRR = 1.5; + g_pairProfiles[idx].optimalTP = TP_BY_SWING_STRUCTURE; + g_pairProfiles[idx].minConfluence = 0.65; + g_pairProfiles[idx].minEntryQuality = 55.0; // * v7.5c: 65->55 for indices + g_pairProfiles[idx].allowScalping = true; // * v7.5 FIX: Auto-Opt decides scalping viability (was hardcoded false) + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = true; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 8; // * v7.5c: 4->8 for indices + g_pairProfiles[idx].riskPercent = 0.5; // * v7.5c FIX: was 1.0 -> too high for indices (margin) + g_pairProfiles[idx].chars.volatilityRank = 6; + g_pairProfiles[idx].chars.bestSession = "NY Open/Power Hour"; + idx++; + // USTEC (NASDAQ - Exness) + g_pairProfiles[idx].symbol = "USTEC"; + g_pairProfiles[idx].category = "Index"; + g_pairProfiles[idx].avgSpread = 150.0; + g_pairProfiles[idx].optimalSL_Mult = 1.2; // * v7.6: 2.5->1.2 + g_pairProfiles[idx].optimalTP_Mult = 2.5; // * v7.6: 5.0->2.5 + g_pairProfiles[idx].optimalRR = 2.0; + g_pairProfiles[idx].optimalTP = TP_BY_SWING_STRUCTURE; + g_pairProfiles[idx].minConfluence = 0.75; // * v7.6: 0.70->0.75 + g_pairProfiles[idx].minEntryQuality = 70.0; // * v7.6: 55->70 CRITICAL FIX + g_pairProfiles[idx].allowScalping = true; // * v7.7 FIX: Enabled (quality gate handles frequency) + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = false; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 2000; // * v7.6b: No limit on pair profile -- EA_MaxDailyTrades input controls this globally + g_pairProfiles[idx].riskPercent = 0.5; + g_pairProfiles[idx].chars.volatilityRank = 7; + g_pairProfiles[idx].chars.bestSession = "NY Open/Power Hour"; + idx++; + // US30 (Dow Jones) + g_pairProfiles[idx].symbol = "US30"; + g_pairProfiles[idx].category = "Index"; + g_pairProfiles[idx].avgSpread = 200.0; // * v7.5c FIX: was 2.0 -- must be POINTS ($2.00 = 200pts) + g_pairProfiles[idx].optimalSL_Mult = 2.5; // * v7.5c: wider SL for indices + g_pairProfiles[idx].optimalTP_Mult = 5.0; // * v7.5c: wider TP for indices + g_pairProfiles[idx].optimalRR = 2.0; + g_pairProfiles[idx].optimalTP = TP_BY_SWING_STRUCTURE; + g_pairProfiles[idx].minConfluence = 0.65; + g_pairProfiles[idx].minEntryQuality = 55.0; // * v7.5c: 65->55 for indices + g_pairProfiles[idx].allowScalping = true; // * v7.5 FIX: Auto-Opt decides scalping viability (was hardcoded false) + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = true; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 8; // * v7.5c: 4->8 for indices + g_pairProfiles[idx].riskPercent = 0.5; // * v7.5c FIX: was 1.0 -> too high for indices (margin) + g_pairProfiles[idx].chars.volatilityRank = 6; + g_pairProfiles[idx].chars.bestSession = "NY Open/Power Hour"; + idx++; + // =========================================================== + // * v6.3: ADDITIONAL PAIR PROFILES (were MISSING -> fell to Unknown) + // =========================================================== + // EURCHF + g_pairProfiles[idx].symbol = "EURCHF"; + g_pairProfiles[idx].category = "Cross"; + g_pairProfiles[idx].avgSpread = 2.0; + g_pairProfiles[idx].optimalSL_Mult = 1.5; + g_pairProfiles[idx].optimalTP_Mult = 2.5; + g_pairProfiles[idx].optimalRR = 1.8; + g_pairProfiles[idx].optimalTP = TP_BY_ATR; + g_pairProfiles[idx].minConfluence = 0.55; + g_pairProfiles[idx].minEntryQuality = 60.0; + g_pairProfiles[idx].allowScalping = true; + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = false; + g_pairProfiles[idx].useBOSRetest = true; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 4; + g_pairProfiles[idx].riskPercent = 1.0; + g_pairProfiles[idx].chars.volatilityRank = 3; + g_pairProfiles[idx].chars.bestSession = "London"; + idx++; + // AUDNZD + g_pairProfiles[idx].symbol = "AUDNZD"; + g_pairProfiles[idx].category = "Cross"; + g_pairProfiles[idx].avgSpread = 2.5; + g_pairProfiles[idx].optimalSL_Mult = 1.5; + g_pairProfiles[idx].optimalTP_Mult = 2.5; + g_pairProfiles[idx].optimalRR = 1.8; + g_pairProfiles[idx].optimalTP = TP_BY_ATR; + g_pairProfiles[idx].minConfluence = 0.50; + g_pairProfiles[idx].minEntryQuality = 55.0; + g_pairProfiles[idx].allowScalping = true; + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = false; + g_pairProfiles[idx].useBOSRetest = true; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 4; + g_pairProfiles[idx].riskPercent = 1.0; + g_pairProfiles[idx].chars.volatilityRank = 3; + g_pairProfiles[idx].chars.bestSession = "Asian/Sydney"; + idx++; + // AUDCAD + g_pairProfiles[idx].symbol = "AUDCAD"; + g_pairProfiles[idx].category = "Cross"; + g_pairProfiles[idx].avgSpread = 2.2; + g_pairProfiles[idx].optimalSL_Mult = 1.5; + g_pairProfiles[idx].optimalTP_Mult = 2.5; + g_pairProfiles[idx].optimalRR = 1.8; + g_pairProfiles[idx].optimalTP = TP_BY_ATR; + g_pairProfiles[idx].minConfluence = 0.55; + g_pairProfiles[idx].minEntryQuality = 58.0; + g_pairProfiles[idx].allowScalping = true; + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = false; + g_pairProfiles[idx].useBOSRetest = true; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 4; + g_pairProfiles[idx].riskPercent = 1.0; + g_pairProfiles[idx].chars.volatilityRank = 4; + g_pairProfiles[idx].chars.bestSession = "Asian/NY"; + idx++; + // GBPAUD + g_pairProfiles[idx].symbol = "GBPAUD"; + g_pairProfiles[idx].category = "VolatileCross"; + g_pairProfiles[idx].avgSpread = 3.5; + g_pairProfiles[idx].optimalSL_Mult = 2.0; + g_pairProfiles[idx].optimalTP_Mult = 3.0; + g_pairProfiles[idx].optimalRR = 1.5; + g_pairProfiles[idx].optimalTP = TP_BY_SWING_STRUCTURE; + g_pairProfiles[idx].minConfluence = 0.65; + g_pairProfiles[idx].minEntryQuality = 68.0; + g_pairProfiles[idx].allowScalping = true; // * v7.5 FIX: Auto-Opt decides scalping viability (was hardcoded false) + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = false; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 3; + g_pairProfiles[idx].riskPercent = 0.6; + g_pairProfiles[idx].chars.volatilityRank = 8; + g_pairProfiles[idx].chars.bestSession = "London/Asian Overlap"; + idx++; + // GBPCAD + g_pairProfiles[idx].symbol = "GBPCAD"; + g_pairProfiles[idx].category = "VolatileCross"; + g_pairProfiles[idx].avgSpread = 3.0; + g_pairProfiles[idx].optimalSL_Mult = 2.0; + g_pairProfiles[idx].optimalTP_Mult = 3.0; + g_pairProfiles[idx].optimalRR = 1.5; + g_pairProfiles[idx].optimalTP = TP_BY_SWING_STRUCTURE; + g_pairProfiles[idx].minConfluence = 0.60; + g_pairProfiles[idx].minEntryQuality = 65.0; + g_pairProfiles[idx].allowScalping = true; // * v7.5 FIX: Auto-Opt decides scalping viability (was hardcoded false) + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = false; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 3; + g_pairProfiles[idx].riskPercent = 0.6; + g_pairProfiles[idx].chars.volatilityRank = 7; + g_pairProfiles[idx].chars.bestSession = "London/NY"; + idx++; + // CADJPY + g_pairProfiles[idx].symbol = "CADJPY"; + g_pairProfiles[idx].category = "VolatileCross"; + g_pairProfiles[idx].avgSpread = 2.5; + g_pairProfiles[idx].optimalSL_Mult = 1.8; + g_pairProfiles[idx].optimalTP_Mult = 2.8; + g_pairProfiles[idx].optimalRR = 1.5; + g_pairProfiles[idx].optimalTP = TP_BY_SWING_STRUCTURE; + g_pairProfiles[idx].minConfluence = 0.60; + g_pairProfiles[idx].minEntryQuality = 65.0; + g_pairProfiles[idx].allowScalping = true; // * v7.5 FIX: Auto-Opt decides scalping viability (was hardcoded false) + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = false; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 3; + g_pairProfiles[idx].riskPercent = 0.6; + g_pairProfiles[idx].chars.volatilityRank = 7; + g_pairProfiles[idx].chars.bestSession = "Asian/NY"; + idx++; + // CHFJPY + g_pairProfiles[idx].symbol = "CHFJPY"; + g_pairProfiles[idx].category = "VolatileCross"; + g_pairProfiles[idx].avgSpread = 2.5; + g_pairProfiles[idx].optimalSL_Mult = 1.8; + g_pairProfiles[idx].optimalTP_Mult = 2.8; + g_pairProfiles[idx].optimalRR = 1.5; + g_pairProfiles[idx].optimalTP = TP_BY_SWING_STRUCTURE; + g_pairProfiles[idx].minConfluence = 0.60; + g_pairProfiles[idx].minEntryQuality = 65.0; + g_pairProfiles[idx].allowScalping = true; // * v7.5 FIX: Auto-Opt decides scalping viability (was hardcoded false) + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = false; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 3; + g_pairProfiles[idx].riskPercent = 0.6; + g_pairProfiles[idx].chars.volatilityRank = 6; + g_pairProfiles[idx].chars.bestSession = "Asian/London"; + idx++; + // =========================================================== + // ADDITIONAL INDICES + // =========================================================== + // DE40 (DAX / GER40) + g_pairProfiles[idx].symbol = "DE40"; + g_pairProfiles[idx].category = "Index"; + g_pairProfiles[idx].avgSpread = 150.0; // * v7.5c FIX: was 1.5 -- must be POINTS (EUR1.50 = 150pts) + g_pairProfiles[idx].optimalSL_Mult = 2.5; // * v7.5c: wider SL for indices + g_pairProfiles[idx].optimalTP_Mult = 5.0; // * v7.5c: wider TP for indices + g_pairProfiles[idx].optimalRR = 2.0; + g_pairProfiles[idx].optimalTP = TP_BY_SWING_STRUCTURE; + g_pairProfiles[idx].minConfluence = 0.65; + g_pairProfiles[idx].minEntryQuality = 55.0; // * v7.5c: 65->55 for indices + g_pairProfiles[idx].allowScalping = true; // * v7.5 FIX: Auto-Opt decides scalping viability (was hardcoded false) + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = true; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 8; // * v7.5c: 4->8 for indices + g_pairProfiles[idx].riskPercent = 0.5; // * v7.5c FIX: 0.75->0.5 (index margin) + g_pairProfiles[idx].chars.volatilityRank = 6; + g_pairProfiles[idx].chars.bestSession = "London Open"; + idx++; + // UK100 (FTSE) + g_pairProfiles[idx].symbol = "UK100"; + g_pairProfiles[idx].category = "Index"; + g_pairProfiles[idx].avgSpread = 150.0; // * v7.5c FIX: was 1.5 -- must be POINTS + g_pairProfiles[idx].optimalSL_Mult = 2.5; // * v7.5c: wider SL for indices + g_pairProfiles[idx].optimalTP_Mult = 5.0; // * v7.5c: wider TP for indices + g_pairProfiles[idx].optimalRR = 2.0; + g_pairProfiles[idx].optimalTP = TP_BY_SWING_STRUCTURE; + g_pairProfiles[idx].minConfluence = 0.65; + g_pairProfiles[idx].minEntryQuality = 55.0; // * v7.5c: 65->55 for indices + g_pairProfiles[idx].allowScalping = true; // * v7.5 FIX: Auto-Opt decides scalping viability (was hardcoded false) + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = true; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 8; // * v7.5c: 4->8 for indices + g_pairProfiles[idx].riskPercent = 0.5; // * v7.5c FIX: 0.75->0.5 (index margin) + g_pairProfiles[idx].chars.volatilityRank = 5; + g_pairProfiles[idx].chars.bestSession = "London Open"; + idx++; + // JP225 (Nikkei) + g_pairProfiles[idx].symbol = "JP225"; + g_pairProfiles[idx].category = "Index"; + g_pairProfiles[idx].avgSpread = 10.0; + g_pairProfiles[idx].optimalSL_Mult = 2.5; // * v7.5c: wider SL for indices + g_pairProfiles[idx].optimalTP_Mult = 5.0; // * v7.5c: wider TP for indices; + g_pairProfiles[idx].optimalRR = 1.8; + g_pairProfiles[idx].optimalTP = TP_BY_SWING_STRUCTURE; + g_pairProfiles[idx].minConfluence = 0.65; + g_pairProfiles[idx].minEntryQuality = 55.0; // * v7.5c: 65->55 for indices + g_pairProfiles[idx].allowScalping = true; // * v7.5 FIX: Auto-Opt decides scalping viability (was hardcoded false) + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = true; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 8; // * v7.5c: 3->8 for indices + g_pairProfiles[idx].riskPercent = 0.5; // * v7.5c FIX: 0.75->0.5 (index margin) + g_pairProfiles[idx].chars.volatilityRank = 7; + g_pairProfiles[idx].chars.bestSession = "Asian Open"; + idx++; + // =========================================================== + // ENERGY + // =========================================================== + // USOIL (WTI Crude) + g_pairProfiles[idx].symbol = "USOIL"; + g_pairProfiles[idx].category = "Energy"; + g_pairProfiles[idx].avgSpread = 4.0; + g_pairProfiles[idx].optimalSL_Mult = 2.0; + g_pairProfiles[idx].optimalTP_Mult = 3.0; + g_pairProfiles[idx].optimalRR = 1.5; + g_pairProfiles[idx].optimalTP = TP_BY_SWING_STRUCTURE; + g_pairProfiles[idx].minConfluence = 0.65; + g_pairProfiles[idx].minEntryQuality = 68.0; + g_pairProfiles[idx].allowScalping = true; // * v7.5 FIX: Auto-Opt decides scalping viability (was hardcoded false) + g_pairProfiles[idx].allowSwing = true; + g_pairProfiles[idx].useLiquidityGrab = true; + g_pairProfiles[idx].useBOSRetest = false; + g_pairProfiles[idx].useOTE = true; + g_pairProfiles[idx].maxDailyTrades = 3; + g_pairProfiles[idx].riskPercent = 0.5; + g_pairProfiles[idx].chars.volatilityRank = 8; + g_pairProfiles[idx].chars.bestSession = "NY Open"; + idx++; + g_totalPairProfiles = idx; + ArrayResize(g_pairProfiles, g_totalPairProfiles); + if(g_verboseLog) { + Print("[TARGET] Loaded ", g_totalPairProfiles, " pair profiles"); + } +} +//+------------------------------------------------------------------+ +//| Load Current Pair Profile | +//+------------------------------------------------------------------+ +bool LoadCurrentPairProfile() +{ + if(!PAIR_OptimizationEnabled || !PAIR_AutoDetect) return false; + string currentSymbol = _Symbol; + // * FIX v7.3: Robust pair matching with broker alias support + // Brokers use various names: XAUUSD, GOLD, GOLD.m, GOLDm, XAUUSDm, etc. + // Normalize both sides and check aliases + string normalizedSymbol = currentSymbol; + StringToUpper(normalizedSymbol); + // Strip common suffixes: .m, .i, .e, .raw, .pro, .std, _SB, etc. + int dotPos = StringFind(normalizedSymbol, "."); + string cleanSymbol = (dotPos > 0) ? StringSubstr(normalizedSymbol, 0, dotPos) : normalizedSymbol; + // Also strip trailing lowercase suffixes (GOLDm -> GOLD, EURUSDi -> EURUSD) + while(StringLen(cleanSymbol) > 3) + { + string lastChar = StringSubstr(cleanSymbol, StringLen(cleanSymbol) - 1, 1); + // If last char is m, i, e, c (common broker suffixes) and not part of symbol + if(lastChar == "M" || lastChar == "I" || lastChar == "E" || lastChar == "C") + { + // Only strip if remaining part is >= 3 chars (avoid stripping "EUR" from "EURC") + string remaining = StringSubstr(cleanSymbol, 0, StringLen(cleanSymbol) - 1); + if(StringLen(remaining) >= 3) + cleanSymbol = remaining; + else + break; + } + else break; + } + for(int i = 0; i < g_totalPairProfiles; i++) + { + string profileSym = g_pairProfiles[i].symbol; + StringToUpper(profileSym); + // Direct match or cleaned match + bool matched = (StringFind(normalizedSymbol, profileSym) >= 0 || + StringFind(profileSym, cleanSymbol) >= 0 || + cleanSymbol == profileSym); + // * Alias matching for metals/commodities + if(!matched) + { + // Gold aliases + if(profileSym == "XAUUSD" && + (cleanSymbol == "GOLD" || cleanSymbol == "XAUUSD" || + StringFind(cleanSymbol, "GOLD") >= 0 || StringFind(cleanSymbol, "XAU") >= 0)) + matched = true; + // Silver aliases + else if(profileSym == "XAGUSD" && + (cleanSymbol == "SILVER" || StringFind(cleanSymbol, "XAG") >= 0 || + StringFind(cleanSymbol, "SILVER") >= 0)) + matched = true; + // Oil aliases + else if((profileSym == "USOIL" || profileSym == "XTIUSD") && + (cleanSymbol == "WTI" || cleanSymbol == "USOIL" || cleanSymbol == "XTIUSD" || + cleanSymbol == "CL" || StringFind(cleanSymbol, "OIL") >= 0 || + StringFind(cleanSymbol, "CRUDE") >= 0)) + matched = true; + // Index aliases + else if(profileSym == "NAS100" && + (cleanSymbol == "USTEC" || cleanSymbol == "NDX" || cleanSymbol == "NQ" || + StringFind(cleanSymbol, "NAS") >= 0 || StringFind(cleanSymbol, "NASDAQ") >= 0)) + matched = true; + else if(profileSym == "US30" && + (cleanSymbol == "DJ30" || cleanSymbol == "DOW" || cleanSymbol == "DJI" || + StringFind(cleanSymbol, "US30") >= 0 || StringFind(cleanSymbol, "DOW") >= 0)) + matched = true; + // * v6.3: Additional index aliases + else if(profileSym == "US100" && + (cleanSymbol == "USTEC" || cleanSymbol == "NDX" || cleanSymbol == "NQ" || + cleanSymbol == "NAS100" || StringFind(cleanSymbol, "NAS") >= 0 || + StringFind(cleanSymbol, "NASDAQ") >= 0)) + matched = true; + else if(profileSym == "US500" && + (cleanSymbol == "SPX" || cleanSymbol == "SPX500" || cleanSymbol == "SP500" || + StringFind(cleanSymbol, "US500") >= 0 || StringFind(cleanSymbol, "SPX") >= 0)) + matched = true; + else if(profileSym == "DE40" && + (cleanSymbol == "GER40" || cleanSymbol == "DAX" || cleanSymbol == "DE30" || + StringFind(cleanSymbol, "DAX") >= 0 || StringFind(cleanSymbol, "GER") >= 0)) + matched = true; + else if(profileSym == "UK100" && + (cleanSymbol == "FTSE" || cleanSymbol == "FTSE100" || + StringFind(cleanSymbol, "FTSE") >= 0 || StringFind(cleanSymbol, "UK100") >= 0)) + matched = true; + else if(profileSym == "JP225" && + (cleanSymbol == "NIKKEI" || cleanSymbol == "NIK225" || cleanSymbol == "JPN225" || + StringFind(cleanSymbol, "NIK") >= 0 || StringFind(cleanSymbol, "JP225") >= 0 || + StringFind(cleanSymbol, "NIKKEI") >= 0)) + matched = true; + } + if(matched) + { + g_currentPairProfile = g_pairProfiles[i]; + // g_gates.computed retired + g_currentSymbolCategory = g_pairProfiles[i].category; + // Apply settings if enabled + if(PAIR_UseOptimalSettings) { + ApplyPairOptimalSettings(); + } + if(g_verboseLog) { + Print("[TARGET] Loaded profile for: ", _Symbol, " (", g_autoOptParams.pair_category, ")"); + } + return true; + } + } + // Pair not found - use default profile + // g_gates.computed retired + g_currentSymbolCategory = "Unknown"; + // Create default profile + if(g_verboseLog) { + Print("[WARN] Pair not in database: ", currentSymbol); + Print(" Using default profile"); + } + return false; +} +//+------------------------------------------------------------------+ +//| Apply Pair Optimal Settings | +//+------------------------------------------------------------------+ +void ApplyPairOptimalSettings() +{ + // Both systems retired. Values now from: + // ApplyPairTFProfile() → g_autoOptParams + g_workingMinRiskReward + // ComputeActiveGates() → g_gates +} +//+------------------------------------------------------------------+ +//| Centralized SL/TP/RR accessors — read from g_autoOptParams | +//| (pair table values applied by ApplyPairTFProfile, step 10) | +//+------------------------------------------------------------------+ +double GetActiveSLMult() +{ + if(AutoOpt_Enabled && g_gates.computed && g_autoOptParams.sl_atr_mult > 0) + return g_autoOptParams.sl_atr_mult; + return EA_StopLossATR; +} +double GetActiveTP1Mult() +{ + if(AutoOpt_Enabled && g_gates.computed && g_autoOptParams.tp_atr_mult > 0) + return g_autoOptParams.tp_atr_mult; + return EA_TP1_ATR; +} +double GetActiveTP2Mult() +{ + if(AutoOpt_Enabled && g_gates.computed && g_autoOptParams.tp_atr_mult > 0) + { + double tp2R = 1.35, tp3R = 1.75; + GetTP2TP3Ratios(g_autoOptParams.tf_category, g_autoOptParams.pair_category, tp2R, tp3R); + return g_autoOptParams.tp_atr_mult * tp2R; + } + return EA_TP2_ATR; +} +double GetActiveTP3Mult() +{ + if(AutoOpt_Enabled && g_gates.computed && g_autoOptParams.tp_atr_mult > 0) + { + double tp2R = 1.35, tp3R = 1.75; + GetTP2TP3Ratios(g_autoOptParams.tf_category, g_autoOptParams.pair_category, tp2R, tp3R); + return g_autoOptParams.tp_atr_mult * tp3R; + } + return EA_TP3_ATR; +} +double GetActiveMinRR() +{ + if(AutoOpt_Enabled && g_workingMinRiskReward > 0) + return g_workingMinRiskReward; + return EA_MinRR; +} +//+------------------------------------------------------------------+ +//| Get Optimal Min Confluence for Current Pair | +//+------------------------------------------------------------------+ +double GetOptimalMinConfluence() +{ + // [ML] Auto-Opt override + if(AutoOpt_Enabled && g_autoOptInitialized) + return g_autoOptParams.min_confluence; + if(!PAIR_OptimizationEnabled) return MinConfluence; + if(PAIR_ManualMinConf > 0) return PAIR_ManualMinConf; + return g_autoOptParams.min_confluence; +} +//+------------------------------------------------------------------+ +//| Get Pair Profile Summary | +//+------------------------------------------------------------------+ +string GetPairProfileSummary() +{ + if(!g_gates.computed) return "No profile loaded"; + return StringFormat("%s | %s | minRR=%.2f | minScore=%.0f | minConf=%d | SL=%.2f | Risk=%.2f%%", + _Symbol, g_autoOptParams.pair_category, + g_workingMinRiskReward, g_gates.minScore, g_gates.minConfirmations, + g_autoOptParams.sl_atr_mult, g_autoOptParams.risk_pct); +} +//+------------------------------------------------------------------+ +//| =============================================================== | +//| [ML] FULL AUTO-OPTIMIZATION ENGINE v1.0 | +//| =============================================================== | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| Initialize Auto-Optimization System | +//+------------------------------------------------------------------+ +void InitializeAutoOptimization() +{ + if(!AutoOpt_Enabled) return; + // Create ATR handle for calibration + g_autoOpt_ATR_Handle = iATR(_Symbol, _Period, AutoOpt_ATR_Period); + if(g_autoOpt_ATR_Handle == INVALID_HANDLE) + { + Print("[WARN] Auto-Opt: Failed to create ATR handle, using cached ATR"); + } + // Take initial market snapshot + TakeMarketSnapshot(); + // Calculate initial parameters + CalculateAutoOptParameters(); + // Apply to working variables + ApplyAutoOptToWorkingVars(); + g_autoOptInitialized = true; + g_lastAutoOptCalc = TimeCurrent(); + Print("[ML] ==================================================="); + Print("[ML] FULL AUTO-OPTIMIZATION INITIALIZED"); + Print("[ML] Symbol: ", _Symbol, " | TF: ", EnumToString(_Period)); + Print("[ML] TF Category: ", GetTFCategoryName(g_marketSnap.tf_category)); + Print("[ML] Vol Regime: ", GetVolRegimeName(g_marketSnap.vol_regime)); + Print("[ML] Pair Category: ", g_autoOptParams.pair_category); + Print("[ML] Auto SL Mult: ", DoubleToString(g_autoOptParams.sl_atr_mult, 2)); + Print("[ML] Auto TP Mult: ", DoubleToString(g_autoOptParams.tp_atr_mult, 2)); + Print("[ML] Auto Risk %: ", DoubleToString(g_autoOptParams.risk_pct, 2)); + Print("[ML] Auto Min Score: ", g_autoOptParams.min_entry_score); + Print("[ML] Auto Min Confluence: ", DoubleToString(g_autoOptParams.min_confluence, 2)); + Print("[ML] Allow Scalping: ", g_autoOptParams.allow_scalping ? "YES (auto)" : "NO (auto)"); + Print("[ML] Allow Swing: ", g_autoOptParams.allow_swing ? "YES (auto)" : "NO (auto)"); + // * v9.03: Show effective TF-aware recalc interval + int _tfMin = PeriodSeconds() / 60; + int _effectiveRecalc = MathMax(AutoOpt_RecalcMinutes, _tfMin * 1); // * v9.36 FIX#152: x4->x1 (1 candle) + Print("[ML] Recalc Every: ", _effectiveRecalc, " min (input=", AutoOpt_RecalcMinutes, + ", TF=", EnumToString(_Period), " -> 1 candle=", _tfMin * 1, "min)"); + Print("[ML] ==================================================="); +} +//+------------------------------------------------------------------+ +//| Periodic recalculation (call from OnTimer or OnCalculate) | +//+------------------------------------------------------------------+ +void UpdateAutoOptimization() +{ + if(!AutoOpt_Enabled || !g_autoOptInitialized) return; + datetime now = TimeCurrent(); + // * v9.36 FIX#152: TF-Aware Recalc — 1 candle per TF (was x4 candles) + // Old x4 formula: H4->960min(16h), D1->5760min(4days) = params stale for entire sessions + // New x1 formula: M5=5m, M15=15m, H1=60m, H4=240m, D1=1440m = recalc each new candle + // Input default changed 15->1 so floor never overrides TF-based interval + int tfMinutes = PeriodSeconds() / 60; + int effectiveRecalcMinutes = MathMax(AutoOpt_RecalcMinutes, tfMinutes * 1); + // * FIX#110: EVENT-TRIGGERED RECALC -- force immediate recalc on significant market events. + // Problem: Time-based interval misses critical moments when market CHARACTER changes suddenly: + // M5/M15: London Open (07:00), NY Open (13:30) -> ATR can 2-3x in minutes + // H1: Session transitions still matter (tfSessionScale=0.7) + // H4: New trading day -> DoW adjustments, daily range resets + // D1: New week (Monday) -> weekly bias, DoW=Monday AM avoidance + // Fix: Track last session/day/week at recalc. If changed -> force recalc regardless of timer. + // ATR spike detection: if live ATR jumped vs snapshot ATR -> conditions changed -> recalc. + // Cooldown: min 1 candle between forced recalcs (prevent churn on choppy opens). + bool forceRecalc = false; + string forceReason = ""; + // Minimum cooldown between ANY recalcs = 1 candle duration (prevent churn) + int minCooldownSecs = PeriodSeconds(); + bool cooldownOK = ((now - g_lastAutoOptCalc) >= minCooldownSecs); + if(cooldownOK && (now - g_lastAutoOptCalc) < effectiveRecalcMinutes * 60) + { + MqlDateTime dt; + TimeToStruct(now, dt); + int hour = dt.hour; + int dow = dt.day_of_week; + ENUM_TF_CATEGORY tfCat = ClassifyTimeframe(_Period); + // ── M5 / M15 / H1: Session transition events ───────────────── + if(tfCat <= TF_CAT_INTRASWING) // M5, M15, M30, H1 + { + // Determine current session bucket (same logic as GetCurrentSessionSL) + string curSess; + if(hour >= 7 && hour < 8) curSess = "LDN_OPEN"; // London first hour + else if(hour >= 12 && hour < 13) curSess = "NY_OPEN"; // NY first hour + else if(hour >= 15 && hour < 16) curSess = "OVERLAP"; // peak overlap + else if(hour >= 23 || hour < 1) curSess = "ASIA_OPEN"; // Asian open + else curSess = "MID"; + static string s_lastSessionBucket = ""; + if(curSess != s_lastSessionBucket) + { + forceRecalc = true; + forceReason = StringFormat("Session transition: %s->%s", s_lastSessionBucket, curSess); + s_lastSessionBucket = curSess; + } + } + // ── H4 / D1: New Day event ──────────────────────────────────── + if(!forceRecalc && tfCat >= TF_CAT_SWING) // H4, D1, W1 + { + static int s_lastDayOfYear = -1; + MqlDateTime dtCheck; TimeToStruct(now, dtCheck); + int curDoy = dtCheck.day_of_year; + if(s_lastDayOfYear >= 0 && curDoy != s_lastDayOfYear) + { + forceRecalc = true; + forceReason = StringFormat("New day (DoY %d->%d)", s_lastDayOfYear, curDoy); + } + s_lastDayOfYear = curDoy; + // D1: New Week (Monday) event + if(!forceRecalc && _Period >= PERIOD_D1 && dow == 1) + { + static int s_lastWeekMonday = -1; + int curWeek = (int)(now / (7 * 86400)); + if(s_lastWeekMonday >= 0 && curWeek != s_lastWeekMonday) + { + forceRecalc = true; + forceReason = "New week (Monday open)"; + } + s_lastWeekMonday = curWeek; + } + } + // ── ALL TFs: ATR spike detection ────────────────────────────── + // If live ATR has jumped significantly vs snapshot -> market character changed + if(!forceRecalc && g_marketSnap.current_atr > 0 && g_cachedATR > 0) + { + // ATR spike threshold scales with TF (short TF = more sensitive) + double atrSpikeThresh; + ENUM_TF_CATEGORY tfCatATR = ClassifyTimeframe(_Period); + switch(tfCatATR) + { + case TF_CAT_SCALP: atrSpikeThresh = 1.35; break; // M5: +35% + case TF_CAT_INTRADAY: atrSpikeThresh = 1.40; break; // M15: +40% + case TF_CAT_INTRASWING: atrSpikeThresh = 1.50; break; // H1: +50% + case TF_CAT_SWING: atrSpikeThresh = 1.60; break; // H4: +60% + case TF_CAT_POSITION: atrSpikeThresh = 1.80; break; // D1: +80% + default: atrSpikeThresh = 1.40; break; + } + if(g_cachedATR > g_marketSnap.current_atr * atrSpikeThresh) + { + forceRecalc = true; + forceReason = StringFormat("ATR spike: %.1f->%.1f (%.0f%%)", + g_marketSnap.current_atr / _Point, + g_cachedATR / _Point, + (g_cachedATR / g_marketSnap.current_atr - 1.0) * 100.0); + } + } + if(!forceRecalc) return; // No event, no time -> skip + } + if(forceRecalc && AutoOpt_ShowLog) + PrintFormat("* FIX#110 AutoOpt FORCE RECALC [TF=%s]: %s", + EnumToString(_Period), forceReason); + // Recalculate + TakeMarketSnapshot(); + CalculateAutoOptParameters(); + ApplyAutoOptToWorkingVars(); + g_lastAutoOptCalc = now; + g_autoOptRecalcCount++; + g_autoOptParams.recalc_count = g_autoOptRecalcCount; + g_autoOptParams.last_update = now; + // * v9.15 FIX#39: Ξαναδημιούργησε TC EMA handles αν άλλαξαν οι periods + // Τα EMA handles δημιουργούνται 1 φορά στο OnInit με raw input. + // Αν το AutoOpt αλλάξει τα periods (π.χ. M15->H1: Fast 21->34), + // τα handles πρέπει να ξαναφτιαχτούν ώστε ο υπολογισμός να είναι σωστός. + if(g_workingTC_EMA_Fast > 0 && g_workingTC_EMA_Slow > 0) + { + static int s_lastEmaFast = 0; + static int s_lastEmaSlow = 0; + if(s_lastEmaFast != g_workingTC_EMA_Fast || s_lastEmaSlow != g_workingTC_EMA_Slow) + { + if(g_tcEmaFastHandle != INVALID_HANDLE) IndicatorRelease(g_tcEmaFastHandle); + if(g_tcEmaSlowHandle != INVALID_HANDLE) IndicatorRelease(g_tcEmaSlowHandle); + g_tcEmaFastHandle = iMA(_Symbol, _Period, g_workingTC_EMA_Fast, 0, MODE_EMA, PRICE_CLOSE); + g_tcEmaSlowHandle = iMA(_Symbol, _Period, g_workingTC_EMA_Slow, 0, MODE_EMA, PRICE_CLOSE); + s_lastEmaFast = g_workingTC_EMA_Fast; + s_lastEmaSlow = g_workingTC_EMA_Slow; + if(g_verboseLog) + PrintFormat("* v9.15 TC EMA handles updated: Fast=%d Slow=%d (TF=%s)", + g_workingTC_EMA_Fast, g_workingTC_EMA_Slow, EnumToString(_Period)); + } + } + if(g_verboseLog) + { + Print("[ML] Auto-Opt Recalc #", g_autoOptRecalcCount, + " | Vol:", GetVolRegimeName(g_marketSnap.vol_regime), + " | SL:", DoubleToString(g_autoOptParams.sl_atr_mult, 2), + " | TP:", DoubleToString(g_autoOptParams.tp_atr_mult, 2), + " | Risk:", DoubleToString(g_autoOptParams.risk_pct, 2), "%", + " | NextRecalc:", effectiveRecalcMinutes, "min (TF=", tfMinutes, "min)"); + } +} +//+------------------------------------------------------------------+ +//| Take Market Snapshot - measure current conditions | +//+------------------------------------------------------------------+ +void TakeMarketSnapshot() +{ + g_marketSnap.snapshot_time = TimeCurrent(); + // === ATR Measurement === + double atrBuf[]; + ArraySetAsSeries(atrBuf, true); + if(g_autoOpt_ATR_Handle != INVALID_HANDLE) + { + if(CopyBuffer(g_autoOpt_ATR_Handle, 0, 0, AutoOpt_VolatilityLookback, atrBuf) > 0) + { + g_marketSnap.current_atr = atrBuf[0]; + // Calculate ATR averages + double sum20 = 0, sum50 = 0; + int cnt20 = MathMin(20, ArraySize(atrBuf)); + int cnt50 = MathMin(50, ArraySize(atrBuf)); + for(int i = 0; i < cnt50; i++) + { + if(i < cnt20) sum20 += atrBuf[i]; + sum50 += atrBuf[i]; + } + g_marketSnap.avg_atr_20 = (cnt20 > 0) ? sum20 / cnt20 : g_marketSnap.current_atr; + g_marketSnap.avg_atr_50 = (cnt50 > 0) ? sum50 / cnt50 : g_marketSnap.current_atr; + // ATR percentile (where current ATR sits vs history) + int belowCount = 0; + int total = ArraySize(atrBuf); + for(int i = 1; i < total; i++) + { + if(atrBuf[i] < g_marketSnap.current_atr) belowCount++; + } + g_marketSnap.atr_percentile = (total > 1) ? (double)belowCount / (total - 1) * 100.0 : 50.0; + } + } + else if(g_cachedATR > 0) + { + g_marketSnap.current_atr = g_cachedATR; + g_marketSnap.avg_atr_20 = g_cachedATR; + g_marketSnap.avg_atr_50 = g_cachedATR; + g_marketSnap.atr_percentile = 50.0; + } + // === Spread Measurement === + double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + g_marketSnap.current_spread = (ask - bid) / _Point; // In POINTS + // * v7.5c FIX: avgSpread UNIT MISMATCH -- profile avgSpread was in PIPS (forex) or PRICE (indices) + // but current_spread is in POINTS. This caused spread_ratio=96.7 for US100 (145pts / 1.5$) + // -> scalping permanently blocked! Now: use live g_spreadAnalysis when available (already in points), + // fall back to profile value converted pip->points. + if(g_spreadAnalysis.avgSpread > 0 && g_spreadAnalysis.lastUpdate > 0) + { + g_marketSnap.avg_spread = g_spreadAnalysis.avgSpread; // Already in POINTS from live data + } + else if(g_gates.computed) + { + // Profile avgSpread is in PIPS -- convert to POINTS + double pipToPoints = (g_pipValue > 0 && _Point > 0) ? g_pipValue / _Point : 1.0; + g_marketSnap.avg_spread = g_autoOptParams.max_spread_pips * pipToPoints; + } + else + { + g_marketSnap.avg_spread = g_marketSnap.current_spread; // No profile, use current as baseline + } + g_marketSnap.spread_ratio = (g_marketSnap.avg_spread > 0) ? g_marketSnap.current_spread / g_marketSnap.avg_spread : 1.0; + // * v7.5c: Diagnostic log for spread ratio (was causing spread_ratio=96.7 -> scalping permanently blocked) + if(g_verboseLog) + Print("* SpreadDiag: current=", DoubleToString(g_marketSnap.current_spread, 1), "pts", + " | avgSpread=", DoubleToString(g_marketSnap.avg_spread, 1), "pts", + " | ratio=", DoubleToString(g_marketSnap.spread_ratio, 2), + " | source=", (g_spreadAnalysis.avgSpread > 0 && g_spreadAnalysis.lastUpdate > 0) ? "LIVE" : + (g_gates.computed ? "PROFILE" : "CURRENT")); + // === Daily Range === + double dayHigh = iHigh(_Symbol, PERIOD_D1, 0); + double dayLow = iLow(_Symbol, PERIOD_D1, 0); + g_marketSnap.daily_range = (dayHigh - dayLow) / _Point; + // Average daily range -- * v9.16: uses g_workingRegime_ADRPeriod (was hardcoded 20) + double drSum = 0; + int adrPeriod = MathMax(5, g_workingRegime_ADRPeriod); // safety floor + for(int i = 1; i <= adrPeriod; i++) + { + drSum += (iHigh(_Symbol, PERIOD_D1, i) - iLow(_Symbol, PERIOD_D1, i)) / _Point; + } + g_marketSnap.avg_daily_range = drSum / (double)adrPeriod; + // Hourly range + g_marketSnap.hourly_range = (iHigh(_Symbol, PERIOD_H1, 0) - iLow(_Symbol, PERIOD_H1, 0)) / _Point; + // === Volatility Ratio === + g_marketSnap.volatility_ratio = (g_marketSnap.avg_atr_50 > 0) ? + g_marketSnap.current_atr / g_marketSnap.avg_atr_50 : 1.0; + // === Volatility Regime Classification === + if(g_marketSnap.atr_percentile <= 15) g_marketSnap.vol_regime = VOL_VERY_LOW; + else if(g_marketSnap.atr_percentile <= 35) g_marketSnap.vol_regime = VOL_LOW; + else if(g_marketSnap.atr_percentile <= 65) g_marketSnap.vol_regime = VOL_NORMAL; + else if(g_marketSnap.atr_percentile <= 85) g_marketSnap.vol_regime = VOL_HIGH; + else g_marketSnap.vol_regime = VOL_EXTREME; + g_marketSnap.is_high_vol = (g_marketSnap.vol_regime >= VOL_HIGH); + g_marketSnap.is_low_vol = (g_marketSnap.vol_regime <= VOL_LOW); + // === Trend Detection (simple EMA comparison) === + double close0 = iClose(_Symbol, _Period, 0); + double ema20 = 0, ema50 = 0; + // Simple moving averages as proxy + double sum1 = 0, sum2 = 0; + int bars1 = MathMin(20, iBars(_Symbol, _Period) - 1); + int bars2 = MathMin(50, iBars(_Symbol, _Period) - 1); + for(int i = 0; i < bars2; i++) + { + double c = iClose(_Symbol, _Period, i); + if(i < bars1) sum1 += c; + sum2 += c; + } + ema20 = (bars1 > 0) ? sum1 / bars1 : close0; + ema50 = (bars2 > 0) ? sum2 / bars2 : close0; + // Trend direction and strength (safe divide) + double safeATR = (g_marketSnap.current_atr > 0) ? g_marketSnap.current_atr : _Point * 100; + if(close0 > ema20 && ema20 > ema50) + { + g_marketSnap.trend_direction = 1; + g_marketSnap.trend_strength = MathMin(100, (close0 - ema50) / safeATR * 25); + } + else if(close0 < ema20 && ema20 < ema50) + { + g_marketSnap.trend_direction = -1; + g_marketSnap.trend_strength = MathMin(100, (ema50 - close0) / safeATR * 25); + } + else + { + g_marketSnap.trend_direction = 0; + g_marketSnap.trend_strength = MathAbs(close0 - ema50) / safeATR * 15; + } + g_marketSnap.is_trending = (g_marketSnap.trend_strength > 40); + g_marketSnap.is_ranging = (g_marketSnap.trend_strength < 25); + g_marketSnap.range_score = 100.0 - g_marketSnap.trend_strength; + // === Timeframe Category === + g_marketSnap.tf_category = ClassifyTimeframe(_Period); + // === Time Info === + MqlDateTime dt; + TimeToStruct(TimeCurrent(), dt); + g_marketSnap.current_hour = dt.hour; + g_marketSnap.current_dow = dt.day_of_week; + // Active session detection + if(dt.hour >= 0 && dt.hour < 8) g_marketSnap.active_session = "Asian"; + else if(dt.hour >= 7 && dt.hour < 12) g_marketSnap.active_session = "London"; + else if(dt.hour >= 12 && dt.hour < 17) g_marketSnap.active_session = "LDN/NY Overlap"; + else if(dt.hour >= 13 && dt.hour < 22) g_marketSnap.active_session = "New York"; + else g_marketSnap.active_session = "Off-Hours"; +} +//+------------------------------------------------------------------+ +//| Classify timeframe into category | +//+------------------------------------------------------------------+ +ENUM_TF_CATEGORY ClassifyTimeframe(ENUM_TIMEFRAMES tf) +{ + if(tf <= PERIOD_M5) return TF_CAT_SCALP; + if(tf <= PERIOD_M30) return TF_CAT_INTRADAY; + if(tf == PERIOD_H1) return TF_CAT_INTRASWING; // * v9.03 FIX#11b: H1 gets own category + if(tf <= PERIOD_H4) return TF_CAT_SWING; + return TF_CAT_POSITION; +} +//+------------------------------------------------------------------+ +//| * v9.16 FIX#46a: TF-aware TP2/TP3 ratio multipliers | +//| Returns TP2 and TP3 as ratios of TP1. | +//| SCALP: tight spacing (quick close), SWING: wide (let runner run) | +//| Also pair-aware: volatile pairs need wider TP spacing. | +//+------------------------------------------------------------------+ +void GetTP2TP3Ratios(ENUM_TF_CATEGORY tfCat, string pairCat, double &tp2Ratio, double &tp3Ratio) +{ + // -- Base ratios per TF -- + switch(tfCat) + { + case TF_CAT_SCALP: // M1-M5: tight targets, quick close + tp2Ratio = 1.25; // TP2 = TP1 x 1.25 + tp3Ratio = 1.50; // TP3 = TP1 x 1.50 + break; + case TF_CAT_INTRADAY: // M15-M30: standard spacing + tp2Ratio = 1.35; // TP2 = TP1 x 1.35 + tp3Ratio = 1.75; // TP3 = TP1 x 1.75 + break; + case TF_CAT_INTRASWING: // H1: wider gaps + tp2Ratio = 1.40; // TP2 = TP1 x 1.40 + tp3Ratio = 1.90; // TP3 = TP1 x 1.90 + break; + case TF_CAT_SWING: // H4: runner needs room + tp2Ratio = 1.50; // TP2 = TP1 x 1.50 + tp3Ratio = 2.10; // TP3 = TP1 x 2.10 + break; + case TF_CAT_POSITION: // D1+: maximum spacing + tp2Ratio = 1.60; // TP2 = TP1 x 1.60 + tp3Ratio = 2.30; // TP3 = TP1 x 2.30 + break; + default: + tp2Ratio = 1.35; + tp3Ratio = 1.75; + break; + } + // -- Pair category adjustment -- + // Volatile pairs need wider TP spacing (moves come in bursts) + if(pairCat == "VolatileCross" || pairCat == "Crypto") + { + tp2Ratio *= 1.10; // +10% wider + tp3Ratio *= 1.10; + } + else if(pairCat == "Metal" || pairCat == "Energy") + { + tp2Ratio *= 1.05; // +5% wider (Gold/Oil have momentum) + tp3Ratio *= 1.08; + } + else if(pairCat == "Index") + { + tp2Ratio *= 1.08; // +8% wider (indices trend well) + tp3Ratio *= 1.12; + } + // Major/Cross: * v9.16 FIX#53f: Majors and Crosses GET wider TP2/TP3. + // Evidence: 0 TP2 hits in 71 trades. TP2 = TP1x1.35 = too close. + // After SL Ladder moves SL to 50% of TP1, still need room. + // Widen TP2/TP3 to give profitable runners more space. + if(pairCat == "Major") + { + tp2Ratio *= 1.15; // +15% wider (was 1.0 = no adjustment!) + tp3Ratio *= 1.20; // +20% wider + } + else if(pairCat == "Cross") + { + tp2Ratio *= 1.10; // +10% wider + tp3Ratio *= 1.15; // +15% wider + } +} +//+------------------------------------------------------------------+ +//| Get pair category (uses existing profiles or auto-detect) | +//+------------------------------------------------------------------+ +string AutoDetectPairCategory() +{ + if(g_gates.computed && g_autoOptParams.pair_category != "") return g_autoOptParams.pair_category; + string sym = _Symbol; + // Metals + if(StringFind(sym, "XAU") >= 0 || StringFind(sym, "GOLD") >= 0) return "Metal"; + if(StringFind(sym, "XAG") >= 0 || StringFind(sym, "SILVER") >= 0) return "Metal"; + if(StringFind(sym, "XPTUSD") >= 0 || StringFind(sym, "XPDUSD") >= 0) return "Metal"; + // Indices + if(StringFind(sym, "US500") >= 0 || StringFind(sym, "US100") >= 0 || + StringFind(sym, "US30") >= 0 || StringFind(sym, "SPX") >= 0 || + StringFind(sym, "NAS") >= 0 || StringFind(sym, "DAX") >= 0 || + StringFind(sym, "FTSE") >= 0 || StringFind(sym, "NIK") >= 0 || + StringFind(sym, "DE40") >= 0 || StringFind(sym, "UK100") >= 0 || + StringFind(sym, "JP225") >= 0 || StringFind(sym, "AUS200") >= 0) return "Index"; + // Crypto + if(StringFind(sym, "BTC") >= 0 || StringFind(sym, "ETH") >= 0 || + StringFind(sym, "LTC") >= 0 || StringFind(sym, "XRP") >= 0) return "Crypto"; + // Oil/Energy + if(StringFind(sym, "OIL") >= 0 || StringFind(sym, "WTI") >= 0 || + StringFind(sym, "BRENT") >= 0 || StringFind(sym, "UKOIL") >= 0 || + StringFind(sym, "NGAS") >= 0) return "Energy"; + // Forex - Major + string majors[] = {"EURUSD","GBPUSD","USDJPY","USDCHF","AUDUSD","USDCAD","NZDUSD"}; + for(int i = 0; i < ArraySize(majors); i++) + { + if(StringFind(sym, majors[i]) >= 0) return "Major"; + } + // Forex - Exotic + if(StringFind(sym, "TRY") >= 0 || StringFind(sym, "ZAR") >= 0 || + StringFind(sym, "MXN") >= 0 || StringFind(sym, "PLN") >= 0 || + StringFind(sym, "HUF") >= 0 || StringFind(sym, "CZK") >= 0 || + StringFind(sym, "SEK") >= 0 || StringFind(sym, "NOK") >= 0 || + StringFind(sym, "SGD") >= 0 || StringFind(sym, "HKD") >= 0) return "Exotic"; + // Forex - Cross (JPY crosses are volatile) + if(StringFind(sym, "JPY") >= 0) return "VolatileCross"; + // Default: Cross + return "Cross"; +} +//+------------------------------------------------------------------+ +//| CORE: Calculate all auto-optimized parameters | +//+------------------------------------------------------------------+ +void CalculateAutoOptParameters() +{ + // Get base pair category + g_autoOptParams.pair_category = AutoDetectPairCategory(); + g_autoOptParams.tf_category = g_marketSnap.tf_category; + g_autoOptParams.vol_regime = g_marketSnap.vol_regime; + // Aggressiveness factor (0.0 to 1.0) + double aggr = AutoOpt_Aggressiveness / 100.0; + // +===========================================================+ + // | STEP 1: BASE PARAMETERS FROM PAIR CATEGORY | + // +===========================================================+ + SetBasePairParameters(g_autoOptParams.pair_category); + // +===========================================================+ + // | STEP 2: TIMEFRAME ADJUSTMENTS | + // +===========================================================+ + if(AutoOpt_AutoTimeframe) + ApplyTimeframeAdjustments(); + // +===========================================================+ + // | STEP 2.5: PAIR-SPECIFIC DETECTION ADJUSTMENTS (v9.03) | + // | Applied AFTER TF adjustments to scale detection per pair | + // +===========================================================+ + ApplyPairDetectionAdjustments(g_autoOptParams.pair_category); + // +===========================================================+ + // | STEP 3: VOLATILITY ADJUSTMENTS | + // +===========================================================+ + if(AutoOpt_AutoVolatility) + ApplyVolatilityAdjustments(); + // +===========================================================+ + // | STEP 4: SPREAD ADJUSTMENTS | + // +===========================================================+ + if(AutoOpt_AutoSpread) + ApplySpreadAdjustments(); + // +===========================================================+ + // | STEP 5: SESSION / TIME ADJUSTMENTS | + // +===========================================================+ + if(AutoOpt_AutoSession) + ApplySessionAdjustments(); + // +===========================================================+ + // | STEP 6: STRATEGY SELECTION | + // +===========================================================+ + if(AutoOpt_AutoStrategies) + ApplyStrategySelection(); + // +===========================================================+ + // | STEP 7: AGGRESSIVENESS SCALING | + // +===========================================================+ + ApplyAggressivenessScaling(aggr); + // +===========================================================+ + // | STEP 8: SAFETY CLAMPS | + // +===========================================================+ + ClampAutoOptParameters(); + // +===========================================================+ + // | STEP 9: APPLY POSITION MANAGEMENT OVERRIDES (v9.03) | + // +===========================================================+ + ApplyPositionManagementOverrides(); + // +===========================================================+ + // | STEP 10: PAIR+TF PRECISE PROFILE OVERRIDE (v9.31 FIX#102)| + // | Applied LAST -- overrides category defaults with exact | + // | values from the settings table (spread/SL/TP/risk/comm) | + // | Does NOT touch FIX#100/101 (those fire at trade open) | + // +===========================================================+ + ApplyPairTFProfile(); + g_autoOptParams.applied_reason = StringFormat( + "%s|%s|%s|Aggr:%d", + g_autoOptParams.pair_category, + GetTFCategoryName(g_autoOptParams.tf_category), + GetVolRegimeName(g_autoOptParams.vol_regime), + AutoOpt_Aggressiveness + ); +} +//+------------------------------------------------------------------+ +//| Step 1: Set base parameters from pair category | +//+------------------------------------------------------------------+ +void SetBasePairParameters(string category) +{ + // If we have a loaded pair profile, use it as base + if(g_gates.computed && PAIR_UseOptimalSettings) + { + // [pair table owns] sl/tp_atr_mult — set by ApplyPairTFProfile + // [pair table owns] g_autoOptParams.min_rr = GetPairTFMinRR(_Symbol, _Period); // * v9.39 FIX#172: TF-aware (was flat optimalRR -> FIX#19 spam) + // * v8.05 FIX: AutoOpt starts from full EA_RiskPercent. + // Profile.riskPercent fraction removed -- it was silently cutting user risk (5%->2.5%->2%). + // AutoOpt may reduce dynamically via volatility/kelly calculations below, but ceiling = EA_RiskPercent. + g_autoOptParams.risk_pct = EA_RiskPercent; + g_autoOptParams.max_daily_trades = EA_MaxDailyTrades; // * v7.4: Use GLOBAL input, not pair profile + // [pair table owns] g_autoOptParams.min_confluence — set by ApplyPairTFProfile + // [pair table owns] g_autoOptParams.min_entry_quality = g_currentPairProfile.minEntryQuality; + // * FIX#460b: PAIR_ManualMinConf > 0 → override min_entry_quality for real trades. + // BEFORE: PAIR_ManualMinConf only fed the legacy GetMinConfluence() path (visual tracker). + // FIX: also feed into g_autoOptParams.min_entry_quality (real trade quality gate). + // MinConfluence=4 (0-1 scale) and min_entry_quality (0-85 score) are different systems — + // PAIR_ManualMinConf overrides min_entry_quality directly (user sets the score floor). + if(PAIR_ManualMinConf > 0) + { + // [pair table owns] min_entry_quality — set by ApplyPairTFProfile + } + // [pair table owns] allow_liq_grab — set by ApplyPairTFProfile via g_workingAllowLIQ + // [pair table owns] allow_bos_retest + // [pair table owns] allow_ote + // [pair table owns] allow_scalping + // [pair table owns] allow_swing + } + else + { + // Default base by category + // * v7.9 FIX ROOT CAUSE BUG#1: All categories previously used hardcoded risk_pct (e.g. 0.5%) + // which completely ignored EA_RiskPercent=5%. The volatility/spread multipliers then scaled + // 0.5% down to 0.25%, while EA_RiskPercent was never used. This caused 0.25% actual risk + // regardless of what the user set. + // Fix: start from EA_RiskPercent as the base. Volatility/spread adjustments will now + // scale proportionally FROM the user's actual risk preference. + // e.g. EA_RiskPercent=5%, VOL_HIGHx0.7 -> 3.5% (not 0.35%) + // The category-specific risk_pct fractions below act as CAP_RATIO (max fraction of EA_RiskPercent): + // Major -> up to 100% of EA_RiskPercent | Index/Metal -> up to 80% (safer due to volatility) + if(category == "Major") + { + // * v9.22 FIX#61: EURUSD M15 AutoOpt corrected + // Backtest proof: avg win=$25, avg loss=$77 -> R:R=0.33 (broken) + // Fix: tighter SL (1.1) + wider TP (4.5) + stricter minRR (1.8) + // This ensures even with 45% win rate, profit factor >1 (target >1.5) + g_autoOptParams.sl_atr_mult = 1.1; // was 1.3 + g_autoOptParams.tp_atr_mult = 4.5; // was 3.5 + // [pair table owns] g_autoOptParams.min_rr = 1.8; // was 1.3 (way too loose -- allowed R:R=0.33 trades) + // * v9.14 FIX#32 note: For Major M15: 3.5/1.3 x 0.85 = 2.29 + g_autoOptParams.risk_pct = EA_RiskPercent; // * v7.9: was 1.0 (ignored user input!) + g_autoOptParams.max_daily_trades = 5; + // * v9.14 FIX#32: Raised quality thresholds for Major M15 + g_autoOptParams.min_confluence = 0.60; // was 0.55 -- too permissive + // [pair table owns] g_autoOptParams.min_entry_quality = 65.0; // was 60.0 -- allowed 48% WR signals + // * v9.34 FIX#133: Per-category MinEV -- Major Forex tight spreads, small EV viable. + // Old 0.25R rejected Score=100 A_PLUS setups (log: 40+ rejections/day on GBPUSD). + // 0.08R: only reject genuinely negative/zero EV setups. + g_autoOptParams.smart_min_ev = 0.08; + } + else if(category == "Metal") + { + g_autoOptParams.sl_atr_mult = 1.5; // * v7.5b: 2.0->1.5 (tighter SL for Gold M5) + g_autoOptParams.tp_atr_mult = 2.5; // * v7.5b: 4.0->2.5 (TP1 hit at R:R 1.67, not 2.0+) + // [pair table owns] g_autoOptParams.min_rr = 1.5; // * v7.5b: 2.0->1.5 (data shows wins at 1.8R) + g_autoOptParams.risk_pct = EA_RiskPercent; // * v8.05 FIX: removed 80% reduction. Gold volatility handled by SL width, not risk cut + g_autoOptParams.max_daily_trades = 5; // * v7.5b: 3->5 (more trades with tighter SL) + g_autoOptParams.min_confluence = 0.60; // * v7.5b: 0.65->0.60 (slightly more signals) + // [pair table owns] g_autoOptParams.min_entry_quality = 60.0; // * v7.5b: 65->60 + // * v9.34 FIX#133: Metal (XAUUSD/XAGUSD) MinEV -- wider spread than Major but big moves. + // 0.10R provides meaningful filter while allowing quality setups through. + // M15 XAUUSD ATR ~$80-150; EV of 0.10R = $8-15 expected profit per $100 risk. + g_autoOptParams.smart_min_ev = 0.10; + } + else if(category == "Index") + { + g_autoOptParams.sl_atr_mult = 1.2; // * v7.6: 2.5->1.2 (matches pair profile fix) + g_autoOptParams.tp_atr_mult = 2.5; // * v7.6: 5.0->2.5 + // [pair table owns] g_autoOptParams.min_rr = 2.0; // * v7.6: 1.5->2.0 (require proper R:R) + g_autoOptParams.risk_pct = EA_RiskPercent; // * v8.05 FIX: removed 70% reduction. EA_RiskPercent is the user's decision; index volatility is handled by wider SL sizing, not risk reduction + g_autoOptParams.max_daily_trades = 2000; // * v7.6b: No hard limit -- quality filter (score>=70) handles frequency naturally + g_autoOptParams.min_confluence = 0.65; // * v7.6: 0.50->0.65 (raise quality bar) + // [pair table owns] g_autoOptParams.min_entry_quality = 70.0; // * v7.6: 55->70 CRITICAL FIX + // * v7.6: SmartEntry thresholds for indices -- RAISED to prevent negative EV trades + // * v8.08 BUG#2 FIX: Floor at EA_MinEntryScore. AutoOpt NEVER weakens user inputs. + g_autoOptParams.smart_min_confidence = (int)EA_MinEntryScore; // * v9.16 FIX#44: SINGLE SOURCE + g_autoOptParams.smart_min_win_prob = SmartEntry_MinWinProb; // * v9.03: Use user input directly (was hardcoded floor 52% -> blocked user's 42% setting) + g_autoOptParams.smart_min_ev = 0.15; // * v9.34 FIX#133: 0.25->0.15 (indices are volatile; 0.25 blocked too many A+ setups; 0.15 provides meaningful EV floor) + g_autoOptParams.allow_scalping = false; // * v7.6: false (spread kills scalping on indices) + } + else if(category == "VolatileCross") + { + g_autoOptParams.sl_atr_mult = 2.0; + g_autoOptParams.tp_atr_mult = 3.5; + // [pair table owns] g_autoOptParams.min_rr = 2.0; + g_autoOptParams.risk_pct = EA_RiskPercent; // * v8.05 FIX: removed 60% reduction + g_autoOptParams.max_daily_trades = 3; + g_autoOptParams.min_confluence = 0.65; + // [pair table owns] g_autoOptParams.min_entry_quality = 65.0; + // * v9.34 FIX#133: VolatileCross (GBPJPY, EURJPY etc.) -- higher spread, need modest EV floor + g_autoOptParams.smart_min_ev = 0.10; + } + else if(category == "Exotic") + { + g_autoOptParams.sl_atr_mult = 2.5; + g_autoOptParams.tp_atr_mult = 4.0; + // [pair table owns] g_autoOptParams.min_rr = 2.0; + g_autoOptParams.risk_pct = EA_RiskPercent * 0.70; // * v8.05: exotics retain 30% safety margin (liquidity risk; was 40% over-penalised) + g_autoOptParams.max_daily_trades = 2; + g_autoOptParams.min_confluence = 0.70; + // [pair table owns] g_autoOptParams.min_entry_quality = 70.0; + // * v9.34 FIX#133: Exotics have wide spread + slippage; 0.15R ensures cost-compensated EV + g_autoOptParams.smart_min_ev = 0.15; + } + else if(category == "Crypto") + { + g_autoOptParams.sl_atr_mult = 2.5; + g_autoOptParams.tp_atr_mult = 5.0; + // [pair table owns] g_autoOptParams.min_rr = 2.0; + g_autoOptParams.risk_pct = EA_RiskPercent * 0.70; // * v8.05: crypto retains 30% safety margin (gap/liquidation risk; was 50% over-penalised) + g_autoOptParams.max_daily_trades = 3; + g_autoOptParams.min_confluence = 0.60; + // [pair table owns] g_autoOptParams.min_entry_quality = 60.0; + // * v9.34 FIX#133: Crypto extreme volatility -- 0.15R compensates gap risk and funding + g_autoOptParams.smart_min_ev = 0.15; + } + else if(category == "Energy") + { + g_autoOptParams.sl_atr_mult = 2.0; + g_autoOptParams.tp_atr_mult = 4.0; + // [pair table owns] g_autoOptParams.min_rr = 2.0; + g_autoOptParams.risk_pct = EA_RiskPercent; // * v8.05 FIX: removed 65% reduction + g_autoOptParams.max_daily_trades = 3; + g_autoOptParams.min_confluence = 0.65; + // [pair table owns] g_autoOptParams.min_entry_quality = 65.0; + // * v9.34 FIX#133: Energy (Oil, NatGas) moderate spread; 0.12R meaningful without over-filtering + g_autoOptParams.smart_min_ev = 0.12; + } + else // Cross / Unknown + { + g_autoOptParams.sl_atr_mult = 1.5; + g_autoOptParams.tp_atr_mult = 2.5; + // [pair table owns] g_autoOptParams.min_rr = 1.5; + g_autoOptParams.risk_pct = EA_RiskPercent; // * v8.05 FIX: removed 75% reduction for unknown pairs + g_autoOptParams.max_daily_trades = 4; + g_autoOptParams.min_confluence = 0.55; + // [pair table owns] g_autoOptParams.min_entry_quality = 58.0; + // * v9.34 FIX#133: Cross / Unknown -- treat like Major (tight spreads for most crosses) + g_autoOptParams.smart_min_ev = 0.08; + } + // Default strategy enables + g_autoOptParams.allow_liq_grab = true; + g_autoOptParams.allow_bos_retest = true; + g_autoOptParams.allow_ote = true; + g_autoOptParams.allow_scalping = true; + g_autoOptParams.allow_swing = true; + } + // * v7.9 FIX: REMOVED old override block that conflicted with the new EA_RiskPercent-based logic. + // Previously: "For Major/Cross only, use EA_RiskPercent" -> all other categories got hardcoded 0.5% + // Now: ALL categories use EA_RiskPercent as their base (with category-specific safety fractions) + // The volatility/spread multipliers in Steps 3-4 then scale proportionally. + g_autoOptParams.max_daily_trades = EA_MaxDailyTrades; + // Common defaults + g_autoOptParams.allow_fvg_entry = true; + g_autoOptParams.allow_ob_entry = true; + g_autoOptParams.allow_breaker_entry = true; + // FVG defaults -- start from inputs (TF adjustments override in Step 2) + double pointValue = _Point; + double atr_in_points = (g_marketSnap.current_atr > 0) ? g_marketSnap.current_atr / pointValue : 100; + g_autoOptParams.fvg_min_size = MathMax(2.0, atr_in_points * 0.05); + g_autoOptParams.fvg_max_age = FVG_MaxAge; // * v9.03 FIX#12: was hardcoded 100 + g_autoOptParams.fvg_extend_bars = FVG_ExtendBars; // * v9.03 FIX#12: was hardcoded 50 + // OB defaults -- start from inputs + g_autoOptParams.ob_volume_mult = OB_VolumeMultiplier; // * v9.03 FIX#12: was hardcoded 1.5 + g_autoOptParams.ob_max_age = OB_MaxAge; // * v9.03 FIX#12: was hardcoded 150 + // Structure/Liquidity defaults -- start from inputs + g_autoOptParams.struct_swing_strength = STRUCT_SwingStrength; // * v9.03 FIX#12: was hardcoded 3 + g_autoOptParams.liq_swing_strength = LIQ_SwingStrength; // * v9.03 FIX#12: was hardcoded 5 + g_autoOptParams.liq_max_age = LIQ_MaxAge; // * v9.03 FIX#12: was hardcoded 200 + // Entry score default + g_autoOptParams.min_entry_score = (int)EA_MinEntryScore; // * v9.16 FIX#44: SINGLE SOURCE -- user input only, no AutoOpt manipulation + // Filters + g_autoOptParams.rsi_overbought = 70; + g_autoOptParams.rsi_oversold = 30; + g_autoOptParams.max_spread_pips = 10.0; // * v9.31 FIX#103a: default 10 pips for unknown pairs (30pts=3pips generic fallback, now 200pts=20pips safe default for unknown pairs) + g_autoOptParams.atr_min_value = 0; + g_autoOptParams.atr_max_value = 0; + // Smart entry defaults + // * v8.08 BUG#2/#5 FIX: AutoOpt defaults must start at or above user input thresholds. + // Old defaults (58, 49%) were BELOW user inputs (65, 52%) -> AutoOpt weakened user protections. + g_autoOptParams.smart_min_confidence = (int)EA_MinEntryScore; // * v9.16 FIX#44: SINGLE SOURCE -- no AutoOpt manipulation + g_autoOptParams.smart_min_win_prob = SmartEntry_MinWinProb; // * v9.03: Use user input directly (was hardcoded 52% floor -> overrode user's SmartEntry_MinWinProb=42%) + // * v9.34 FIX#133: Common MinEV default -- 0.08R. + // Per-category values set in SetBasePairParameters (Major=0.08, Metal=0.10, Index=0.15, etc.) + // override this. This default catches any unrecognised category. + // Old 0.25R: log analysis showed 40+ A/A+ setups rejected/day on GBPUSD M15. + // Root cause: EV formula yields 0.08-0.18R on most M15 setups (WinP~55-58%); + // 0.25R is only achievable in strong trend conditions, not ranging/choppy markets. + g_autoOptParams.smart_min_ev = 0.08; // * v9.34 FIX#133: 0.25->0.08 (per-category override above takes precedence) + // Win Probability weights + // * FIX#463: Use user inputs as base instead of hardcoded values. + // BEFORE: 0.20/0.15/0.15/0.20/0.15/0.15 regardless of WinProb_* inputs. + // FIX: init from inputs — ApplyAutoOptToWorkingParams will override with g_autoOptParams values. + g_autoOptParams.wp_trend_weight = WinProb_TrendWeight; + g_autoOptParams.wp_structure_weight = WinProb_StructureWeight; + g_autoOptParams.wp_zone_weight = WinProb_ZoneWeight; + g_autoOptParams.wp_confluence_weight = WinProb_ConfluenceWeight; + g_autoOptParams.wp_timing_weight = WinProb_TimingWeight; + g_autoOptParams.wp_pattern_weight = WinProb_PatternWeight; + g_autoOptParams.wp_min_threshold = WinProb_MinThreshold; + // Position sizing + g_autoOptParams.pos_trending_bonus = 1.2; + g_autoOptParams.pos_ranging_penalty = 0.8; + // * v9.03: Position management defaults (start from input values) + g_autoOptParams.breakeven_rr = EA_BreakEven_RR; + // v9.16 REMOVED: g_autoOptParams.trail_start_rr = EA_TrailStart_RR; + // v9.16 REMOVED: g_autoOptParams.trail_stop_atr = EA_TrailStop_ATR; + // * FIX#309b: if per-TF se_minrr_override active, use it as base — not global input + g_autoOptParams.smart_exit_min_rr = (g_autoOptParams.se_minrr_override > 0) + ? g_autoOptParams.se_minrr_override + : EA_SmartExit_MinProfit_RR; + g_autoOptParams.smart_exit_signals = EA_SmartExit_Signals; + // v9.16 REMOVED: g_autoOptParams.trail_volatile_start = EA_Trail_Volatile_Start; + // v9.16 REMOVED: g_autoOptParams.trail_volatile_dist = EA_Trail_Volatile_Dist; + // v9.16 REMOVED: g_autoOptParams.trail_trending_start = EA_Trail_Trending_Start; + // v9.16 REMOVED: g_autoOptParams.trail_trending_dist = EA_Trail_Trending_Dist; + // * v9.03 FIX#11: Detection param defaults (start from input values) + g_autoOptParams.ote_max_age = OTE_MaxAge; + g_autoOptParams.bb_max_age = BB_MaxAge; + g_autoOptParams.mb_max_age = MB_MaxAge; + g_autoOptParams.trendline_max_age = Trendline_MaxAge; + g_autoOptParams.crt_lookback = CRT_LookbackBars; + g_autoOptParams.crt_expiry = CRT_ExpiryBars; + g_autoOptParams.tbs_expiry = TBS_ExpiryBars; + g_autoOptParams.amd_accum_max_bars = AMD_AccumMaxBars; + g_autoOptParams.sb_max_age = SB_MaxAge; + g_autoOptParams.signal_expiry_bars = SignalExpiryBars; + g_autoOptParams.fvg_min_strength = FVG_MinStrength; + g_autoOptParams.regime_lookback = Regime_Lookback; + g_autoOptParams.regime_confirm_bars = Regime_ConfirmBars; + g_autoOptParams.divergence_lookback = Divergence_Lookback; + g_autoOptParams.trendline_lookback = Trendline_Lookback; + g_autoOptParams.fib_lookback = FIB_LookbackBars; + // Session defaults + g_autoOptParams.use_session_filter = true; + g_autoOptParams.session_london = true; + g_autoOptParams.session_ny = true; + g_autoOptParams.session_asian = true; + // * v9.24 FIX#82 STEP1: Pair-specific BASE values for new params + // TC RSI: start from user inputs (TF-adjust in Step2) + g_autoOptParams.tc_rsi_min = TC_RSI_Min; + g_autoOptParams.tc_rsi_max = TC_RSI_Max; + g_autoOptParams.tc_min_slope_atr = TC_MinSlopeATR; + g_autoOptParams.tc_base_score = TC_BaseScore; + // TP R:Rs: start from user inputs (TF-adjust in Step2, pair-adjust at end of Step2) + g_autoOptParams.tp1_rr = InpTP1_RR; + g_autoOptParams.tp2_rr = InpTP2_RR; + g_autoOptParams.tp3_rr = InpTP3_RR; + // Judas / TBS TP R:Rs: start from user inputs + g_autoOptParams.judas_tp1_rr = Judas_TP1_RR; + g_autoOptParams.judas_tp2_rr = Judas_TP2_RR; + g_autoOptParams.judas_tp3_rr = Judas_TP3_RR; + g_autoOptParams.tbs_tp1_rr = TBS_TP1_RR; + g_autoOptParams.tbs_tp2_rr = TBS_TP2_RR; + g_autoOptParams.tbs_tp3_rr = TBS_TP3_RR; + // CRT ranges: start from user inputs + g_autoOptParams.crt_min_range_atr = CRT_MinRangeATR; + g_autoOptParams.crt_max_range_atr = CRT_MaxRangeATR; + // Regime thresholds: start from user inputs + g_autoOptParams.regime_trend_threshold = Regime_TrendThreshold; + g_autoOptParams.regime_trend_adx_min = Regime_TrendADXMin; + // MTF confidence: pair-specific base (Step2.5 refines per pair category) + g_autoOptParams.mtf_min_confidence = MTF_MinConfidence; + // VSA strength: start from user input (Step2.5 refines per pair category) + g_autoOptParams.vsa_min_strength = VSA_MinStrength; +} +//+------------------------------------------------------------------+ +//| Step 2: Adjust for timeframe | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| Step 2: Adjust for timeframe - CLEANED VERSION | +//+------------------------------------------------------------------+ +void ApplyTimeframeAdjustments() +{ + // * v9.15 FIX#36: Complete TF-aware scaling for ALL relevant inputs + // Previously: only ~15 params were adjusted. Now ALL numeric inputs are scaled + // using TF category multipliers derived from how many M5-equivalent bars fit per TF. + // M5=1x (base), M15=3x, M30=6x, H1=12x, H4=48x, D1=288x + // Scaling philosophy: age/lookback bars grow with TF (more bars needed to see structure) + // quality thresholds tighten (fewer but better trades on higher TF) + // risk and frequency adapt to TF noise level + ENUM_TF_CATEGORY tfCat = g_marketSnap.tf_category; + // -- TF multiplier for bar-count inputs (ages, lookbacks, expiries) -- + // M5=1.0, M15=3.0, H1=12.0, H4=48.0 (proportional to candle duration vs M5 base) + int tfMinutes = PeriodSeconds() / 60; + double tfBarMult = MathMax(1.0, tfMinutes / 5.0); // relative to M5 base + switch(tfCat) + { + // ============================================================ + // M1-M5: SCALP + // ============================================================ + case TF_CAT_SCALP: + // SL/TP/RR + g_autoOptParams.sl_atr_mult *= 0.85; + g_autoOptParams.tp_atr_mult *= 0.80; + // [pair table owns] g_autoOptParams.min_rr = MathMax(1.2, g_autoOptParams.min_rr * 0.90); + // Quality + // [pair table owns] g_autoOptParams.min_entry_quality += 10; + g_autoOptParams.min_confluence *= 1.15; + // * v9.16 FIX#44: REMOVED -- EA_MinEntryScore is single source of truth + // Risk / frequency + g_autoOptParams.risk_pct *= 0.60; + g_autoOptParams.max_daily_trades = MathMin(12, g_autoOptParams.max_daily_trades * 2); + // -- Bar-count inputs: short because M5 candles are plentiful -- + // * FIX#458: Use inputs as base with TF floor (was fully hardcoded, ignoring user inputs) + g_autoOptParams.fvg_max_age = MathMax(30, (int)(FVG_MaxAge * 0.10)); + g_autoOptParams.fvg_extend_bars = 15; + g_autoOptParams.ob_max_age = MathMax(30, (int)(OB_MaxAge * 0.33)); + g_autoOptParams.liq_max_age = MathMax(40, (int)(LIQ_MaxAge * 0.67)); + g_autoOptParams.struct_swing_strength = 2; + g_autoOptParams.liq_swing_strength = 3; + g_autoOptParams.ote_max_age = MathMax(25, (int)(OTE_MaxAge * 0.20)); + g_autoOptParams.bb_max_age = MathMax(20, (int)(BB_MaxAge * 0.25)); + g_autoOptParams.mb_max_age = MathMax(15, (int)(MB_MaxAge * 0.25)); + g_autoOptParams.trendline_max_age = 40; + g_autoOptParams.crt_lookback = 20; + g_autoOptParams.crt_expiry = 12; + g_autoOptParams.tbs_expiry = 8; + g_autoOptParams.amd_accum_max_bars = 24; + g_autoOptParams.sb_max_age = 15; + g_autoOptParams.signal_expiry_bars = 12; + g_autoOptParams.regime_lookback = 20; + g_autoOptParams.regime_confirm_bars = 2; + g_autoOptParams.divergence_lookback = 15; + g_autoOptParams.trendline_lookback = 20; // SCALP default — pair table overrides per pair if needed (FIX#311) + g_autoOptParams.fib_lookback = 50; + // -- FVG/Pattern quality: accept weaker (more frequent on M5) -- + g_autoOptParams.fvg_min_strength = 0.30; + // -- Position management: tight -- scalp profits vanish fast -- + // * v9.24 FIX#81 BUG-A: SCALP breakeven was hardcoded 1.2, ignoring user input. + // Rule: AutoOpt uses MathMax(user_floor, TF_minimum) -- NEVER below user input. + // SCALP minimum = 1.2R (fastest profit protection), user can set higher. + // * v9.30 FIX#101: DYNAMIC BE/SE from tp1_rr (replaces hardcoded MathMax floors) + // Old: MathMax(user, X.X) -> hardcoded for M15, wrong for all other TFs + // New: BE = tp1_rr x 0.50, SE = tp1_rr x 0.40 + // Works for ALL pairs/TFs automatically -- no manual tuning needed + // Per-trade system (FIX#100) refines further at trade open + g_autoOptParams.breakeven_rr = MathMax(0.50, g_autoOptParams.tp1_rr * 0.65); // * v9.31 FIX#121C: BE 0.50->0.65 | * v9.49 FIX#200: floor 0.35→0.50R + g_autoOptParams.smart_exit_min_rr = MathMax(0.50, g_autoOptParams.tp1_rr * 0.55); // * v9.31 FIX#121B: SE min 0.40->0.55 | * v9.49 FIX#200: floor 0.30→0.50R + g_autoOptParams.smart_exit_signals = 3; + // g_autoOptParams.trail_volatile_start = 1.8; // REMOVED + // g_autoOptParams.trail_volatile_dist = 0.6; // REMOVED + // g_autoOptParams.trail_trending_start = 1.3; // REMOVED + // g_autoOptParams.trail_trending_dist = 0.8; // REMOVED + // -- Indicator-specific inputs for M5 -- + g_autoOptParams.regime_adr_period = 14; + g_autoOptParams.tc_ema_fast = TC_EMA_Fast; // keep as-is (21) + g_autoOptParams.tc_ema_slow = TC_EMA_Slow; // keep as-is (50) + g_autoOptParams.tc_pullback_bars = 3; + g_autoOptParams.vp_period = MathMax(20, (int)(VP_Period / tfBarMult)); + g_autoOptParams.mm_lookback = MathMax(10, (int)(MM_LookbackPeriod / tfBarMult)); + g_autoOptParams.pd_lookback = MathMax(30, (int)(PD_LookbackBars / tfBarMult)); + g_autoOptParams.winkprob_lookback = WinProb_LookbackTrades; // base value + g_autoOptParams.judas_sl_atr = Judas_SL_ATR * 1.0; // * FIX#461: use input as base (×1.0 for Scalp) + g_autoOptParams.tbs_min_sweep_atr = TBS_MinSweepATR; + g_autoOptParams.tbs_max_sweep_atr = TBS_MaxSweepATR; + g_autoOptParams.tbs_confirmation_bars = 2; // * v9.16 FIX#48: SCALP = fast confirm + g_autoOptParams.corr_update_mins = 15; // * v9.16 FIX#48: SCALP = frequent updates + g_autoOptParams.amd_manip_move_atr = AMD_ManipMoveATR; + g_autoOptParams.amd_dist_min_move = AMD_DistMinMove; + g_autoOptParams.news_mins_before_high = News_MinsBeforeHigh; + g_autoOptParams.news_mins_after_high = News_MinsAfterHigh; + // =========================================================== + // * v9.16 FIX#52: PAIR-AWARE SCALP SL/TP FLOORS + // Problem: Generic SCALP tightening (x0.85 SL, x0.80 TP) is BACKWARDS + // for Major/Cross pairs on M1/M5. + // Evidence: EURUSD M5 SL=1.105xATR=3.9p -> spread/SL=31% -> unviable + // EURUSD M1 SL=1.105xATR=1.7p -> spread/SL=72% -> impossible + // Root cause: Low-ATR pairs need WIDER SL on short TFs to absorb spread. + // High-ATR pairs (Gold, GBP) are fine with tighter SL (spread is tiny vs ATR). + // Fix: Apply SL/TP FLOORS per pair category. Floors override tightening + // when the generic multiplier would make spread cost too high. + // =========================================================== + { + string scalp_cat = g_autoOptParams.pair_category; + if(scalp_cat == "Major") + { + if(_Period <= PERIOD_M1) + { + // M1 Majors: ATR ~1.5p, spread ~0.3-1.2p + // Need SL floor 2.5xATR=3.75p -> spread/SL=8-32% + // Only viable in London/NY (spread ~0.3p -> 8%) + g_autoOptParams.sl_atr_mult = MathMax(g_autoOptParams.sl_atr_mult, 2.5); + g_autoOptParams.tp_atr_mult = MathMax(g_autoOptParams.tp_atr_mult, 5.0); + // [pair table owns] g_autoOptParams.min_rr = MathMax(1.3, g_autoOptParams.min_rr); + // [pair table owns] g_autoOptParams.min_entry_quality += 5; // Extra strict: only best setups + g_autoOptParams.risk_pct *= 0.50; // Half of scalp risk (0.30% total) + g_autoOptParams.max_daily_trades = MathMin(5, g_autoOptParams.max_daily_trades); + // Faster position management -- M1 profits vanish in seconds + // * v9.30 FIX#101: Dynamic BE/SE from tp1_rr (set below at SCALP FIX#82) + // v9.16 REMOVED: g_autoOptParams.trail_start_rr = 1.0; + // v9.16 REMOVED: g_autoOptParams.trail_stop_atr = 0.5; + // v9.16 REMOVED: g_autoOptParams.trail_volatile_start = 1.2; + // v9.16 REMOVED: g_autoOptParams.trail_volatile_dist = 0.4; + // v9.16 REMOVED: g_autoOptParams.trail_trending_start = 0.8; + // v9.16 REMOVED: g_autoOptParams.trail_trending_dist = 0.5; + } + else // M2-M5 + { + // M5 Majors: ATR ~3.5p, spread ~0.5-1.2p + // Need SL floor 1.8xATR=6.3p -> spread/SL=8-19% + g_autoOptParams.sl_atr_mult = MathMax(g_autoOptParams.sl_atr_mult, 1.8); + g_autoOptParams.tp_atr_mult = MathMax(g_autoOptParams.tp_atr_mult, 3.8); + } + } + else if(scalp_cat == "Cross") + { + if(_Period <= PERIOD_M1) + { + g_autoOptParams.sl_atr_mult = MathMax(g_autoOptParams.sl_atr_mult, 2.2); + g_autoOptParams.tp_atr_mult = MathMax(g_autoOptParams.tp_atr_mult, 4.5); + g_autoOptParams.risk_pct *= 0.50; + g_autoOptParams.max_daily_trades = MathMin(5, g_autoOptParams.max_daily_trades); + // * v9.30 FIX#101: Dynamic BE/SE from tp1_rr (set at SCALP FIX#82) + // v9.16 REMOVED: g_autoOptParams.trail_start_rr = 1.0; + // v9.16 REMOVED: g_autoOptParams.trail_stop_atr = 0.5; + } + else // M2-M5 + { + g_autoOptParams.sl_atr_mult = MathMax(g_autoOptParams.sl_atr_mult, 1.6); + g_autoOptParams.tp_atr_mult = MathMax(g_autoOptParams.tp_atr_mult, 3.5); + } + } + // Metal/Index/VolatileCross: high ATR -> generic SCALP tightening is fine + if(g_verboseLog) + PrintFormat(" * FIX#52 SCALP %s %s: SL=%.2fxATR TP=%.2fxATR -> R:R=%.2f | Risk=%.2f%%", + scalp_cat, EnumToString(_Period), + g_autoOptParams.sl_atr_mult, g_autoOptParams.tp_atr_mult, + g_autoOptParams.tp_atr_mult / g_autoOptParams.sl_atr_mult, + g_autoOptParams.risk_pct); + } + // -- Dynamic scalp viability check -- + { + double scalp_sl_dist = g_marketSnap.current_atr * g_autoOptParams.sl_atr_mult; + if(scalp_sl_dist <= 0) + g_autoOptParams.allow_scalping = true; + else + { + double spread_price = g_marketSnap.current_spread * _Point; + g_autoOptParams.allow_scalping = (spread_price / scalp_sl_dist <= 0.22); + } + } + // * v9.24 FIX#82 TF-scaling: SCALP + g_autoOptParams.tc_rsi_min = MathMax(TC_RSI_Min, 48.0); // Tight: only strong momentum + g_autoOptParams.tc_rsi_max = MathMin(TC_RSI_Max, 52.0); + g_autoOptParams.tc_min_slope_atr = MathMax(TC_MinSlopeATR, 0.08); // Steeper: filter noise + g_autoOptParams.tc_base_score = MathMin((double)TC_BaseScore, 22.0); + g_autoOptParams.tp1_rr = MathMax(InpTP1_RR * 0.75, 1.5); + g_autoOptParams.tp2_rr = MathMax(InpTP2_RR * 0.75, 2.0); + g_autoOptParams.tp3_rr = MathMax(InpTP3_RR * 0.75, 2.5); + g_autoOptParams.judas_tp1_rr = MathMax(Judas_TP1_RR * 0.80, 1.2); + g_autoOptParams.judas_tp2_rr = MathMax(Judas_TP2_RR * 0.80, 1.8); + g_autoOptParams.judas_tp3_rr = MathMax(Judas_TP3_RR * 0.80, 2.5); + g_autoOptParams.tbs_tp1_rr = MathMax(TBS_TP1_RR * 0.80, 0.8); + g_autoOptParams.tbs_tp2_rr = MathMax(TBS_TP2_RR * 0.80, 1.5); + g_autoOptParams.tbs_tp3_rr = MathMax(TBS_TP3_RR * 0.80, 2.0); + g_autoOptParams.crt_min_range_atr = 0.5; + g_autoOptParams.crt_max_range_atr = 2.0; + g_autoOptParams.regime_trend_threshold = MathMax(Regime_TrendThreshold, 55.0); // Need confirmed trend + g_autoOptParams.regime_trend_adx_min = MathMax(Regime_TrendADXMin, 28.0); + // * v9.30 FIX#101: Dynamic BE/SE thresholds from tp1_rr + // Works for ALL pairs and ALL TFs -- no manual tuning needed + // * v9.31 FIX#121C/B: BE fires at 65% of TP1 distance, SE checks from 55% + // Per-trade calc (FIX#100) refines further using actual SL/TP at open + g_autoOptParams.breakeven_rr = MathMax(0.50, g_autoOptParams.tp1_rr * 0.65); // * v9.31 FIX#121C: BE 0.50->0.65 | * v9.49 FIX#200: floor 0.35→0.50R + g_autoOptParams.smart_exit_min_rr = MathMax(0.50, g_autoOptParams.tp1_rr * 0.55); // * v9.31 FIX#121B: SE min 0.40->0.55 | * v9.49 FIX#200: floor 0.30→0.50R + // * v9.26 FIX#94a: SCALP TF position sizing scaling + // Pair base (from ApplyPairDetectionAdjustments) x TF modifier + // SCALP trend: reduce bonus (short TF = less room to run, spread cost bites more) + // * v10.23 FIX#309a: SCALP range penalty removed (was ×0.75 → total 0.60 = -40%). + // Root cause: M5 EURUSD backtest showed regime almost always classified RANGING/CHOPPY + // → 0.60 penalty cancelled all win streak gains (streak +15% × ranging -40% = net -29%). + // M5 ranging protection now comes from score_cap=78 + req_kz + req_trend (pair table). + // Keeping the lot multiplier flat (×1.0) lets win streak and compounding work correctly. + // SCALP range: was ×0.75 → now ×1.00 (no extra SCALP penalty on top of category 0.80) + g_autoOptParams.pos_trending_bonus = MathMin(1.50, g_autoOptParams.pos_trending_bonus * 0.90); // SCALP: -10% trend bonus (unchanged) + g_autoOptParams.pos_ranging_penalty = g_autoOptParams.pos_ranging_penalty; // * FIX#309a/311: per-pair override applied in ApplyPairTFProfile (step 10) + break; + // ============================================================ + // M15-M30: INTRADAY + // ============================================================ + case TF_CAT_INTRADAY: + // * v9.24 FIX#81b: M15 and M30 differentiation. + // Old: both got IDENTICAL settings (no TF scaling in INTRADAY case). + // M15 is primary TF -> base values stay as-is (from SetBasePairParameters). + // M30 sits between M15 and H1 -> slightly wider SL/TP, later BE. + if(_Period == PERIOD_M30) + { + g_autoOptParams.sl_atr_mult *= 1.05; // M30: 5% wider SL than M15 + g_autoOptParams.tp_atr_mult *= 1.08; // M30: 8% wider TP (extra room for daily range) + // [pair table owns] g_autoOptParams.min_rr = MathMax(g_autoOptParams.min_rr, 1.4); // M30: min 1.4R + // * v9.30 FIX#101: M30 dynamic BE/SE set below (after tp1_rr scaled x1.05) + // Bar counts: M30 = M15 * 1.5 (proportional to candle duration) + // (M15 values are set below -- M30 overrides specific counts) + } + // M15: no scaling -- these ARE the optimized base values from pair profile (FIX#30/32) + // Quality: slightly tighter than scalp + // [pair table owns] g_autoOptParams.min_entry_quality += 5; + g_autoOptParams.min_confluence *= 1.05; + // Risk / frequency + // * v9.16 FIX#53d: REMOVED x0.85 risk reduction. User's EA_RiskPercent is intentional. + // M15 is the PRIMARY timeframe -- no reason to cut risk. Volatility/spread adjustments + // handle dynamic risk scaling elsewhere (ApplyPairDetectionAdjustments). + g_autoOptParams.max_daily_trades = MathMin(EA_MaxDailyTrades, g_autoOptParams.max_daily_trades); + // -- Bar-count inputs scaled x3 vs M5 -- + g_autoOptParams.fvg_max_age = (int)MathMin(FVG_MaxAge * 3, 180); + g_autoOptParams.fvg_extend_bars = (int)MathMin(FVG_ExtendBars * 3, 90); + g_autoOptParams.ob_max_age = (int)MathMin(OB_MaxAge * 3, 240); + g_autoOptParams.liq_max_age = (int)MathMin(LIQ_MaxAge * 3, 150); + g_autoOptParams.struct_swing_strength = MathMax(3, (int)(STRUCT_SwingStrength)); + g_autoOptParams.liq_swing_strength = MathMax(5, (int)(LIQ_SwingStrength)); + g_autoOptParams.ote_max_age = (int)MathMin(OTE_MaxAge * 3, 300); + g_autoOptParams.bb_max_age = (int)MathMin(BB_MaxAge * 3, 300); + g_autoOptParams.mb_max_age = (int)MathMin(MB_MaxAge * 3, 240); + g_autoOptParams.trendline_max_age = (int)MathMin(Trendline_MaxAge * 3, 300); + g_autoOptParams.crt_lookback = (int)MathMin(CRT_LookbackBars * 3, 150); + g_autoOptParams.crt_expiry = (int)MathMin(CRT_ExpiryBars * 3, 90); + g_autoOptParams.tbs_expiry = (int)MathMin(TBS_ExpiryBars * 3, 45); + g_autoOptParams.amd_accum_max_bars = (int)MathMin(AMD_AccumMaxBars * 3, 150); + g_autoOptParams.sb_max_age = (int)MathMin(SB_MaxAge * 3, 90); + g_autoOptParams.signal_expiry_bars = (int)MathMin(SignalExpiryBars * 3, 72); + g_autoOptParams.regime_lookback = (int)MathMin(Regime_Lookback * 3, 54); + g_autoOptParams.regime_confirm_bars = Regime_ConfirmBars; + g_autoOptParams.divergence_lookback = (int)MathMin(Divergence_Lookback * 3, 60); + g_autoOptParams.trendline_lookback = (int)MathMin(Trendline_Lookback * 3, 90); + g_autoOptParams.fib_lookback = (int)MathMin(FIB_LookbackBars * 3, 300); + g_autoOptParams.fvg_min_strength = MathMax(FVG_MinStrength, 0.35); + // -- Position management: balanced -- + // * v9.24 FIX#81 BUG-B: M15 had no TF floor for breakeven_rr. + // If user set EA_BreakEven_RR=1.0, BE would trigger at 1.0R on M15 (too tight!). + // M15 INTRADAY minimum = 1.2R (same as SCALP floor, but via MathMax so user wins if higher). + // * v9.30 FIX#101: INTRADAY dynamic BE/SE set after tp1_rr (below at FIX#82) + // v9.16 REMOVED: g_autoOptParams.trail_start_rr = EA_TrailStart_RR; + // v9.16 REMOVED: g_autoOptParams.trail_stop_atr = EA_TrailStop_ATR; + g_autoOptParams.smart_exit_signals = EA_SmartExit_Signals; + // v9.16 REMOVED: g_autoOptParams.trail_volatile_start = EA_Trail_Volatile_Start; + // v9.16 REMOVED: g_autoOptParams.trail_volatile_dist = EA_Trail_Volatile_Dist; + // v9.16 REMOVED: g_autoOptParams.trail_trending_start = EA_Trail_Trending_Start; + // v9.16 REMOVED: g_autoOptParams.trail_trending_dist = EA_Trail_Trending_Dist; + // -- Indicator inputs scaled for M15 -- + g_autoOptParams.regime_adr_period = 20; // * M15: standard 20-day ADR + g_autoOptParams.tc_ema_fast = TC_EMA_Fast; + g_autoOptParams.tc_ema_slow = TC_EMA_Slow; + g_autoOptParams.tc_pullback_bars = TC_PullbackBars; + g_autoOptParams.vp_period = VP_Period; // use input directly + g_autoOptParams.mm_lookback = MM_LookbackPeriod; + g_autoOptParams.pd_lookback = PD_LookbackBars; + g_autoOptParams.winkprob_lookback = WinProb_LookbackTrades; + g_autoOptParams.judas_sl_atr = Judas_SL_ATR * 1.2; // * FIX#461 + g_autoOptParams.tbs_min_sweep_atr = TBS_MinSweepATR; + g_autoOptParams.tbs_max_sweep_atr = TBS_MaxSweepATR * 1.2; + g_autoOptParams.tbs_confirmation_bars = 3; // * v9.16 FIX#48: INTRADAY = standard + g_autoOptParams.corr_update_mins = 30; // * v9.16 FIX#48: INTRADAY + g_autoOptParams.amd_manip_move_atr = AMD_ManipMoveATR * 1.2; + g_autoOptParams.amd_dist_min_move = AMD_DistMinMove * 1.2; + g_autoOptParams.news_mins_before_high = (int)(News_MinsBeforeHigh * 1.5); + g_autoOptParams.news_mins_after_high = (int)(News_MinsAfterHigh * 1.5); + g_autoOptParams.allow_scalping = true; // M15 allows scalping + g_autoOptParams.allow_swing = true; + // * v9.24 FIX#82 TF-scaling: INTRADAY M15/M30 + if(_Period == PERIOD_M30) + { + // M30: between M15 (base) and H1 -- mild scaling + g_autoOptParams.tc_rsi_min = MathMin(TC_RSI_Min, 44.0); + g_autoOptParams.tc_rsi_max = MathMax(TC_RSI_Max, 56.0); + g_autoOptParams.tc_min_slope_atr = MathMin(TC_MinSlopeATR, 0.045); + g_autoOptParams.tc_base_score = MathMax((double)TC_BaseScore, 26.0); + g_autoOptParams.tp1_rr = InpTP1_RR * 1.05; + g_autoOptParams.tp2_rr = InpTP2_RR * 1.08; + g_autoOptParams.tp3_rr = InpTP3_RR * 1.10; + // * v9.30 FIX#101: M30 dynamic BE/SE from tp1_rr + g_autoOptParams.breakeven_rr = MathMax(0.50, g_autoOptParams.tp1_rr * 0.65); // * v9.31 FIX#121C: BE 0.50->0.65 | * v9.49 FIX#200: floor 0.35→0.50R + g_autoOptParams.smart_exit_min_rr = MathMax(0.50, g_autoOptParams.tp1_rr * 0.55); // * v9.31 FIX#121B: SE min 0.40->0.55 | * v9.49 FIX#200: floor 0.30→0.50R + g_autoOptParams.judas_tp1_rr = Judas_TP1_RR * 1.10; + g_autoOptParams.judas_tp2_rr = Judas_TP2_RR * 1.10; + g_autoOptParams.judas_tp3_rr = Judas_TP3_RR * 1.10; + g_autoOptParams.tbs_tp1_rr = TBS_TP1_RR * 1.10; + g_autoOptParams.tbs_tp2_rr = TBS_TP2_RR * 1.10; + g_autoOptParams.tbs_tp3_rr = TBS_TP3_RR * 1.10; + g_autoOptParams.crt_min_range_atr = 0.75; + g_autoOptParams.crt_max_range_atr = 3.5; + g_autoOptParams.regime_trend_threshold = MathMin(Regime_TrendThreshold, 46.0); + g_autoOptParams.regime_trend_adx_min = MathMin(Regime_TrendADXMin, 23.0); + } + else // M15: base -- user inputs are the optimised primary TF values + { + g_autoOptParams.tc_rsi_min = TC_RSI_Min; + g_autoOptParams.tc_rsi_max = TC_RSI_Max; + g_autoOptParams.tc_min_slope_atr = TC_MinSlopeATR; + g_autoOptParams.tc_base_score = TC_BaseScore; + g_autoOptParams.tp1_rr = InpTP1_RR; + // * v9.30 FIX#101: Dynamic BE/SE thresholds from tp1_rr + // * v9.36 FIX#157: M15 BE floor raised 0.35->1.80R (Excel M15 BE=1.8R) + // Old: MathMax(0.35, tp1_rr*0.65) = MathMax(0.35, 1.30) = 1.30R -- trades closed too early + // New: MathMax(1.80, tp1_rr*0.65) = 1.80R -- matches Excel optimal setting + g_autoOptParams.breakeven_rr = MathMax(1.80, g_autoOptParams.tp1_rr * 0.65); // * v9.36 FIX#157: floor 0.35->1.80R + g_autoOptParams.smart_exit_min_rr = MathMax(0.50, g_autoOptParams.tp1_rr * 0.55); // * v9.31 FIX#121B: SE min 0.40->0.55 | * v9.49 FIX#200: floor 0.30→0.50R + g_autoOptParams.tp2_rr = InpTP2_RR; + g_autoOptParams.tp3_rr = InpTP3_RR; + g_autoOptParams.judas_tp1_rr = Judas_TP1_RR; + g_autoOptParams.judas_tp2_rr = Judas_TP2_RR; + g_autoOptParams.judas_tp3_rr = Judas_TP3_RR; + g_autoOptParams.tbs_tp1_rr = TBS_TP1_RR; + g_autoOptParams.tbs_tp2_rr = TBS_TP2_RR; + g_autoOptParams.tbs_tp3_rr = TBS_TP3_RR; + g_autoOptParams.crt_min_range_atr = CRT_MinRangeATR; + g_autoOptParams.crt_max_range_atr = CRT_MaxRangeATR; + g_autoOptParams.regime_trend_threshold = Regime_TrendThreshold; + g_autoOptParams.regime_trend_adx_min = Regime_TrendADXMin; + } + // * v9.26 FIX#94b: INTRADAY TF position sizing scaling + // M15: pair base is the reference -- no scaling (SetBasePairParameters was tuned for M15) + // M30: slightly better trend bonus (more noise filtered vs M15) + if(_Period == PERIOD_M30) + { + g_autoOptParams.pos_trending_bonus = MathMin(1.50, g_autoOptParams.pos_trending_bonus * 1.05); + g_autoOptParams.pos_ranging_penalty = MathMax(0.40, g_autoOptParams.pos_ranging_penalty * 0.95); + } + // M15: no change -- base pair values are the primary reference + break; + // ============================================================ + // * v9.16 FIX#53e: H1 INTRASWING -- DEDICATED section + // Was falling through to H4 SWING with identical settings. + // H1 is between M15 and H4: tighter than H4, wider than M15. + // ============================================================ + case TF_CAT_INTRASWING: + // SL/TP/RR -- between M15 (x1.0) and H4 (x1.20/1.30) + g_autoOptParams.sl_atr_mult *= 1.10; + g_autoOptParams.tp_atr_mult *= 1.15; + // [pair table owns] g_autoOptParams.min_rr = MathMax(1.5, g_autoOptParams.min_rr * 1.05); + // Quality: between M15 and H4 + // [pair table owns] g_autoOptParams.min_entry_quality += 6; + g_autoOptParams.min_confluence *= 1.08; + // Risk / frequency -- more trades than H4, fewer than M15 + // * v9.49 FIX#206: floor 3→4 — GBPUSD profile=4, 4*2/3=2 → MathMax(3,2)=3 (wrong). + // H1 should allow as many trades as the pair profile specifies. 4 trades/day is correct + // for pairs with max_daily_trades=4. The 2/3 reducer comes from "H1 has fewer setups than M15" + // but for H4-level pairs, that logic over-reduces. Floor=4 ensures pair profile is respected. + g_autoOptParams.max_daily_trades = MathMax(4, g_autoOptParams.max_daily_trades * 2 / 3); + // -- Bar-count inputs x6 vs M5 (between M15's x3 and H4's x12) -- + g_autoOptParams.fvg_max_age = (int)MathMin(FVG_MaxAge * 6, 360); + g_autoOptParams.fvg_extend_bars = (int)MathMin(FVG_ExtendBars* 6, 180); + g_autoOptParams.ob_max_age = (int)MathMin(OB_MaxAge * 6, 480); + g_autoOptParams.liq_max_age = (int)MathMin(LIQ_MaxAge * 6, 300); + g_autoOptParams.struct_swing_strength = MathMax(4, STRUCT_SwingStrength + 1); + g_autoOptParams.liq_swing_strength = MathMax(6, LIQ_SwingStrength + 1); + g_autoOptParams.ote_max_age = (int)MathMin(OTE_MaxAge * 6, 600); + g_autoOptParams.bb_max_age = (int)MathMin(BB_MaxAge * 6, 600); + g_autoOptParams.mb_max_age = (int)MathMin(MB_MaxAge * 6, 480); + g_autoOptParams.trendline_max_age = (int)MathMin(Trendline_MaxAge * 6, 600); + g_autoOptParams.crt_lookback = (int)MathMin(CRT_LookbackBars * 3, 150); + g_autoOptParams.crt_expiry = (int)MathMin(CRT_ExpiryBars * 3, 90); + g_autoOptParams.tbs_expiry = (int)MathMin(TBS_ExpiryBars * 3, 45); + g_autoOptParams.amd_accum_max_bars = (int)MathMin(AMD_AccumMaxBars * 3, 150); + g_autoOptParams.sb_max_age = (int)MathMin(SB_MaxAge * 3, 90); + g_autoOptParams.signal_expiry_bars = (int)MathMin(SignalExpiryBars * 3, 72); + g_autoOptParams.regime_lookback = (int)MathMin(Regime_Lookback * 3, 54); + g_autoOptParams.regime_confirm_bars = MathMin(4, Regime_ConfirmBars + 1); + g_autoOptParams.divergence_lookback = (int)MathMin(Divergence_Lookback * 3, 60); + g_autoOptParams.trendline_lookback = (int)MathMin(Trendline_Lookback * 3, 90); + g_autoOptParams.fib_lookback = (int)MathMin(FIB_LookbackBars * 3, 300); + g_autoOptParams.fvg_min_strength = MathMax(FVG_MinStrength, 0.40); + // -- Position management: between M15 and H4 -- + // * v9.30 FIX#101: H1 dynamic BE/SE set after tp1_rr (below at FIX#82) + // v9.16 REMOVED: g_autoOptParams.trail_start_rr = MathMax(EA_TrailStart_RR, 2.2); + // v9.16 REMOVED: g_autoOptParams.trail_stop_atr = MathMax(EA_TrailStop_ATR, 1.1); + g_autoOptParams.smart_exit_signals = MathMax(EA_SmartExit_Signals, 3); + // v9.16 REMOVED: g_autoOptParams.trail_volatile_start = MathMax(EA_Trail_Volatile_Start, 2.5); + // v9.16 REMOVED: g_autoOptParams.trail_volatile_dist = MathMax(EA_Trail_Volatile_Dist, 0.9); + // v9.16 REMOVED: g_autoOptParams.trail_trending_start = MathMax(EA_Trail_Trending_Start, 2.0); + // v9.16 REMOVED: g_autoOptParams.trail_trending_dist = MathMax(EA_Trail_Trending_Dist, 1.2); + // -- Indicator inputs for H1 -- + g_autoOptParams.regime_adr_period = 20; // H1: 20-day ADR (more data than H4) + g_autoOptParams.tc_ema_fast = MathMin(26, TC_EMA_Fast + 5); + g_autoOptParams.tc_ema_slow = MathMin(65, TC_EMA_Slow + 15); + g_autoOptParams.tc_pullback_bars = MathMin(6, TC_PullbackBars + 1); + g_autoOptParams.vp_period = MathMin(150, VP_Period + 50); + g_autoOptParams.mm_lookback = MathMin(40, MM_LookbackPeriod * 2); + g_autoOptParams.pd_lookback = MathMin(200, PD_LookbackBars * 2); + g_autoOptParams.winkprob_lookback = MathMin(400, WinProb_LookbackTrades + 100); + g_autoOptParams.judas_sl_atr = Judas_SL_ATR * 1.3; // * FIX#461 + g_autoOptParams.tbs_min_sweep_atr = TBS_MinSweepATR * 1.3; + g_autoOptParams.tbs_max_sweep_atr = TBS_MaxSweepATR * 1.5; + g_autoOptParams.tbs_confirmation_bars = 3; + g_autoOptParams.corr_update_mins = 45; + g_autoOptParams.amd_manip_move_atr = AMD_ManipMoveATR * 1.3; + g_autoOptParams.amd_dist_min_move = AMD_DistMinMove * 1.3; + g_autoOptParams.news_mins_before_high = (int)(News_MinsBeforeHigh * 1.5); + g_autoOptParams.news_mins_after_high = (int)(News_MinsAfterHigh * 1.5); + g_autoOptParams.allow_scalping = false; // No scalp on H1 + g_autoOptParams.allow_swing = true; + // * v9.24 FIX#82 TF-scaling: INTRASWING H1 + g_autoOptParams.tc_rsi_min = MathMin(TC_RSI_Min, 42.0); + g_autoOptParams.tc_rsi_max = MathMax(TC_RSI_Max, 58.0); + g_autoOptParams.tc_min_slope_atr = MathMin(TC_MinSlopeATR, 0.04); + g_autoOptParams.tc_base_score = MathMax((double)TC_BaseScore, 28.0); + // * v9.49 FIX#208: H1 TP multipliers raised 1.20/1.25/1.30 → 1.35/1.45/1.60. + // ROOT CAUSE: H1 pair profile already sets tp1=3.20xATR (FIX#199). AutoOpt was then + // further scaling tp1_rr with only x1.20 → final tp1_rr barely above pair profile. + // With new 25/25/50 TP distribution (FIX#200), TP3 runner needs more room. + // x1.35/1.45/1.60 aligns with FIX#199 ratios and the higher runner allocation. + g_autoOptParams.tp1_rr = InpTP1_RR * 1.35; // * FIX#208: 1.20→1.35 + g_autoOptParams.tp2_rr = InpTP2_RR * 1.45; // * FIX#208: 1.25→1.45 + g_autoOptParams.tp3_rr = InpTP3_RR * 1.60; // * FIX#208: 1.30→1.60 (runner gets most room) + g_autoOptParams.judas_tp1_rr = Judas_TP1_RR * 1.35; + g_autoOptParams.judas_tp2_rr = Judas_TP2_RR * 1.45; + g_autoOptParams.judas_tp3_rr = Judas_TP3_RR * 1.40; + g_autoOptParams.tbs_tp1_rr = TBS_TP1_RR * 1.35; + g_autoOptParams.tbs_tp2_rr = TBS_TP2_RR * 1.45; + g_autoOptParams.tbs_tp3_rr = TBS_TP3_RR * 1.60; + g_autoOptParams.crt_min_range_atr = 0.70; + g_autoOptParams.crt_max_range_atr = 4.0; + g_autoOptParams.regime_trend_threshold = MathMin(Regime_TrendThreshold, 45.0); + g_autoOptParams.regime_trend_adx_min = MathMin(Regime_TrendADXMin, 22.0); + // * v9.30 FIX#101: Dynamic BE/SE thresholds from tp1_rr + // Works for ALL pairs and ALL TFs -- no manual tuning needed + // * v9.31 FIX#121C/B: BE fires at 65% of TP1 distance, SE checks from 55% + // Per-trade calc (FIX#100) refines further using actual SL/TP at open + // * v9.49 FIX#200: H1 BE floor raised 0.35→0.55R (was moving BE at 0.35R = too early for H1 momentum). + // SE floor raised 0.30→0.50R (was closing at 0.30R = avg win $46 < avg loss $49 → W/L ratio 0.94). + // MathMax ensures these are absolute minimums — per-trade FIX#100 further raises from actual TP1/SL ratio. + g_autoOptParams.breakeven_rr = MathMax(0.55, g_autoOptParams.tp1_rr * 0.65); // * FIX#200: floor 0.35→0.55R (H1 needs more room before BE) + g_autoOptParams.smart_exit_min_rr = MathMax(0.50, g_autoOptParams.tp1_rr * 0.55); // * FIX#200: floor 0.30→0.50R (SmartExit min matches new input default) + // * v9.26 FIX#94c: INTRASWING (H1) position sizing scaling + // H1: trends more reliable than M15, but ranging still valid (ICT OB/FVG at H1 = strong) + // Moderate bonus boost: H1 trending = larger ATR moves, worth more lots + // Ranging penalty milder vs scalp: H1 range = defined OB zone, still tradeable + g_autoOptParams.pos_trending_bonus = MathMin(1.50, g_autoOptParams.pos_trending_bonus * 1.10); // H1: +10% trend bonus + g_autoOptParams.pos_ranging_penalty = MathMax(0.40, g_autoOptParams.pos_ranging_penalty * 0.90); // H1: -10% range penalty + break; + // ============================================================ + // H4: SWING + // ============================================================ + case TF_CAT_SWING: + // SL/TP/RR -- wider, fewer but higher quality trades + g_autoOptParams.sl_atr_mult *= 1.20; + g_autoOptParams.tp_atr_mult *= 1.30; + // * v9.40 FIX#174: Remove *1.10 multiplier for min_rr on H4. + // FIX#172 already returns TF-aware value from GetPairTFMinRR (1.80 for H4). + // *1.10 = 1.98 was double-dipping: FIX#19 then clamped to achievable (1.68-1.95). + // Evidence: all 7 trades had adjMinRR=1.80 (FIX#19 clamped from 1.98 each bar). + // Fix: MathMax(1.5, min_rr) -- keep H4 floor but no TF multiplier on top. + // [pair table owns] g_autoOptParams.min_rr = MathMax(1.5, g_autoOptParams.min_rr); + // Quality: higher bar -- * v9.25 FIX#88: pair-aware (Major/Cross H4 gets +4 instead of +8) + // Old: flat +8 raised Major threshold to 73, blocking too many valid H4 setups (only 13 trades in 2 months). + // Fix: Major/Cross get +4 (still higher bar vs lower TFs), volatile pairs keep +8. + { + string swCat = g_autoOptParams.pair_category; + // [pair table owns] min_entry_quality quality boost removed — pair table sets min_conf directly + } + g_autoOptParams.min_confluence *= 1.10; + // * v9.16 FIX#44: REMOVED -- EA_MinEntryScore is single source of truth + // Risk / frequency + // * v9.36 FIX#150: comment fix (was "H1" -- this IS the H4 case) + // * v9.49 FIX#207: Remove /2 divisor — GBPUSD profile=4, 4/2=2→MathMax(2,2)=2 (was halving all pairs). + // H4 has fewer setups by nature (fewer bars, wider ATR-based filters) — the pair profile already + // encodes the correct max_daily_trades for H4. Dividing further just blocks valid setups. + // Keep MathMax(2,...) as absolute floor (H4 should still allow at least 2 trades/day). + g_autoOptParams.risk_pct *= 1.0; // full risk on H4 (higher quality -- pair profile sets exact %) + g_autoOptParams.max_daily_trades = MathMax(2, g_autoOptParams.max_daily_trades); + // -- Bar-count inputs scaled x12 vs M5 -- + g_autoOptParams.fvg_max_age = (int)MathMin(FVG_MaxAge * 12, 120); // H4: 20 trading days (was 720=4months) + g_autoOptParams.fvg_extend_bars = (int)MathMin(FVG_ExtendBars* 12, 360); + g_autoOptParams.ob_max_age = (int)MathMin(OB_MaxAge * 12, 120); // H4: 20 trading days (was 960) + g_autoOptParams.liq_max_age = (int)MathMin(LIQ_MaxAge * 12, 90); // H4: 15 trading days (was 600) + g_autoOptParams.struct_swing_strength = MathMax(6, STRUCT_SwingStrength + 2); // * v9.36 FIX#150: min 5->6 (Excel H4=6) + g_autoOptParams.liq_swing_strength = MathMax(7, LIQ_SwingStrength + 2); + g_autoOptParams.ote_max_age = (int)MathMin(OTE_MaxAge * 12, 1200); + g_autoOptParams.bb_max_age = (int)MathMin(BB_MaxAge * 12, 1200); + g_autoOptParams.mb_max_age = (int)MathMin(MB_MaxAge * 12, 960); + g_autoOptParams.trendline_max_age = (int)MathMin(Trendline_MaxAge * 12, 1200); + g_autoOptParams.crt_lookback = (int)MathMin(CRT_LookbackBars * 4, 200); + g_autoOptParams.crt_expiry = (int)MathMin(CRT_ExpiryBars * 4, 120); + g_autoOptParams.tbs_expiry = (int)MathMin(TBS_ExpiryBars * 4, 60); + g_autoOptParams.amd_accum_max_bars = (int)MathMin(AMD_AccumMaxBars * 4, 200); + g_autoOptParams.sb_max_age = (int)MathMin(SB_MaxAge * 4, 120); + g_autoOptParams.signal_expiry_bars = (int)MathMin(SignalExpiryBars * 4, 96); + g_autoOptParams.regime_lookback = 30; // * v9.36 FIX#150: x4 formula=72 -> fixed 30 (Excel H4=30, x4 overshoots) + g_autoOptParams.regime_confirm_bars = MathMin(6, Regime_ConfirmBars + 2); + g_autoOptParams.divergence_lookback = (int)MathMin(Divergence_Lookback * 4, 80); + g_autoOptParams.trendline_lookback = (int)MathMin(Trendline_Lookback * 4, 120); + g_autoOptParams.fib_lookback = (int)MathMin(FIB_LookbackBars * 4, 400); + g_autoOptParams.fvg_min_strength = MathMax(FVG_MinStrength, 0.40); // * v9.36 FIX#150: 0.45->0.40 (Excel H4=0.40, 0.45 blocked too many valid FVGs) + // * v9.36 FIX#150: H4 OB requires stronger volume confirmation (Excel H4 OB_VolumeMultiplier=1.6) + g_autoOptParams.ob_volume_mult = MathMax(g_autoOptParams.ob_volume_mult, 1.6); + // -- Position management: wider for H1 swings -- + // * v9.29 FIX#98c: SWING (H4) AutoOpt overrides corrected + // OLD: MathMax(user,1.8) and MathMax(user,1.3) -> ALWAYS overrode to 1.8/1.3R regardless of input + // With H4 TP1=2.5xATR=55p and SL=41p -> TP1=1.34R, these thresholds were past TP1 -> never fired + // NEW: MathMin so AutoOpt never RAISES above the user input (which is now correctly 0.7R) + // * v9.30 FIX#101: H4 dynamic BE/SE set after tp1_rr (below at FIX#82) + // v9.16 REMOVED: g_autoOptParams.trail_start_rr = MathMax(EA_TrailStart_RR, 2.5); + // v9.16 REMOVED: g_autoOptParams.trail_stop_atr = MathMax(EA_TrailStop_ATR, 1.2); + g_autoOptParams.smart_exit_signals = MathMax(EA_SmartExit_Signals, 3); + // v9.16 REMOVED: g_autoOptParams.trail_volatile_start = MathMax(EA_Trail_Volatile_Start, 2.8); + // v9.16 REMOVED: g_autoOptParams.trail_volatile_dist = MathMax(EA_Trail_Volatile_Dist, 1.0); + // v9.16 REMOVED: g_autoOptParams.trail_trending_start = MathMax(EA_Trail_Trending_Start, 2.2); + // v9.16 REMOVED: g_autoOptParams.trail_trending_dist = MathMax(EA_Trail_Trending_Dist, 1.3); + // -- Indicator inputs for H1 -- + g_autoOptParams.regime_adr_period = 14; // * H1/H4: shorter ADR (14 trading days) + g_autoOptParams.tc_ema_fast = MathMin(34, TC_EMA_Fast + 13); + g_autoOptParams.tc_ema_slow = MathMin(89, TC_EMA_Slow + 39); + g_autoOptParams.tc_pullback_bars = MathMin(8, TC_PullbackBars + 2); + g_autoOptParams.vp_period = MathMin(200, VP_Period * 2); + g_autoOptParams.mm_lookback = MathMin(60, MM_LookbackPeriod * 3); + g_autoOptParams.pd_lookback = MathMin(300, PD_LookbackBars * 3); + g_autoOptParams.winkprob_lookback = MathMin(500, WinProb_LookbackTrades * 2); + g_autoOptParams.judas_sl_atr = Judas_SL_ATR * 1.5; // * FIX#461 + g_autoOptParams.tbs_min_sweep_atr = TBS_MinSweepATR * 1.5; + g_autoOptParams.tbs_max_sweep_atr = TBS_MaxSweepATR * 2.0; + g_autoOptParams.tbs_confirmation_bars = 4; // * v9.16 FIX#48: SWING = more confirm + g_autoOptParams.corr_update_mins = 60; // * v9.16 FIX#48: SWING = standard + g_autoOptParams.amd_manip_move_atr = AMD_ManipMoveATR * 1.5; + g_autoOptParams.amd_dist_min_move = AMD_DistMinMove * 1.5; + g_autoOptParams.news_mins_before_high = (int)(News_MinsBeforeHigh * 2.0); + g_autoOptParams.news_mins_after_high = (int)(News_MinsAfterHigh * 2.0); + g_autoOptParams.allow_scalping = false; // No scalp on H1 + g_autoOptParams.allow_swing = true; + // * v9.24 FIX#82 TF-scaling: SWING H4 + g_autoOptParams.tc_rsi_min = MathMin(TC_RSI_Min, 42.0); // * v9.36 FIX#150: 40.0->42.0 (Excel H4 TC_RSI_Min=42) + g_autoOptParams.tc_rsi_max = MathMax(TC_RSI_Max, 58.0); // * v9.36 FIX#150: 60.0->58.0 (Excel H4 TC_RSI_Max=58) + g_autoOptParams.tc_min_slope_atr = MathMin(TC_MinSlopeATR, 0.03); + g_autoOptParams.tc_base_score = MathMax((double)TC_BaseScore, 30.0); + g_autoOptParams.tp1_rr = InpTP1_RR * 1.40; + g_autoOptParams.tp2_rr = InpTP2_RR * 1.50; + g_autoOptParams.tp3_rr = InpTP3_RR * 1.60; + g_autoOptParams.judas_tp1_rr = Judas_TP1_RR * 1.50; + g_autoOptParams.judas_tp2_rr = Judas_TP2_RR * 1.55; + g_autoOptParams.judas_tp3_rr = Judas_TP3_RR * 1.50; + g_autoOptParams.tbs_tp1_rr = TBS_TP1_RR * 1.50; + g_autoOptParams.tbs_tp2_rr = TBS_TP2_RR * 1.50; + g_autoOptParams.tbs_tp3_rr = TBS_TP3_RR * 1.50; + g_autoOptParams.crt_min_range_atr = 0.60; + g_autoOptParams.crt_max_range_atr = 5.0; + g_autoOptParams.regime_trend_threshold = MathMin(Regime_TrendThreshold, 42.0); + g_autoOptParams.regime_trend_adx_min = MathMin(Regime_TrendADXMin, 20.0); + // * v9.30 FIX#101: Dynamic BE/SE thresholds from tp1_rr + // Works for ALL pairs and ALL TFs -- no manual tuning needed + // * v9.31 FIX#121C/B: BE fires at 65% of TP1 distance, SE checks from 55% + // Per-trade calc (FIX#100) refines further using actual SL/TP at open + // * v10.08 FIX#277: H4 BE floor raised 0.35→0.50R. + // Old comment said "per-trade FIX#100 dominates at 1.17R+". True — but the floor + // is the FALLBACK when perTrade lookup fails (new position, first tick, MTP array miss). + // 0.35R floor on H4 means any failed lookup fires BE at 0.35R = noise territory. + // 0.50R is consistent with every other TF floor and safe for H4 (TP1≈1.8-2.0R). + g_autoOptParams.breakeven_rr = MathMax(0.50, g_autoOptParams.tp1_rr * 0.65); // * FIX#277: floor 0.35→0.50R (H4 fallback BE, consistent with all other TF floors) + g_autoOptParams.smart_exit_min_rr = MathMax(0.50, g_autoOptParams.tp1_rr * 0.55); // * FIX#200: floor 0.30→0.50R (prevent early SmartExit below 0.50R on H4) + // * v9.26 FIX#94d: SWING (H4) position sizing scaling + // H4 trending = highest quality ICT setups (OB + HTF aligned + killzone = ideal) + // H4 ranging = reduced size but still valid (H4 range = accumulation/distribution zone) + // Pair base already has correct characteristic values -- H4 applies a meaningful boost + g_autoOptParams.pos_trending_bonus = MathMin(1.50, g_autoOptParams.pos_trending_bonus * 1.20); // H4: +20% on pair base (XAUUSD: 1.40x1.20=1.50 cap, EURUSD: 1.25x1.20=1.50) + g_autoOptParams.pos_ranging_penalty = MathMax(0.40, g_autoOptParams.pos_ranging_penalty * 0.95); // H4: mild range softening (H4 OBs still valid in range) + break; + // ============================================================ + // H4-D1: POSITION + // ============================================================ + case TF_CAT_POSITION: + default: + g_autoOptParams.sl_atr_mult *= 1.50; + g_autoOptParams.tp_atr_mult *= 1.70; + // [pair table owns] g_autoOptParams.min_rr = MathMax(2.0, g_autoOptParams.min_rr * 1.25); + // [pair table owns] g_autoOptParams.min_entry_quality += 12; + g_autoOptParams.min_confluence *= 1.20; + // * v9.16 FIX#44: REMOVED -- EA_MinEntryScore is single source of truth + g_autoOptParams.risk_pct *= 1.0; + g_autoOptParams.max_daily_trades = MathMax(1, g_autoOptParams.max_daily_trades / 3); + // -- Bar-count inputs: very wide, H4=48xM5 -- + g_autoOptParams.fvg_max_age = (int)MathMin(FVG_MaxAge * (int)tfBarMult, 90); // D1: 90 trading days ~1 quarter (was 2000=8yrs) + g_autoOptParams.fvg_extend_bars = (int)MathMin(FVG_ExtendBars* (int)tfBarMult, 1000); + g_autoOptParams.ob_max_age = (int)MathMin(OB_MaxAge * (int)tfBarMult, 90); // D1: 90 trading days (was 2000) + g_autoOptParams.liq_max_age = (int)MathMin(LIQ_MaxAge * (int)tfBarMult, 60); // D1: 60 trading days ~1 quarter (was 2000) + g_autoOptParams.struct_swing_strength = MathMax(7, STRUCT_SwingStrength + 4); + g_autoOptParams.liq_swing_strength = MathMax(10,LIQ_SwingStrength + 4); + g_autoOptParams.ote_max_age = (int)MathMin(OTE_MaxAge * (int)tfBarMult, 3000); + g_autoOptParams.bb_max_age = (int)MathMin(BB_MaxAge * (int)tfBarMult, 3000); + g_autoOptParams.mb_max_age = (int)MathMin(MB_MaxAge * (int)tfBarMult, 3000); + g_autoOptParams.trendline_max_age = (int)MathMin(Trendline_MaxAge *(int)tfBarMult, 3000); + g_autoOptParams.crt_lookback = (int)MathMin(CRT_LookbackBars * 8, 400); + g_autoOptParams.crt_expiry = (int)MathMin(CRT_ExpiryBars * 8, 240); + g_autoOptParams.tbs_expiry = (int)MathMin(TBS_ExpiryBars * 8, 120); + g_autoOptParams.amd_accum_max_bars = (int)MathMin(AMD_AccumMaxBars * 8, 400); + g_autoOptParams.sb_max_age = (int)MathMin(SB_MaxAge * 8, 240); + g_autoOptParams.signal_expiry_bars = (int)MathMin(SignalExpiryBars * 8, 192); + g_autoOptParams.regime_lookback = (int)MathMin(Regime_Lookback * 6, 108); + g_autoOptParams.regime_confirm_bars = MathMin(10,Regime_ConfirmBars + 5); + g_autoOptParams.divergence_lookback = (int)MathMin(Divergence_Lookback * 6, 120); + g_autoOptParams.trendline_lookback = (int)MathMin(Trendline_Lookback * 6, 180); + g_autoOptParams.fib_lookback = (int)MathMin(FIB_LookbackBars * 6, 600); + g_autoOptParams.fvg_min_strength = MathMax(FVG_MinStrength, 0.55); + // -- Position management: very wide -- + // * v9.30 FIX#101: D1 dynamic BE/SE set after tp1_rr (below at FIX#82) + // v9.16 REMOVED: g_autoOptParams.trail_start_rr = MathMax(EA_TrailStart_RR, 3.0); + // v9.16 REMOVED: g_autoOptParams.trail_stop_atr = MathMax(EA_TrailStop_ATR, 1.8); + g_autoOptParams.smart_exit_signals = MathMax(EA_SmartExit_Signals, 4); + // v9.16 REMOVED: g_autoOptParams.trail_volatile_start = MathMax(EA_Trail_Volatile_Start, 3.5); + // v9.16 REMOVED: g_autoOptParams.trail_volatile_dist = MathMax(EA_Trail_Volatile_Dist, 1.5); + // v9.16 REMOVED: g_autoOptParams.trail_trending_start = MathMax(EA_Trail_Trending_Start, 3.0); + // v9.16 REMOVED: g_autoOptParams.trail_trending_dist = MathMax(EA_Trail_Trending_Dist, 1.8); + // -- Indicator inputs: position-grade -- + g_autoOptParams.regime_adr_period = 10; // * D1+: shorter ADR (10 trading days ~= 2 weeks) + g_autoOptParams.tc_ema_fast = MathMin(55, TC_EMA_Fast + 34); + g_autoOptParams.tc_ema_slow = MathMin(200,TC_EMA_Slow + 150); + g_autoOptParams.tc_pullback_bars = MathMin(15, TC_PullbackBars + 7); + g_autoOptParams.vp_period = MathMin(500, VP_Period * 5); + g_autoOptParams.mm_lookback = MathMin(100,MM_LookbackPeriod * 5); + g_autoOptParams.pd_lookback = MathMin(500,PD_LookbackBars * 5); + g_autoOptParams.winkprob_lookback = MathMin(1000,WinProb_LookbackTrades * 4); + g_autoOptParams.judas_sl_atr = Judas_SL_ATR * 2.0; // * FIX#461 + g_autoOptParams.tbs_min_sweep_atr = TBS_MinSweepATR * 2.5; + g_autoOptParams.tbs_max_sweep_atr = TBS_MaxSweepATR * 3.5; + g_autoOptParams.tbs_confirmation_bars = 2; // * v9.16 FIX#48: POSITION = daily candles significant + g_autoOptParams.corr_update_mins = 120; // * v9.16 FIX#48: POSITION = slow updates + g_autoOptParams.amd_manip_move_atr = AMD_ManipMoveATR * 2.0; + g_autoOptParams.amd_dist_min_move = AMD_DistMinMove * 2.0; + g_autoOptParams.news_mins_before_high = (int)(News_MinsBeforeHigh * 3.0); + g_autoOptParams.news_mins_after_high = (int)(News_MinsAfterHigh * 3.0); + g_autoOptParams.allow_scalping = false; + g_autoOptParams.allow_swing = true; + // * v9.24 FIX#82 TF-scaling: POSITION D1+ + g_autoOptParams.tc_rsi_min = MathMin(TC_RSI_Min, 38.0); + g_autoOptParams.tc_rsi_max = MathMax(TC_RSI_Max, 62.0); + g_autoOptParams.tc_min_slope_atr = MathMin(TC_MinSlopeATR, 0.02); + g_autoOptParams.tc_base_score = MathMax((double)TC_BaseScore, 32.0); + g_autoOptParams.tp1_rr = InpTP1_RR * 1.70; + g_autoOptParams.tp2_rr = InpTP2_RR * 1.80; + g_autoOptParams.tp3_rr = InpTP3_RR * 2.00; + g_autoOptParams.judas_tp1_rr = Judas_TP1_RR * 1.80; + g_autoOptParams.judas_tp2_rr = Judas_TP2_RR * 1.80; + g_autoOptParams.judas_tp3_rr = Judas_TP3_RR * 1.80; + g_autoOptParams.tbs_tp1_rr = TBS_TP1_RR * 1.80; + g_autoOptParams.tbs_tp2_rr = TBS_TP2_RR * 1.80; + g_autoOptParams.tbs_tp3_rr = TBS_TP3_RR * 1.80; + g_autoOptParams.crt_min_range_atr = 0.50; + g_autoOptParams.crt_max_range_atr = 6.0; + g_autoOptParams.regime_trend_threshold = MathMin(Regime_TrendThreshold, 40.0); + g_autoOptParams.regime_trend_adx_min = MathMin(Regime_TrendADXMin, 18.0); + // * v9.30 FIX#101: D1 dynamic BE/SE from tp1_rr + g_autoOptParams.breakeven_rr = MathMax(0.50, g_autoOptParams.tp1_rr * 0.65); // * v9.31 FIX#121C: BE 0.50->0.65 | * v9.49 FIX#200: floor 0.35→0.50R + g_autoOptParams.smart_exit_min_rr = MathMax(0.50, g_autoOptParams.tp1_rr * 0.55); // * v9.31 FIX#121B: SE min 0.40->0.55 | * v9.49 FIX#200: floor 0.30→0.50R + // * v9.26 FIX#94e: POSITION (D1+) position sizing scaling + // D1 trending = macro trend -- highest conviction BUT SL is very wide (200-500p on EURUSD) + // -> pair_category already provides the trending_bonus ceiling + // -> apply cap at 1.35 (not 1.50): wide SL means each lot = huge $ risk, cap conservatively + // D1 ranging = dangerous (wide range, wide SL) -- strongest penalty of all TFs + g_autoOptParams.pos_trending_bonus = MathMin(1.35, g_autoOptParams.pos_trending_bonus * 1.15); // D1: +15% but hard cap 1.35 (wide SL safety) + g_autoOptParams.pos_ranging_penalty = MathMax(0.35, g_autoOptParams.pos_ranging_penalty * 0.85); // D1: -15% extra (wide range SL = capital risk) + break; + } +} +void ApplyVolatilityAdjustments() +{ + double volRatio = g_marketSnap.volatility_ratio; + ENUM_VOL_REGIME regime = g_marketSnap.vol_regime; + switch(regime) + { + case VOL_VERY_LOW: + // Very quiet market - wider targets relative, smaller risk, require more confluence + g_autoOptParams.sl_atr_mult *= 1.3; // Wider SL since ATR is already small + g_autoOptParams.tp_atr_mult *= 1.5; // Wider TP to catch moves when they come + // * v9.24 FIX#81 BUG-C: Vol-aware BE/SmartExit thresholds + // VOL_VERY_LOW = clean, predictable moves -> BE can be slightly earlier (x0.92) + // Floor: never below pair/TF minimum already set in Step 2 + g_autoOptParams.breakeven_rr = MathMax(EA_BreakEven_RR, g_autoOptParams.breakeven_rr * 0.92); + g_autoOptParams.smart_exit_min_rr = MathMax( + g_autoOptParams.se_minrr_override > 0 ? g_autoOptParams.se_minrr_override : EA_SmartExit_MinProfit_RR, + g_autoOptParams.smart_exit_min_rr * 0.92); // FIX#309b: floor=override or global + // * v9.16 FIX#54: Pair-category-aware VOL risk scaling + // Problem: VOL_VERY_LOW x 0.6 designed for Metals/Indices (dead market = danger) + // But EURUSD is NATURALLY low-volatility -> hits VOL_VERY_LOW constantly + // Result: risk ALWAYS clamped to floor (1.0% -> 0.6% -> +spread(0.85) -> 0.51% -> floor 0.5%) + // Fix: Majors get x0.85 (mild reduction), others keep x0.6 (real danger signal) + // * v9.25 FIX#87: VOL_VERY_LOW risk exemption for H4+ Major pairs + // EURUSD H4 ATR 17-22 pips = normal market behaviour (low vol is the baseline for Majors). + // Old: Major VOL_VERY_LOW x 0.85 -> combined with Asian session = 0.85 x 0.76 = 0.65% -> clamped to 0.70%. + // Fix: H4+ (SWING/POSITION) Major/Cross skips the risk reduction since their ATR is naturally low. + // Intraday (M15/H1) still gets 0.85x as Asian low-vol IS unusual there. + { + string volCat = g_autoOptParams.pair_category; + bool isHighTF = (g_autoOptParams.tf_category == TF_CAT_SWING || g_autoOptParams.tf_category == TF_CAT_POSITION); + if(isHighTF && (volCat == "Major" || volCat == "Cross")) + g_autoOptParams.risk_pct *= 1.0; // H4+ Major/Cross: no penalty (low vol is normal) + else if(volCat == "Major") + g_autoOptParams.risk_pct *= 0.85; // M15/H1 Major: mild -- low vol unusual at these TFs + else if(volCat == "Cross") + g_autoOptParams.risk_pct *= 0.75; // Crosses: moderate + else + g_autoOptParams.risk_pct *= 0.6; // Metals/Indices/Energy: original (dead market = danger) + } + g_autoOptParams.min_confluence *= 1.15; // Need more reasons to enter + // * v9.16 FIX#44: REMOVED -- EA_MinEntryScore is single source of truth + // * v7.5b FIX: Don't unconditionally block scalping here! + // TF_CAT_SCALP (Step 2) already made a DYNAMIC spread/SL ratio check. + // VOL_VERY_LOW was overriding that smart decision every time. + // Instead, only raise quality bar for scalping -- let spread check decide viability. + if(g_autoOptParams.tf_category == TF_CAT_SCALP) + { // * v9.16 FIX#44: Score removed -- scalp in low vol allowed if spread OK + } + else + g_autoOptParams.allow_scalping = false; // Non-scalp TFs: don't scalp in dead market + break; + case VOL_LOW: + g_autoOptParams.sl_atr_mult *= 1.15; + g_autoOptParams.tp_atr_mult *= 1.25; + g_autoOptParams.risk_pct *= 0.8; + g_autoOptParams.min_confluence *= 1.05; + // * v9.24 FIX#82: VSA + WP threshold -- VOL_LOW adjustment + g_autoOptParams.vsa_min_strength = MathMax(35.0, g_autoOptParams.vsa_min_strength - 5.0); // Lower bar: detect signals in quiet market + g_autoOptParams.wp_min_threshold = MathMin(0.75, g_autoOptParams.wp_min_threshold + 0.02); // Raise bar: cleaner market = expect higher WP + break; + case VOL_NORMAL: + // Standard - no adjustments + break; + case VOL_HIGH: + g_autoOptParams.sl_atr_mult *= 1.1; // Slightly wider SL + g_autoOptParams.tp_atr_mult *= 1.2; // Bigger targets available + // * v9.24 FIX#81 BUG-C: High vol = large swings -> same RR threshold -> BE too early + // Price can retrace 0.3R easily before continuing -> need 1.2x wider BE/SmartExit + // Pair-aware: Majors/Crosses need less widening than Metals/Indices (smaller typical range) + { + double volHighMult = (g_autoOptParams.pair_category == "Major" || g_autoOptParams.pair_category == "Cross") + ? 1.12 : 1.20; + g_autoOptParams.breakeven_rr = MathMax(EA_BreakEven_RR, g_autoOptParams.breakeven_rr * volHighMult); + g_autoOptParams.smart_exit_min_rr = MathMax( + g_autoOptParams.se_minrr_override > 0 ? g_autoOptParams.se_minrr_override : EA_SmartExit_MinProfit_RR, + g_autoOptParams.smart_exit_min_rr * 1.08); // FIX#309b + } + // * v9.16 FIX#54: Pair-aware VOL_HIGH risk scaling + { + string volHCat = g_autoOptParams.pair_category; + if(volHCat == "Major" || volHCat == "Cross") + g_autoOptParams.risk_pct *= 0.85; // Majors/Crosses: mild reduction + else + g_autoOptParams.risk_pct *= 0.7; // Metals/Indices: original + } + g_autoOptParams.max_daily_trades = MathMax(1, g_autoOptParams.max_daily_trades - 1); + // * v9.16 FIX#44: REMOVED -- EA_MinEntryScore is single source of truth + // Favor trend-following in high vol + g_autoOptParams.wp_trend_weight += 0.05; + // * v9.24 FIX#82: VSA + WP threshold -- VOL_HIGH + g_autoOptParams.vsa_min_strength = MathMin(80.0, g_autoOptParams.vsa_min_strength + 5.0); + g_autoOptParams.wp_min_threshold = MathMax(0.40, g_autoOptParams.wp_min_threshold - 0.03); + break; + case VOL_EXTREME: + g_autoOptParams.sl_atr_mult *= 1.3; + g_autoOptParams.tp_atr_mult *= 1.5; + g_autoOptParams.risk_pct *= 0.4; // Much lower risk + // * v9.24 FIX#81 BUG-C: Extreme vol = massive swings, whipsaws + // Need much wider BE/SmartExit (x1.35) to avoid being stopped on normal retracements + // Extreme volatility widens swings but the BE threshold must remain reachable. + // Cap the multiplier so breakeven_rr stays within the typical peak RR on H1/H4. + g_autoOptParams.breakeven_rr = MathMax(EA_BreakEven_RR, g_autoOptParams.breakeven_rr * 1.10); + g_autoOptParams.smart_exit_min_rr = MathMax( + g_autoOptParams.se_minrr_override > 0 ? g_autoOptParams.se_minrr_override : EA_SmartExit_MinProfit_RR, + g_autoOptParams.smart_exit_min_rr * 1.20); // FIX#309b + g_autoOptParams.max_daily_trades = MathMax(1, g_autoOptParams.max_daily_trades / 2); + g_autoOptParams.min_confluence *= 1.2; + // * v9.16 FIX#44: REMOVED -- EA_MinEntryScore is single source of truth + g_autoOptParams.min_entry_quality += 10; + // * v9.35 FIX#137: VOL_EXTREME scalping block -- TF_CAT_SCALP (M5) exception. + // Old: allow_scalping=false for ALL TFs. Caused 5+ hours of zero trades on GBPUSD M5 + // because NY session ATR spike → VOL_EXTREME → blocked scalping permanently for that day. + // Root: ATR percentile uses 100-bar lookback. On M5, 100 bars = 8.3h. Post-NY spike, + // even low ATR bars still appear in top percentile for hours → VOL_EXTREME persists. + // Fix: For TF_CAT_SCALP (M5 is the SCALP timeframe), DON'T block scalping in extreme vol. + // Instead, add +15 more to min_entry_quality (total: +25 vs normal VOL_EXTREME +10). + // This ensures only truly elite setups trade during extreme vol, without dead zones. + // Non-scalp TFs (M15, H1, H4): keep existing allow_scalping=false behavior. + if(g_autoOptParams.tf_category == TF_CAT_SCALP) + g_autoOptParams.min_entry_quality += 15; // Additional quality gate: total +25 in extreme vol + else + g_autoOptParams.allow_scalping = false; // Non-scalp TFs: still block in extreme vol + break; + } +} +//+------------------------------------------------------------------+ +//| Step 4: Adjust for spread conditions | +//+------------------------------------------------------------------+ +void ApplySpreadAdjustments() +{ + double spreadRatio = g_marketSnap.spread_ratio; + if(spreadRatio > 3.0) + { + // Extreme spread - very restrictive + // * v9.16 FIX#44: REMOVED -- EA_MinEntryScore is single source of truth + g_autoOptParams.min_confluence *= 1.3; + g_autoOptParams.risk_pct *= 0.3; + g_autoOptParams.max_daily_trades = 1; + // * v9.35 FIX#137: spread_ratio > 3.0 on TF_CAT_SCALP -- raise quality bar instead of hard block. + // spread_ratio = current_spread / avg_spread. avg_spread biased to Asian session (very tight). + // During London/NY, any normal spread can produce ratio > 3.0 against Asian baseline. + // For M5 SCALP: allow_scalping=false means ZERO trades for hours. Use quality gate instead. + if(g_autoOptParams.tf_category == TF_CAT_SCALP) + g_autoOptParams.min_entry_quality += 15; // Hard quality gate: only elite setups in extreme spread + else + g_autoOptParams.allow_scalping = false; + } + else if(spreadRatio > 2.0) + { + // High spread + // * v9.16 FIX#44: REMOVED -- EA_MinEntryScore is single source of truth + g_autoOptParams.min_confluence *= 1.15; + g_autoOptParams.risk_pct *= 0.6; + // * v9.35 FIX#137: spread_ratio > 2.0 on TF_CAT_SCALP -- same reasoning as > 3.0 above. + // For non-scalp TFs: keep existing block (they have wider SL that spread eats more into). + if(g_autoOptParams.tf_category == TF_CAT_SCALP) + g_autoOptParams.min_entry_quality += 10; // Moderate quality gate for high spread + else + g_autoOptParams.allow_scalping = false; + } + else if(spreadRatio > 1.5) + { + // Above normal spread + // * v9.16 FIX#44: REMOVED -- EA_MinEntryScore is single source of truth + g_autoOptParams.risk_pct *= 0.85; + } + else if(spreadRatio < 0.7) + { + // Very tight spread - favorable + g_autoOptParams.risk_pct *= 1.1; + // * v9.16 FIX#44: REMOVED -- EA_MinEntryScore is single source of truth + } + // * v9.24 FIX#81c: max_spread_pips -- preserve pair-specific floor from Step 2.5. + // Old: ATR-based calc REPLACED pair values (Metal->50, Index->80) with generic ATRx0.1. + // XAUUSD M15: ATR=9pips -> max_spread=0.9pips -> blocked all trades (spread usually 2-3p)! + // Real XAUUSD spread floor from pair profile = 50pips -> trades allowed correctly. + // Fix: ATR calc provides a DYNAMIC estimate. Final value = MathMax(pair_floor, atr_calc). + // This ensures pair-specific knowledge (Metal=50, Index=80) acts as MINIMUM, + // while ATR-based calc can only WIDEN (not narrow) the allowed spread. + // * v7.4 FIX: Use g_pipValue (correctly handles 2-digit Gold where pip=point=0.01) + double atrInPips = (g_pipValue > 0) ? g_marketSnap.current_atr / g_pipValue : 0; + if(atrInPips > 0) + { + double atrSpread = MathMax(5.0, atrInPips * 0.1); // ATR-based estimate (min 5 pips) + // MathMax: keep whichever is larger (pair floor OR ATR estimate) + g_autoOptParams.max_spread_pips = MathMax(g_autoOptParams.max_spread_pips, atrSpread); + } + // If atrInPips == 0: keep whatever Step 2.5 set (pair-specific floor intact) +} +//+------------------------------------------------------------------+ +//| * v9.03 FIX#12: PAIR-SPECIFIC DETECTION ADJUSTMENTS | +//| Applied AFTER TF adjustments -- scales detection params per pair | +//| category. TF sets the base (bar count), this adjusts for pair | +//| characteristics (noise, volume patterns, regime speed). | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| STEP 10: Pair+TF Profile Override (v9.31 FIX#102) | +//| Applies exact values from the settings table for every | +//| pair x timeframe combination. Called LAST in AutoOpt pipeline. | +//| Overrides: spread, SL_min, SL_ATR, TP_ATR, risk%, trending | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| UNIFIED PAIR-TF PROFILE SYSTEM (v10.01 REFACTOR) | +//| | +//| SINGLE SOURCE OF TRUTH for all pair×TF parameters. | +//| | +//| OLD ARCHITECTURE (3 separate tables, constantly desyncing): | +//| 1. ApplyPairTFProfile() — sl/tp/mrr arrays per pair | +//| 2. GetPairTFMinRR() — DUPLICATE mrr array (desynced!) | +//| 3. InitializeAllPairProfiles() — H4-only pairProfiles[] | +//| → Result: 63 R:R mismatches in v10.00 audit | +//| | +//| NEW ARCHITECTURE (one struct, one function, zero desyncs): | +//| GetPairTFConfig(sym) → PairTFConfig struct | +//| ApplyPairTFProfile() reads from GetPairTFConfig() | +//| GetPairTFMinRR() reads from GetPairTFConfig() | +//| Self-validates: OnInit() calls ValidatePairTFConfigs() | +//| → tp1/sl >= mrr guaranteed at compile-time verification | +//+------------------------------------------------------------------+ + +// ── Column indices ───────────────────────────────────────────────── +// tf=0:M5 tf=1:M15 tf=2:H1 tf=3:H4 tf=4:D1 +// PairTFConfig struct moved above (v10.01 refactor) -- definition is near OptimizedProfile + +//+------------------------------------------------------------------+ +//| GetPairTFConfig — THE ONLY TABLE IN THE EA FOR PAIR PARAMETERS | +//| All other functions (Apply, GetMinRR, etc.) call this. | +//+------------------------------------------------------------------+ +PairTFConfig GetPairTFConfig(string sym) +{ + PairTFConfig c; + // Zero-init + ArrayInitialize(c.sl, 1.20); ArrayInitialize(c.tp1, 1.80); + ArrayInitialize(c.tp2, 2.50); ArrayInitialize(c.tp3, 3.50); + ArrayInitialize(c.mrr, 1.10); ArrayInitialize(c.risk, 1.00); + ArrayInitialize(c.sl_min, 5.0); ArrayInitialize(c.spread, 20.0); + // * FIX#289: min_conf and min_ev are now per-TF arrays. + // 0 = use category default from UpdateAutoOptimization (36 Major, 40 Metal etc). + // Set non-zero to override for a specific TF based on backtest results. + // Index: [0]=M5 [1]=M15 [2]=H1 [3]=H4 [4]=D1 + ArrayInitialize(c.min_conf, 0.0); // 0 = category default + ArrayInitialize(c.min_ev, 0.0); // 0 = category default + // * FIX#303: se_minrr/req_kz/req_trend per TF. 0=use global input. + ArrayInitialize(c.se_minrr, 0.0); // 0 = use EA_SmartExit_MinProfit_RR + ArrayInitialize(c.req_kz, 0); // 0 = use EA_RequireKillzone + ArrayInitialize(c.req_trend, 0); // 0 = use EA_RequireTrend + // * FIX#304: max_positions=0 → use EA global; score_cap=0 → no cap + ArrayInitialize(c.max_positions, 0); + ArrayInitialize(c.score_cap, 0); + // * v10.25 FIX#311: new per-TF fields — 0 = use global/category default + ArrayInitialize(c.tp1_pct, 0); // 0 = use EA_TP1_Percent + ArrayInitialize(c.tp2_pct, 0); // 0 = use EA_TP2_Percent + ArrayInitialize(c.tp3_pct, 0); // 0 = use EA_TP3_Percent + ArrayInitialize(c.block_neutral, 0); // 0 = use EA_D1CHoCH_BlockNeutral + ArrayInitialize(c.trendline_lb, 0); // 0 = use AutoOpt default + ArrayInitialize(c.rng_pen_tf, 0.0); // 0 = use category default + ArrayInitialize(c.d1choch_gate, 0); // 0 = off (all TF), 1 = on + // * v10.26 FIX#312: CT thresholds — 0 = use hardcoded defaults + ArrayInitialize(c.ct_ev_min, 0.0); // 0 = 0.20R (Forex hardcoded) + ArrayInitialize(c.ct_score_min, 0); // 0 = 58 (Forex hardcoded) + ArrayInitialize(c.ct_wp_min, 0.0); // 0 = 62% (low EV hardcoded) + ArrayInitialize(c.ct_wp_mid, 0.0); // 0 = 58% (mid EV hardcoded) + ArrayInitialize(c.ct_wp_high, 0.0); // 0 = 55% (high EV hardcoded) + // * v10.27 FIX#314: position sizing per-TF — 0 = use category/global default + ArrayInitialize(c.tr_bonus_tf, 0.0); // 0 = c.tr_bonus + ArrayInitialize(c.loss_streak_cut_tf, 0.0); // 0 = PosSize_LossStreakCut global + ArrayInitialize(c.max_lot_tf, 0.0); // 0 = SYMBOL_VOLUME_MAX (broker) + // * FIX#363: OB per-TF override — 0=global, 1=force-on, -1=force-off + ArrayInitialize(c.allow_ob, 0); // 0 = inherit global EnableOB + ArrayInitialize(c.allow_bos_retest, 0); // 0 = inherit global (enabled by default) + // * FIX#454: remaining technique overrides — 0=global, -1=force-off, 1=force-on + ArrayInitialize(c.allow_fvg, 0); // 0 = inherit global EnableFVG + ArrayInitialize(c.allow_ote, 0); // 0 = inherit global EnableOTE + ArrayInitialize(c.allow_liq, 0); // 0 = inherit global EnableLiquidity + ArrayInitialize(c.allow_breaker, 0); // 0 = inherit global EnableBreakerBlocks + ArrayInitialize(c.allow_tc, 0); // 0 = inherit global EnableTrendCont + // * FIX#421: SmartExit per-TF — 0 = use built-in defaults + ArrayInitialize(c.se_rsi_ob, 0.0); // 0 = default 70 + ArrayInitialize(c.se_rsi_os, 0.0); // 0 = default 30 + ArrayInitialize(c.se_peak_th1, 0.0); // 0 = default 0.65 + ArrayInitialize(c.se_peak_th2, 0.0); // 0 = default 0.72 + ArrayInitialize(c.se_peak_th3, 0.0); // 0 = default 0.78 + ArrayInitialize(c.se_override_rr, 0.0); // 0 = disabled + // * FIX#423: Cooperative SE thresholds — 0 = use built-in defaults + ArrayInitialize(c.se_mom_ratio, 0.0); // 0 = default 0.70 + ArrayInitialize(c.se_mom_score_mult, 0.0); // 0 = default 1.40 + ArrayInitialize(c.max_cost_pct, 0.0); // 0 = use global COST_MaxCostPercent + ArrayInitialize(c.min_confluence_cap, 0.0); // 0 = use FIX#165 TF defaults + c.comm=7.0; c.tr_bonus=1.20; c.rng_pen=0.80; c.min_wp=40.0; + // * FIX#282: 0=use category default from UpdateAutoOptimization + c.category = "Major"; + + // ── MAJOR FOREX ────────────────────────────────────────────── + if(sym == "EURUSD") + { + c.comm = 7.0; + c.tr_bonus = 1.20; + c.rng_pen = 0.80; + c.min_wp = 40.0; + c.category = "Major"; + + // ════════════════════════════════════════════════════════ + // M5 [index 0] + // ATR≈2p SL=1.80×ATR≈3.6p TP1=3.50×ATR≈7p RR=1.94≥mrr=1.60 ✓ + // OB WR=76% (+$1875), FVG WR=72% — TC/BREAKER disabled (negative EV) + // ════════════════════════════════════════════════════════ + c.sl[0] = 1.80; c.tp1[0] = 3.50; + c.tp2[0] = 5.00; c.tp3[0] = 7.00; + c.mrr[0] = 1.60; c.risk[0] = 0.50; + c.sl_min[0] = 2.0; c.spread[0] = 2.0; + + c.min_conf[0] = 55.0; // profitable zone ≥55 on M5 + c.min_ev[0] = 0.10; + + c.allow_tc[0] = -1; // TC: 14T WR=64% Net=-$1153 (late entry) + c.allow_breaker[0] = -1; // BREAKER: negative EV on M5 + c.max_positions[0] = 1; // 3-deal groups -$1667; single +$1606 + + c.tp1_pct[0] = 60; c.tp2_pct[0] = 25; c.tp3_pct[0] = 15; + c.score_cap[0]= 90; // above 90 = overfitting zone on M5 + + c.se_minrr[0] = 0.80; // avg win was tiny; 0.80R = real profit + c.req_kz[0] = 1; // London/NY killzone required + c.req_trend[0]= -1; // soft HTF gate active — hard block off + + c.trendline_lb[0] = -1; // 30-bar=2.5h → false trendlines; disable + c.rng_pen_tf[0] = 0.80; // flat (removes SCALP×0.75 cancellation) + + c.se_rsi_ob[0] = 72.0; c.se_rsi_os[0] = 28.0; + c.se_peak_th1[0] = 0.60; c.se_peak_th2[0] = 0.72; c.se_peak_th3[0] = 0.78; + c.se_override_rr[0] = 0.0; // no 2-cat early close on M5 + + c.max_cost_pct[0] = 28.0; // SL≈3.6p, spread≈0.5p → 14% → allow 28% + c.min_confluence_cap[0] = 0.0; // use FIX#165 TF default + + + // ════════════════════════════════════════════════════════ + // M15 [index 1] + // ATR≈5p SL=2.50×ATR≈12p TP1=4.00×ATR≈20p RR=1.60≥mrr=1.50 ✓ + // TC/BREAKER disabled (WR<50%, consistently negative EV) + // ════════════════════════════════════════════════════════ + c.sl[1] = 2.50; c.tp1[1] = 4.00; + c.tp2[1] = 6.00; c.tp3[1] = 8.00; + c.mrr[1] = 1.50; c.risk[1] = 1.00; + c.sl_min[1] = 4.0; c.spread[1] = 3.0; + + c.min_conf[1] = 60.0; // profitable zone 60-89 on M15 + c.min_ev[1] = 0.10; + + c.allow_tc[1] = -1; // TC: 13T WR=46% Net=-$1777 + c.allow_breaker[1] = -1; // BREAKER: 9T WR=33% Net=-$1286 + c.max_positions[1] = 1; + + c.tp1_pct[1] = 60; c.tp2_pct[1] = 25; c.tp3_pct[1] = 15; + c.score_cap[1]= 90; + + c.se_minrr[1] = 0.90; // was closing at $7-$47; 0.90R = real profit + c.req_kz[1] = 1; + c.req_trend[1]= -1; + + c.se_rsi_ob[1] = 72.0; c.se_rsi_os[1] = 28.0; + c.se_peak_th1[1] = 0.60; c.se_peak_th2[1] = 0.72; c.se_peak_th3[1] = 0.78; + c.se_override_rr[1] = 0.0; + + c.max_cost_pct[1] = 30.0; // SL≈12p, spread≈2p → 17% → allow 30% + c.min_confluence_cap[1] = 0.0; + + + // ════════════════════════════════════════════════════════ + // H1 [index 2] + // ATR≈12p SL=1.20×ATR≈14p TP1=2.00×ATR≈24p RR=1.67≥mrr=1.50 ✓ + // risk[2]=1.20%: ceiling=1.20×1.50=1.80% max effective. + // Παλιό 1.50%×A+(1.40)×Kelly(1.68)=2.70% → 4.41 lots → DD trigger + // Νέο 1.20% → worst case 1.80% → ~3.24 lots → FTMO safe + // ════════════════════════════════════════════════════════ + c.sl[2] = 1.20; c.tp1[2] = 2.00; + c.tp2[2] = 3.00; c.tp3[2] = 4.00; + c.mrr[2] = 1.50; c.risk[2] = 1.50; + c.sl_min[2] = 5.0; c.spread[2] = 10.0; + + c.min_conf[2] = 44.0; // OB/FVG score 44-49/85 is real edge on H1 + c.min_ev[2] = 0.07; // EV floor from Jan-Feb 2026 backtest + + c.allow_tc[2] = -1; // TC BUY WR=25%; OB/FVG carry the H1 edge + c.max_positions[2] = 1; // H1: single position (multi-TP splits triple the risk) + + c.tp1_pct[2] = 60; c.tp2_pct[2] = 25; c.tp3_pct[2] = 15; + c.score_cap[2]= 95; // H1 high scores more valid; liquidity traps ≥95 + + c.se_minrr[2] = 0.45; // H1: SE fires when 3+ cats + RR>=0.45R. Avg loss peak=0.72R → catches all near-misses + c.se_override_rr[2]= 0.85; // 2 cats ≥0.85R → close (captures 0.97R peak) + + c.choppy_min_conf[2] = 55.0; // CHOPPY: raise bar + c.block_tc_choppy[2] = 1; + c.choppy_lot_mult[2] = 0.70; + c.mtf_hard_block[2] = 0; // soft penalty only — hard block → deadlock + + c.se_rsi_ob[2] = 70.0; c.se_rsi_os[2] = 30.0; + c.se_peak_th1[2] = 0.72; c.se_peak_th2[2] = 0.78; c.se_peak_th3[2] = 0.82; + c.se_mom_ratio[2] = 0.70; // bar body must shrink <70% of prev + c.se_mom_score_mult[2] = 1.50; // momentum fade multiplier + + c.max_cost_pct[2] = 50.0; // H1: spread=4p/SL=8p=50% — Nov04 07:00 44% was blocked at 40% + c.min_confluence_cap[2]= 0.60; // FVG/OB typically achieves 0.60-0.75 + + c.d1choch_gate[2] = 0; // off on H1 + + // Disable techniques with negative edge on H1 EURUSD (backtest Nov-Dec 2024): + // MB_ENTRY (Mitigation Block): 2T / 0% WR / -$965 — mitigated OBs re-enter + // without clean reaction on H1. Uses TECH_BREAKER internally. + c.allow_breaker[2] = -1; + // KILLZONE_ENTRY (Silver Bullet): 8T / 62% WR / -$550 — payoff ratio broken. + // Large SL (structure-based) vs small TP (KZ breakout only). Uses TECH_SILVER_BULLET. + c.allow_liq[2] = -1; + + + // ════════════════════════════════════════════════════════ + // H4 [index 3] + // ATR≈35p SL=1.60×ATR≈56p TP1=3.20×ATR≈112p RR=2.00≥mrr=1.60 ✓ + // OB disabled (WR=30% Net=-$905 — structural trap zones) + // ════════════════════════════════════════════════════════ + c.sl[3] = 1.60; c.tp1[3] = 3.20; + c.tp2[3] = 4.80; c.tp3[3] = 6.40; + c.mrr[3] = 1.60; c.risk[3] = 1.80; + c.sl_min[3] = 10.0; c.spread[3] = 20.0; + + c.min_conf[3] = 44.0; + c.min_ev[3] = 0.00; // NN too uncertain on H4 to gate reliably + + c.allow_ob[3] = -1; // OB: 7W/16L WR=30% Net=-$905 + c.allow_bos_retest[3] = 0; // BOS_RETEST: global default (enabled) + + c.tp1_pct[3] = 50; c.tp2_pct[3] = 25; c.tp3_pct[3] = 25; + c.score_cap[3]= 0; // no cap on H4 + + c.se_minrr[3] = 1.20; // was firing at 0.31/0.56/0.69R on H4 + c.se_override_rr[3]= 3.00; // larger targets need more confirmation + + c.choppy_min_conf[3] = 48.0; + c.block_tc_choppy[3] = 1; + c.choppy_lot_mult[3] = 0.60; + c.mtf_hard_block[3] = 1; // STRONG MTF opposition = hard block on H4 + + c.se_rsi_ob[3] = 68.0; c.se_rsi_os[3] = 32.0; + c.se_peak_th1[3] = 0.72; c.se_peak_th2[3] = 0.76; c.se_peak_th3[3] = 0.80; + c.se_mom_ratio[3] = 0.70; + c.se_mom_score_mult[3] = 1.40; + + c.max_cost_pct[3] = 15.0; // SL≈56p, spread≈1p → 2% → tighten to 15% + + // CT gates relaxed — blocked BOS_RETEST had score=63-68, EV=0.09-0.12R, WP=56-57% + // All quality-B setups that likely would have won (BOS_RETEST 100% WR in test) + c.ct_ev_min[3] = 0.08; + c.ct_score_min[3] = 50; + c.ct_wp_min[3] = 52.0; + c.ct_wp_mid[3] = 50.0; + c.ct_wp_high[3] = 48.0; + + // Position sizing: each of the ~5 trades/month matters; protect streaks + c.tr_bonus_tf[3] = 1.35; + c.loss_streak_cut_tf[3] = 0.08; + c.max_lot_tf[3] = 4.00; + + // Mean Reversion: H4 EURUSD spends ~40% of bars in CHOPPY/RANGING + c.mr_enabled[3] = 1; + c.mr_rsi_buy[3] = 32; // more extreme confirmation than default 35 + c.mr_rsi_sell[3] = 68; + c.mr_sl_mult[3] = 0.40; // wider buffer for H4 wicks + c.mr_min_range_atr[3] = 1.8; // range ≥63p at ATR=35p (viable midpoint TP) + + c.d1choch_gate[3] = 0; + + + // ════════════════════════════════════════════════════════ + // D1 [index 4] + // ATR≈80p SL=2.50×ATR≈200p TP1=6.50×ATR≈520p RR=2.60≥mrr=1.70 ✓ + // Kelly bypass active (< 10 D1 trades — formula unreliable) + // ════════════════════════════════════════════════════════ + c.sl[4] = 2.50; c.tp1[4] = 6.50; + c.tp2[4] = 9.00; c.tp3[4] = 12.0; + c.mrr[4] = 1.70; c.risk[4] = 1.50; // flat rate; Kelly bypassed + c.sl_min[4] = 30.0; c.spread[4] = 50.0; + + c.min_conf[4] = 40.0; + c.min_ev[4] = 0.00; + + c.max_positions[4] = 1; // one position at a time on D1 + + c.tp1_pct[4] = 60; c.tp2_pct[4] = 25; c.tp3_pct[4] = 15; + + c.se_minrr[4] = 1.50; + c.se_override_rr[4]= 4.00; // 2 cats need 4.0R before early close on D1 + + c.choppy_min_conf[4] = 50.0; + c.block_tc_choppy[4] = 1; + c.mtf_hard_block[4] = 1; + + // D1 SE peak thresholds: allow 30-40% pullbacks (normal on D1) + c.se_rsi_ob[4] = 65.0; c.se_rsi_os[4] = 35.0; + c.se_peak_th1[4] = 0.50; c.se_peak_th2[4] = 0.55; c.se_peak_th3[4] = 0.65; + + c.max_cost_pct[4] = 10.0; + c.sl_min[4] = 30.0; + + c.d1choch_gate[4] = 1; // D1 CHoCH gate active on D1 only + + // ── H4 [index 3] — additional fields ───────────────────────── + c.min_conf[3] = 44.0; c.min_ev[3] = 0.00; + c.max_positions[3] = 0; + c.se_peak_th1[3] = 0.72; c.se_peak_th2[3] = 0.76; c.se_peak_th3[3] = 0.80; + c.d1choch_gate[3] = 0; + + // ── D1 [index 4] ────────────────────────────────────────────── + // ATR≈80p SL=2.50×ATR≈200p TP1=6.50×ATR≈520p RR=2.60≥mrr=1.70 ✓ + // Kelly bypass active (< 10 D1 trades — formula unreliable) + c.sl[4] = 2.50; c.tp1[4] = 6.50; + c.tp2[4] = 9.00; c.tp3[4] = 12.0; + c.mrr[4] = 1.70; c.risk[4] = 1.50; + c.sl_min[4] = 30.0; c.spread[4] = 50.0; + c.min_conf[4] = 40.0; c.min_ev[4] = 0.00; + c.se_minrr[4] = 1.50; + c.tp1_pct[4] = 60; c.tp2_pct[4] = 25; c.tp3_pct[4] = 15; + c.max_cost_pct[4] = 10.0; + c.max_positions[4] = 1; + c.se_peak_th1[4] = 0.50; c.se_peak_th2[4] = 0.55; c.se_peak_th3[4] = 0.65; + c.se_override_rr[4]= 4.00; + c.choppy_min_conf[4] = 50.0; + c.block_tc_choppy[4] = 1; + c.mtf_hard_block[4] = 1; + c.d1choch_gate[4] = 1; +} + else if(sym == "GBPUSD") + { + c.comm = 7.0; + c.tr_bonus = 1.15; + c.rng_pen = 0.75; + c.min_wp = 40.0; + c.category = "Major"; + + // ── M5 [index 0] + c.sl[0] = 1.20; c.tp1[0] = 1.80; + c.tp2[0] = 2.30; c.tp3[0] = 2.80; + c.mrr[0] = 1.10; c.risk[0] = 0.50; + c.sl_min[0] = 4.0; c.spread[0] = 6.0; + c.min_conf[0] = 45.0; c.min_ev[0] = 0.10; + c.se_minrr[0] = 0.50; + c.tp1_pct[0] = 60; c.tp2_pct[0] = 25; c.tp3_pct[0] = 15; + c.max_cost_pct[0] = 28.0; + c.allow_tc[0] = -1; + c.req_kz[0] = 1; + c.req_trend[0] = -1; + c.max_positions[0] = 1; + c.se_peak_th1[0] = 0.60; c.se_peak_th2[0] = 0.72; c.se_peak_th3[0] = 0.78; + c.d1choch_gate[0] = 0; + + // ── M15 [index 1] + c.sl[1] = 2.00; c.tp1[1] = 3.00; + c.tp2[1] = 4.00; c.tp3[1] = 5.00; + c.mrr[1] = 1.30; c.risk[1] = 1.00; + c.sl_min[1] = 5.0; c.spread[1] = 6.0; + c.min_conf[1] = 60.0; c.min_ev[1] = 0.10; + c.se_minrr[1] = 0.70; + c.tp1_pct[1] = 60; c.tp2_pct[1] = 25; c.tp3_pct[1] = 15; + c.max_cost_pct[1] = 30.0; + c.allow_tc[1] = -1; + c.req_kz[1] = 1; + c.req_trend[1] = -1; + c.max_positions[1] = 1; + c.se_peak_th1[1] = 0.60; c.se_peak_th2[1] = 0.72; c.se_peak_th3[1] = 0.78; + c.d1choch_gate[1] = 0; + + // ── H1 [index 2] + c.sl[2] = 2.00; c.tp1[2] = 3.20; + c.tp2[2] = 4.20; c.tp3[2] = 5.50; + c.mrr[2] = 1.50; c.risk[2] = 1.50; + c.sl_min[2] = 7.0; c.spread[2] = 15.0; + c.min_conf[2] = 50.0; c.min_ev[2] = 0.07; + c.se_minrr[2] = 0.80; + c.tp1_pct[2] = 50; c.tp2_pct[2] = 30; c.tp3_pct[2] = 20; + c.max_cost_pct[2] = 30.0; + c.allow_tc[2] = -1; + c.req_kz[2] = 0; + c.req_trend[2] = 0; + c.max_positions[2] = 1; // H1: single position (multi-TP split wastes 3× risk per trade) + c.se_peak_th1[2] = 0.72; c.se_peak_th2[2] = 0.78; c.se_peak_th3[2] = 0.82; + c.d1choch_gate[2] = 0; + + // ── H4 [index 3] + c.sl[3] = 2.20; c.tp1[3] = 4.00; + c.tp2[3] = 5.20; c.tp3[3] = 7.00; + c.mrr[3] = 1.50; c.risk[3] = 2.00; + c.sl_min[3] = 12.0; c.spread[3] = 30.0; + c.min_conf[3] = 44.0; c.min_ev[3] = 0.00; + c.se_minrr[3] = 1.20; + c.tp1_pct[3] = 60; c.tp2_pct[3] = 25; c.tp3_pct[3] = 15; + c.max_cost_pct[3] = 15.0; + c.allow_tc[3] = 0; + c.req_kz[3] = 0; + c.req_trend[3] = 0; + c.max_positions[3] = 0; + c.se_peak_th1[3] = 0.72; c.se_peak_th2[3] = 0.76; c.se_peak_th3[3] = 0.80; + c.d1choch_gate[3] = 0; + + // ── D1 [index 4] + c.sl[4] = 3.00; c.tp1[4] = 5.50; + c.tp2[4] = 7.50; c.tp3[4] = 10.50; + c.mrr[4] = 1.80; c.risk[4] = 2.00; + c.sl_min[4] = 25.0; c.spread[4] = 60.0; + c.min_conf[4] = 40.0; c.min_ev[4] = 0.00; + c.se_minrr[4] = 1.50; + c.tp1_pct[4] = 60; c.tp2_pct[4] = 25; c.tp3_pct[4] = 15; + c.max_cost_pct[4] = 10.0; + c.allow_tc[4] = 0; + c.req_kz[4] = 0; + c.req_trend[4] = 0; + c.max_positions[4] = 0; + c.se_peak_th1[4] = 0.50; c.se_peak_th2[4] = 0.55; c.se_peak_th3[4] = 0.65; + c.d1choch_gate[4] = 1; + + } + else if(sym == "AUDUSD" || sym == "NZDUSD") + { + c.comm = 7.0; + c.tr_bonus = 1.2; + c.rng_pen = 0.8; + c.min_wp = 40.0; + c.category = "Major"; + + // ── M5 [index 0] + c.sl[0] = 1.20; c.tp1[0] = 1.80; + c.tp2[0] = 2.30; c.tp3[0] = 2.80; + c.mrr[0] = 1.50; c.risk[0] = 0.50; + c.sl_min[0] = 3.0; c.spread[0] = 4.0; + c.min_conf[0] = 45.0; c.min_ev[0] = 0.10; + c.se_minrr[0] = 0.50; + c.tp1_pct[0] = 60; c.tp2_pct[0] = 25; c.tp3_pct[0] = 15; + c.max_cost_pct[0] = 28.0; + c.allow_tc[0] = -1; + c.req_kz[0] = 1; + c.req_trend[0] = -1; + c.max_positions[0] = 1; + c.se_peak_th1[0] = 0.60; c.se_peak_th2[0] = 0.72; c.se_peak_th3[0] = 0.78; + c.d1choch_gate[0] = 0; + + // ── M15 [index 1] + c.sl[1] = 1.80; c.tp1[1] = 2.80; + c.tp2[1] = 3.50; c.tp3[1] = 4.50; + c.mrr[1] = 1.30; c.risk[1] = 1.00; + c.sl_min[1] = 4.0; c.spread[1] = 4.0; + c.min_conf[1] = 60.0; c.min_ev[1] = 0.10; + c.se_minrr[1] = 0.70; + c.tp1_pct[1] = 60; c.tp2_pct[1] = 25; c.tp3_pct[1] = 15; + c.max_cost_pct[1] = 30.0; + c.allow_tc[1] = -1; + c.req_kz[1] = 1; + c.req_trend[1] = -1; + c.max_positions[1] = 1; + c.se_peak_th1[1] = 0.60; c.se_peak_th2[1] = 0.72; c.se_peak_th3[1] = 0.78; + c.d1choch_gate[1] = 0; + + // ── H1 [index 2] + c.sl[2] = 2.00; c.tp1[2] = 3.00; + c.tp2[2] = 4.00; c.tp3[2] = 5.50; + c.mrr[2] = 1.50; c.risk[2] = 1.00; + c.sl_min[2] = 6.0; c.spread[2] = 10.0; + c.min_conf[2] = 50.0; c.min_ev[2] = 0.07; + c.se_minrr[2] = 0.80; + c.tp1_pct[2] = 50; c.tp2_pct[2] = 30; c.tp3_pct[2] = 20; + c.max_cost_pct[2] = 30.0; + c.allow_tc[2] = -1; + c.req_kz[2] = 0; + c.req_trend[2] = 0; + c.max_positions[2] = 1; // H1: single position (multi-TP split wastes 3× risk per trade) + c.se_peak_th1[2] = 0.72; c.se_peak_th2[2] = 0.78; c.se_peak_th3[2] = 0.82; + c.d1choch_gate[2] = 0; + + // ── H4 [index 3] + c.sl[3] = 2.00; c.tp1[3] = 3.60; + c.tp2[3] = 4.70; c.tp3[3] = 6.50; + c.mrr[3] = 1.45; c.risk[3] = 1.50; + c.sl_min[3] = 12.0; c.spread[3] = 20.0; + c.min_conf[3] = 44.0; c.min_ev[3] = 0.00; + c.se_minrr[3] = 1.20; + c.tp1_pct[3] = 60; c.tp2_pct[3] = 25; c.tp3_pct[3] = 15; + c.max_cost_pct[3] = 15.0; + c.allow_tc[3] = 0; + c.req_kz[3] = 0; + c.req_trend[3] = 0; + c.max_positions[3] = 0; + c.se_peak_th1[3] = 0.72; c.se_peak_th2[3] = 0.76; c.se_peak_th3[3] = 0.80; + c.d1choch_gate[3] = 0; + + // ── D1 [index 4] + c.sl[4] = 2.50; c.tp1[4] = 4.00; + c.tp2[4] = 5.50; c.tp3[4] = 7.50; + c.mrr[4] = 1.60; c.risk[4] = 2.00; + c.sl_min[4] = 20.0; c.spread[4] = 40.0; + c.min_conf[4] = 40.0; c.min_ev[4] = 0.00; + c.se_minrr[4] = 1.50; + c.tp1_pct[4] = 60; c.tp2_pct[4] = 25; c.tp3_pct[4] = 15; + c.max_cost_pct[4] = 10.0; + c.allow_tc[4] = 0; + c.req_kz[4] = 0; + c.req_trend[4] = 0; + c.max_positions[4] = 0; + c.se_peak_th1[4] = 0.50; c.se_peak_th2[4] = 0.55; c.se_peak_th3[4] = 0.65; + c.d1choch_gate[4] = 1; + + } + else if(sym == "USDJPY") + { + c.comm = 7.0; + c.tr_bonus = 1.2; + c.rng_pen = 0.8; + c.min_wp = 40.0; + c.category = "Major"; + + // ── M5 [index 0] + c.sl[0] = 1.20; c.tp1[0] = 1.80; + c.tp2[0] = 2.30; c.tp3[0] = 2.90; + c.mrr[0] = 1.50; c.risk[0] = 0.50; + c.sl_min[0] = 3.0; c.spread[0] = 4.0; + c.min_conf[0] = 45.0; c.min_ev[0] = 0.10; + c.se_minrr[0] = 0.50; + c.tp1_pct[0] = 60; c.tp2_pct[0] = 25; c.tp3_pct[0] = 15; + c.max_cost_pct[0] = 28.0; + c.allow_tc[0] = -1; + c.req_kz[0] = 1; + c.req_trend[0] = -1; + c.max_positions[0] = 1; + c.se_peak_th1[0] = 0.60; c.se_peak_th2[0] = 0.72; c.se_peak_th3[0] = 0.78; + c.d1choch_gate[0] = 0; + + // ── M15 [index 1] + c.sl[1] = 1.80; c.tp1[1] = 2.80; + c.tp2[1] = 3.70; c.tp3[1] = 4.70; + c.mrr[1] = 1.30; c.risk[1] = 1.00; + c.sl_min[1] = 4.0; c.spread[1] = 4.0; + c.min_conf[1] = 60.0; c.min_ev[1] = 0.10; + c.se_minrr[1] = 0.70; + c.tp1_pct[1] = 60; c.tp2_pct[1] = 25; c.tp3_pct[1] = 15; + c.max_cost_pct[1] = 30.0; + c.allow_tc[1] = -1; + c.req_kz[1] = 1; + c.req_trend[1] = -1; + c.max_positions[1] = 1; + c.se_peak_th1[1] = 0.60; c.se_peak_th2[1] = 0.72; c.se_peak_th3[1] = 0.78; + c.d1choch_gate[1] = 0; + + // ── H1 [index 2] + c.sl[2] = 2.00; c.tp1[2] = 3.00; + c.tp2[2] = 4.00; c.tp3[2] = 5.50; + c.mrr[2] = 1.50; c.risk[2] = 1.00; + c.sl_min[2] = 6.0; c.spread[2] = 10.0; + c.min_conf[2] = 50.0; c.min_ev[2] = 0.07; + c.se_minrr[2] = 0.80; + c.tp1_pct[2] = 50; c.tp2_pct[2] = 30; c.tp3_pct[2] = 20; + c.max_cost_pct[2] = 30.0; + c.allow_tc[2] = -1; + c.req_kz[2] = 0; + c.req_trend[2] = 0; + c.max_positions[2] = 1; // H1: single position (multi-TP split wastes 3× risk per trade) + c.se_peak_th1[2] = 0.72; c.se_peak_th2[2] = 0.78; c.se_peak_th3[2] = 0.82; + c.d1choch_gate[2] = 0; + + // ── H4 [index 3] + c.sl[3] = 2.00; c.tp1[3] = 3.60; + c.tp2[3] = 4.80; c.tp3[3] = 6.50; + c.mrr[3] = 1.45; c.risk[3] = 1.50; + c.sl_min[3] = 12.0; c.spread[3] = 20.0; + c.min_conf[3] = 44.0; c.min_ev[3] = 0.00; + c.se_minrr[3] = 1.20; + c.tp1_pct[3] = 60; c.tp2_pct[3] = 25; c.tp3_pct[3] = 15; + c.max_cost_pct[3] = 15.0; + c.allow_tc[3] = 0; + c.req_kz[3] = 0; + c.req_trend[3] = 0; + c.max_positions[3] = 0; + c.se_peak_th1[3] = 0.72; c.se_peak_th2[3] = 0.76; c.se_peak_th3[3] = 0.80; + c.d1choch_gate[3] = 0; + + // ── D1 [index 4] + c.sl[4] = 2.50; c.tp1[4] = 4.00; + c.tp2[4] = 5.50; c.tp3[4] = 7.50; + c.mrr[4] = 1.60; c.risk[4] = 2.00; + c.sl_min[4] = 20.0; c.spread[4] = 40.0; + c.min_conf[4] = 40.0; c.min_ev[4] = 0.00; + c.se_minrr[4] = 1.50; + c.tp1_pct[4] = 60; c.tp2_pct[4] = 25; c.tp3_pct[4] = 15; + c.max_cost_pct[4] = 10.0; + c.allow_tc[4] = 0; + c.req_kz[4] = 0; + c.req_trend[4] = 0; + c.max_positions[4] = 0; + c.se_peak_th1[4] = 0.50; c.se_peak_th2[4] = 0.55; c.se_peak_th3[4] = 0.65; + c.d1choch_gate[4] = 1; + + } + else if(sym == "USDCAD") + { + c.comm = 7.0; + c.tr_bonus = 1.2; + c.rng_pen = 0.75; + c.min_wp = 40.0; + c.category = "Major"; + + // ── M5 [index 0] + c.sl[0] = 1.20; c.tp1[0] = 1.80; + c.tp2[0] = 2.30; c.tp3[0] = 2.80; + c.mrr[0] = 1.50; c.risk[0] = 0.50; + c.sl_min[0] = 3.0; c.spread[0] = 3.0; + c.min_conf[0] = 45.0; c.min_ev[0] = 0.10; + c.se_minrr[0] = 0.50; + c.tp1_pct[0] = 60; c.tp2_pct[0] = 25; c.tp3_pct[0] = 15; + c.max_cost_pct[0] = 28.0; + c.allow_tc[0] = -1; + c.req_kz[0] = 1; + c.req_trend[0] = -1; + c.max_positions[0] = 1; + c.se_peak_th1[0] = 0.60; c.se_peak_th2[0] = 0.72; c.se_peak_th3[0] = 0.78; + c.d1choch_gate[0] = 0; + + // ── M15 [index 1] + c.sl[1] = 1.80; c.tp1[1] = 2.80; + c.tp2[1] = 3.70; c.tp3[1] = 4.70; + c.mrr[1] = 1.30; c.risk[1] = 1.00; + c.sl_min[1] = 5.0; c.spread[1] = 3.0; + c.min_conf[1] = 60.0; c.min_ev[1] = 0.10; + c.se_minrr[1] = 0.70; + c.tp1_pct[1] = 60; c.tp2_pct[1] = 25; c.tp3_pct[1] = 15; + c.max_cost_pct[1] = 30.0; + c.allow_tc[1] = -1; + c.req_kz[1] = 1; + c.req_trend[1] = -1; + c.max_positions[1] = 1; + c.se_peak_th1[1] = 0.60; c.se_peak_th2[1] = 0.72; c.se_peak_th3[1] = 0.78; + c.d1choch_gate[1] = 0; + + // ── H1 [index 2] + c.sl[2] = 2.00; c.tp1[2] = 3.00; + c.tp2[2] = 4.00; c.tp3[2] = 5.50; + c.mrr[2] = 1.50; c.risk[2] = 1.00; + c.sl_min[2] = 7.0; c.spread[2] = 8.0; + c.min_conf[2] = 50.0; c.min_ev[2] = 0.07; + c.se_minrr[2] = 0.80; + c.tp1_pct[2] = 60; c.tp2_pct[2] = 25; c.tp3_pct[2] = 15; + c.max_cost_pct[2] = 30.0; + c.allow_tc[2] = -1; + c.req_kz[2] = 0; + c.req_trend[2] = 0; + c.max_positions[2] = 1; // H1: single position (multi-TP split wastes 3× risk per trade) + c.se_peak_th1[2] = 0.72; c.se_peak_th2[2] = 0.78; c.se_peak_th3[2] = 0.82; + c.d1choch_gate[2] = 0; + + // ── H4 [index 3] + c.sl[3] = 2.00; c.tp1[3] = 3.60; + c.tp2[3] = 4.80; c.tp3[3] = 6.50; + c.mrr[3] = 1.45; c.risk[3] = 1.50; + c.sl_min[3] = 12.0; c.spread[3] = 20.0; + c.min_conf[3] = 44.0; c.min_ev[3] = 0.00; + c.se_minrr[3] = 1.20; + c.tp1_pct[3] = 60; c.tp2_pct[3] = 25; c.tp3_pct[3] = 15; + c.max_cost_pct[3] = 15.0; + c.allow_tc[3] = 0; + c.req_kz[3] = 0; + c.req_trend[3] = 0; + c.max_positions[3] = 0; + c.se_peak_th1[3] = 0.72; c.se_peak_th2[3] = 0.76; c.se_peak_th3[3] = 0.80; + c.d1choch_gate[3] = 0; + + // ── D1 [index 4] + c.sl[4] = 2.50; c.tp1[4] = 4.00; + c.tp2[4] = 5.50; c.tp3[4] = 7.50; + c.mrr[4] = 1.60; c.risk[4] = 2.00; + c.sl_min[4] = 22.0; c.spread[4] = 40.0; + c.min_conf[4] = 40.0; c.min_ev[4] = 0.00; + c.se_minrr[4] = 1.50; + c.tp1_pct[4] = 60; c.tp2_pct[4] = 25; c.tp3_pct[4] = 15; + c.max_cost_pct[4] = 10.0; + c.allow_tc[4] = 0; + c.req_kz[4] = 0; + c.req_trend[4] = 0; + c.max_positions[4] = 0; + c.se_peak_th1[4] = 0.50; c.se_peak_th2[4] = 0.55; c.se_peak_th3[4] = 0.65; + c.d1choch_gate[4] = 1; + + } + else if(sym == "USDCHF") + { + c.comm = 7.0; + c.tr_bonus = 1.2; + c.rng_pen = 0.8; + c.min_wp = 40.0; + c.category = "Major"; + + // ── M5 [index 0] + c.sl[0] = 1.20; c.tp1[0] = 1.80; + c.tp2[0] = 2.30; c.tp3[0] = 2.80; + c.mrr[0] = 1.50; c.risk[0] = 0.50; + c.sl_min[0] = 3.0; c.spread[0] = 2.5; + c.min_conf[0] = 45.0; c.min_ev[0] = 0.10; + c.se_minrr[0] = 0.50; + c.tp1_pct[0] = 60; c.tp2_pct[0] = 25; c.tp3_pct[0] = 15; + c.max_cost_pct[0] = 28.0; + c.allow_tc[0] = -1; + c.req_kz[0] = 1; + c.req_trend[0] = -1; + c.max_positions[0] = 1; + c.se_peak_th1[0] = 0.60; c.se_peak_th2[0] = 0.72; c.se_peak_th3[0] = 0.78; + c.d1choch_gate[0] = 0; + + // ── M15 [index 1] + c.sl[1] = 1.80; c.tp1[1] = 2.80; + c.tp2[1] = 3.70; c.tp3[1] = 4.70; + c.mrr[1] = 1.30; c.risk[1] = 1.00; + c.sl_min[1] = 4.0; c.spread[1] = 2.5; + c.min_conf[1] = 60.0; c.min_ev[1] = 0.10; + c.se_minrr[1] = 0.70; + c.tp1_pct[1] = 60; c.tp2_pct[1] = 25; c.tp3_pct[1] = 15; + c.max_cost_pct[1] = 30.0; + c.allow_tc[1] = -1; + c.req_kz[1] = 1; + c.req_trend[1] = -1; + c.max_positions[1] = 1; + c.se_peak_th1[1] = 0.60; c.se_peak_th2[1] = 0.72; c.se_peak_th3[1] = 0.78; + c.d1choch_gate[1] = 0; + + // ── H1 [index 2] + c.sl[2] = 2.00; c.tp1[2] = 3.20; + c.tp2[2] = 4.20; c.tp3[2] = 5.50; + c.mrr[2] = 1.50; c.risk[2] = 1.50; + c.sl_min[2] = 6.0; c.spread[2] = 8.0; + c.min_conf[2] = 50.0; c.min_ev[2] = 0.07; + c.se_minrr[2] = 0.80; + c.tp1_pct[2] = 50; c.tp2_pct[2] = 30; c.tp3_pct[2] = 20; + c.max_cost_pct[2] = 30.0; + c.allow_tc[2] = -1; + c.req_kz[2] = 0; + c.req_trend[2] = 0; + c.max_positions[2] = 1; // H1: single position (multi-TP split wastes 3× risk per trade) + c.se_peak_th1[2] = 0.72; c.se_peak_th2[2] = 0.78; c.se_peak_th3[2] = 0.82; + c.d1choch_gate[2] = 0; + + // ── H4 [index 3] + c.sl[3] = 2.00; c.tp1[3] = 3.60; + c.tp2[3] = 4.80; c.tp3[3] = 6.50; + c.mrr[3] = 1.45; c.risk[3] = 2.00; + c.sl_min[3] = 12.0; c.spread[3] = 20.0; + c.min_conf[3] = 44.0; c.min_ev[3] = 0.00; + c.se_minrr[3] = 1.20; + c.tp1_pct[3] = 60; c.tp2_pct[3] = 25; c.tp3_pct[3] = 15; + c.max_cost_pct[3] = 15.0; + c.allow_tc[3] = 0; + c.req_kz[3] = 0; + c.req_trend[3] = 0; + c.max_positions[3] = 0; + c.se_peak_th1[3] = 0.72; c.se_peak_th2[3] = 0.76; c.se_peak_th3[3] = 0.80; + c.d1choch_gate[3] = 0; + + // ── D1 [index 4] + c.sl[4] = 2.50; c.tp1[4] = 4.20; + c.tp2[4] = 5.80; c.tp3[4] = 7.80; + c.mrr[4] = 1.65; c.risk[4] = 2.00; + c.sl_min[4] = 20.0; c.spread[4] = 40.0; + c.min_conf[4] = 40.0; c.min_ev[4] = 0.00; + c.se_minrr[4] = 1.50; + c.tp1_pct[4] = 60; c.tp2_pct[4] = 25; c.tp3_pct[4] = 15; + c.max_cost_pct[4] = 10.0; + c.allow_tc[4] = 0; + c.req_kz[4] = 0; + c.req_trend[4] = 0; + c.max_positions[4] = 0; + c.se_peak_th1[4] = 0.50; c.se_peak_th2[4] = 0.55; c.se_peak_th3[4] = 0.65; + c.d1choch_gate[4] = 1; + + } + // ── JPY CROSSES ────────────────────────────────────────────── + else if(sym == "GBPJPY") + { + c.comm = 8.0; + c.tr_bonus = 1.45; + c.rng_pen = 0.55; + c.min_wp = 40.0; + c.category = "VolatileCross"; + + // ── M5 [index 0] + c.sl[0] = 1.30; c.tp1[0] = 2.00; + c.tp2[0] = 2.70; c.tp3[0] = 3.30; + c.mrr[0] = 1.50; c.risk[0] = 0.60; + c.sl_min[0] = 5.0; c.spread[0] = 8.0; + c.min_conf[0] = 50.0; c.min_ev[0] = 0.10; + c.se_minrr[0] = 0.60; + c.tp1_pct[0] = 60; c.tp2_pct[0] = 25; c.tp3_pct[0] = 15; + c.max_cost_pct[0] = 35.0; + c.allow_tc[0] = -1; + c.req_kz[0] = 1; + c.req_trend[0] = -1; + c.max_positions[0] = 1; + c.se_peak_th1[0] = 0.65; c.se_peak_th2[0] = 0.74; c.se_peak_th3[0] = 0.80; + c.d1choch_gate[0] = 0; + + // ── M15 [index 1] + c.sl[1] = 2.00; c.tp1[1] = 3.10; + c.tp2[1] = 4.20; c.tp3[1] = 5.30; + c.mrr[1] = 1.50; c.risk[1] = 0.80; + c.sl_min[1] = 8.0; c.spread[1] = 8.0; + c.min_conf[1] = 62.0; c.min_ev[1] = 0.10; + c.se_minrr[1] = 0.80; + c.tp1_pct[1] = 60; c.tp2_pct[1] = 25; c.tp3_pct[1] = 15; + c.max_cost_pct[1] = 38.0; + c.allow_tc[1] = -1; + c.req_kz[1] = 1; + c.req_trend[1] = -1; + c.max_positions[1] = 1; + c.se_peak_th1[1] = 0.65; c.se_peak_th2[1] = 0.74; c.se_peak_th3[1] = 0.80; + c.d1choch_gate[1] = 0; + + // ── H1 [index 2] + c.sl[2] = 2.20; c.tp1[2] = 3.40; + c.tp2[2] = 4.60; c.tp3[2] = 5.80; + c.mrr[2] = 1.50; c.risk[2] = 1.00; + c.sl_min[2] = 12.0; c.spread[2] = 20.0; + c.min_conf[2] = 52.0; c.min_ev[2] = 0.07; + c.se_minrr[2] = 1.00; + c.tp1_pct[2] = 55; c.tp2_pct[2] = 25; c.tp3_pct[2] = 20; + c.max_cost_pct[2] = 35.0; + c.allow_tc[2] = -1; + c.req_kz[2] = 0; + c.req_trend[2] = 0; + c.max_positions[2] = 1; // H1: single position (multi-TP split wastes 3× risk per trade) + c.se_peak_th1[2] = 0.75; c.se_peak_th2[2] = 0.80; c.se_peak_th3[2] = 0.84; + c.d1choch_gate[2] = 0; + + // ── H4 [index 3] + c.sl[3] = 2.50; c.tp1[3] = 4.60; + c.tp2[3] = 6.20; c.tp3[3] = 8.00; + c.mrr[3] = 1.80; c.risk[3] = 1.20; + c.sl_min[3] = 20.0; c.spread[3] = 40.0; + c.min_conf[3] = 46.0; c.min_ev[3] = 0.00; + c.se_minrr[3] = 1.30; + c.tp1_pct[3] = 60; c.tp2_pct[3] = 25; c.tp3_pct[3] = 15; + c.max_cost_pct[3] = 20.0; + c.allow_tc[3] = 0; + c.req_kz[3] = 0; + c.req_trend[3] = 0; + c.max_positions[3] = 0; + c.se_peak_th1[3] = 0.74; c.se_peak_th2[3] = 0.78; c.se_peak_th3[3] = 0.82; + c.d1choch_gate[3] = 0; + + // ── D1 [index 4] + c.sl[4] = 3.20; c.tp1[4] = 6.50; + c.tp2[4] = 8.80; c.tp3[4] = 11.50; + c.mrr[4] = 1.90; c.risk[4] = 1.80; + c.sl_min[4] = 35.0; c.spread[4] = 80.0; + c.min_conf[4] = 42.0; c.min_ev[4] = 0.00; + c.se_minrr[4] = 1.60; + c.tp1_pct[4] = 60; c.tp2_pct[4] = 25; c.tp3_pct[4] = 15; + c.max_cost_pct[4] = 12.0; + c.allow_tc[4] = 0; + c.req_kz[4] = 0; + c.req_trend[4] = 0; + c.max_positions[4] = 0; + c.se_peak_th1[4] = 0.52; c.se_peak_th2[4] = 0.57; c.se_peak_th3[4] = 0.67; + c.d1choch_gate[4] = 1; + + } + else if(sym == "EURJPY" || sym == "AUDJPY" || sym == "CADJPY" || sym == "CHFJPY" || sym == "NZDJPY") + { + c.comm = 7.0; + c.tr_bonus = 1.4; + c.rng_pen = 0.6; + c.min_wp = 40.0; + c.category = "VolatileCross"; + + // ── M5 [index 0] + c.sl[0] = 1.25; c.tp1[0] = 1.90; + c.tp2[0] = 2.50; c.tp3[0] = 3.10; + c.mrr[0] = 1.50; c.risk[0] = 0.70; + c.sl_min[0] = 4.0; c.spread[0] = 5.0; + c.min_conf[0] = 50.0; c.min_ev[0] = 0.10; + c.se_minrr[0] = 0.60; + c.tp1_pct[0] = 60; c.tp2_pct[0] = 25; c.tp3_pct[0] = 15; + c.max_cost_pct[0] = 35.0; + c.allow_tc[0] = -1; + c.req_kz[0] = 1; + c.req_trend[0] = -1; + c.max_positions[0] = 1; + c.se_peak_th1[0] = 0.65; c.se_peak_th2[0] = 0.74; c.se_peak_th3[0] = 0.80; + c.d1choch_gate[0] = 0; + + // ── M15 [index 1] + c.sl[1] = 1.90; c.tp1[1] = 2.80; + c.tp2[1] = 3.80; c.tp3[1] = 4.80; + c.mrr[1] = 1.40; c.risk[1] = 0.85; + c.sl_min[1] = 6.0; c.spread[1] = 5.0; + c.min_conf[1] = 62.0; c.min_ev[1] = 0.10; + c.se_minrr[1] = 0.80; + c.tp1_pct[1] = 60; c.tp2_pct[1] = 25; c.tp3_pct[1] = 15; + c.max_cost_pct[1] = 38.0; + c.allow_tc[1] = -1; + c.req_kz[1] = 1; + c.req_trend[1] = -1; + c.max_positions[1] = 1; + c.se_peak_th1[1] = 0.65; c.se_peak_th2[1] = 0.74; c.se_peak_th3[1] = 0.80; + c.d1choch_gate[1] = 0; + + // ── H1 [index 2] + c.sl[2] = 2.10; c.tp1[2] = 3.20; + c.tp2[2] = 4.30; c.tp3[2] = 5.50; + c.mrr[2] = 1.50; c.risk[2] = 1.00; + c.sl_min[2] = 9.0; c.spread[2] = 15.0; + c.min_conf[2] = 52.0; c.min_ev[2] = 0.07; + c.se_minrr[2] = 1.00; + c.tp1_pct[2] = 55; c.tp2_pct[2] = 25; c.tp3_pct[2] = 20; + c.max_cost_pct[2] = 35.0; + c.allow_tc[2] = -1; + c.req_kz[2] = 0; + c.req_trend[2] = 0; + c.max_positions[2] = 1; // H1: single position (multi-TP split wastes 3× risk per trade) + c.se_peak_th1[2] = 0.75; c.se_peak_th2[2] = 0.80; c.se_peak_th3[2] = 0.84; + c.d1choch_gate[2] = 0; + + // ── H4 [index 3] + c.sl[3] = 2.30; c.tp1[3] = 4.20; + c.tp2[3] = 5.70; c.tp3[3] = 7.30; + c.mrr[3] = 1.80; c.risk[3] = 1.30; + c.sl_min[3] = 15.0; c.spread[3] = 35.0; + c.min_conf[3] = 46.0; c.min_ev[3] = 0.00; + c.se_minrr[3] = 1.30; + c.tp1_pct[3] = 60; c.tp2_pct[3] = 25; c.tp3_pct[3] = 15; + c.max_cost_pct[3] = 20.0; + c.allow_tc[3] = 0; + c.req_kz[3] = 0; + c.req_trend[3] = 0; + c.max_positions[3] = 0; + c.se_peak_th1[3] = 0.74; c.se_peak_th2[3] = 0.78; c.se_peak_th3[3] = 0.82; + c.d1choch_gate[3] = 0; + + // ── D1 [index 4] + c.sl[4] = 2.90; c.tp1[4] = 5.80; + c.tp2[4] = 7.80; c.tp3[4] = 10.20; + c.mrr[4] = 1.90; c.risk[4] = 1.85; + c.sl_min[4] = 28.0; c.spread[4] = 70.0; + c.min_conf[4] = 42.0; c.min_ev[4] = 0.00; + c.se_minrr[4] = 1.60; + c.tp1_pct[4] = 60; c.tp2_pct[4] = 25; c.tp3_pct[4] = 15; + c.max_cost_pct[4] = 12.0; + c.allow_tc[4] = 0; + c.req_kz[4] = 0; + c.req_trend[4] = 0; + c.max_positions[4] = 0; + c.se_peak_th1[4] = 0.52; c.se_peak_th2[4] = 0.57; c.se_peak_th3[4] = 0.67; + c.d1choch_gate[4] = 1; + + } + // ── NON-JPY CROSSES ────────────────────────────────────────── + else if(sym == "EURGBP" || sym == "EURAUD" || sym == "EURCAD" || sym == "EURNZD" || + sym == "GBPAUD" || sym == "GBPCAD" || sym == "GBPNZD" || sym == "AUDCAD" || + sym == "AUDNZD" || sym == "AUDCHF" || sym == "NZDCAD" || sym == "NZDCHF" || sym == "CADCHF") + { + c.comm = 7.0; + c.tr_bonus = 1.25; + c.rng_pen = 0.75; + c.min_wp = 40.0; + c.category = "Cross"; + + // ── M5 [index 0] + c.sl[0] = 1.20; c.tp1[0] = 1.80; + c.tp2[0] = 2.30; c.tp3[0] = 2.80; + c.mrr[0] = 1.10; c.risk[0] = 0.50; + c.sl_min[0] = 3.0; c.spread[0] = 5.0; + c.min_conf[0] = 45.0; c.min_ev[0] = 0.10; + c.se_minrr[0] = 0.50; + c.tp1_pct[0] = 60; c.tp2_pct[0] = 25; c.tp3_pct[0] = 15; + c.max_cost_pct[0] = 30.0; + c.allow_tc[0] = -1; + c.req_kz[0] = 1; + c.req_trend[0] = -1; + c.max_positions[0] = 1; + c.se_peak_th1[0] = 0.60; c.se_peak_th2[0] = 0.72; c.se_peak_th3[0] = 0.78; + c.d1choch_gate[0] = 0; + + // ── M15 [index 1] + c.sl[1] = 1.80; c.tp1[1] = 2.80; + c.tp2[1] = 3.70; c.tp3[1] = 4.70; + c.mrr[1] = 1.40; c.risk[1] = 0.80; + c.sl_min[1] = 5.0; c.spread[1] = 5.0; + c.min_conf[1] = 60.0; c.min_ev[1] = 0.10; + c.se_minrr[1] = 0.70; + c.tp1_pct[1] = 60; c.tp2_pct[1] = 25; c.tp3_pct[1] = 15; + c.max_cost_pct[1] = 32.0; + c.allow_tc[1] = -1; + c.req_kz[1] = 1; + c.req_trend[1] = -1; + c.max_positions[1] = 1; + c.se_peak_th1[1] = 0.60; c.se_peak_th2[1] = 0.72; c.se_peak_th3[1] = 0.78; + c.d1choch_gate[1] = 0; + + // ── H1 [index 2] + c.sl[2] = 2.00; c.tp1[2] = 3.20; + c.tp2[2] = 4.20; c.tp3[2] = 5.50; + c.mrr[2] = 1.50; c.risk[2] = 1.00; + c.sl_min[2] = 7.0; c.spread[2] = 12.0; + c.min_conf[2] = 50.0; c.min_ev[2] = 0.07; + c.se_minrr[2] = 0.80; + c.tp1_pct[2] = 50; c.tp2_pct[2] = 30; c.tp3_pct[2] = 20; + c.max_cost_pct[2] = 30.0; + c.allow_tc[2] = -1; + c.req_kz[2] = 0; + c.req_trend[2] = 0; + c.max_positions[2] = 1; // H1: single position (multi-TP split wastes 3× risk per trade) + c.se_peak_th1[2] = 0.72; c.se_peak_th2[2] = 0.78; c.se_peak_th3[2] = 0.82; + c.d1choch_gate[2] = 0; + + // ── H4 [index 3] + c.sl[3] = 2.20; c.tp1[3] = 4.00; + c.tp2[3] = 5.30; c.tp3[3] = 6.80; + c.mrr[3] = 1.60; c.risk[3] = 1.20; + c.sl_min[3] = 12.0; c.spread[3] = 25.0; + c.min_conf[3] = 44.0; c.min_ev[3] = 0.00; + c.se_minrr[3] = 1.20; + c.tp1_pct[3] = 60; c.tp2_pct[3] = 25; c.tp3_pct[3] = 15; + c.max_cost_pct[3] = 18.0; + c.allow_tc[3] = 0; + c.req_kz[3] = 0; + c.req_trend[3] = 0; + c.max_positions[3] = 0; + c.se_peak_th1[3] = 0.72; c.se_peak_th2[3] = 0.76; c.se_peak_th3[3] = 0.80; + c.d1choch_gate[3] = 0; + + // ── D1 [index 4] + c.sl[4] = 2.80; c.tp1[4] = 5.50; + c.tp2[4] = 7.50; c.tp3[4] = 10.00; + c.mrr[4] = 1.80; c.risk[4] = 1.80; + c.sl_min[4] = 22.0; c.spread[4] = 50.0; + c.min_conf[4] = 40.0; c.min_ev[4] = 0.00; + c.se_minrr[4] = 1.50; + c.tp1_pct[4] = 60; c.tp2_pct[4] = 25; c.tp3_pct[4] = 15; + c.max_cost_pct[4] = 12.0; + c.allow_tc[4] = 0; + c.req_kz[4] = 0; + c.req_trend[4] = 0; + c.max_positions[4] = 0; + c.se_peak_th1[4] = 0.50; c.se_peak_th2[4] = 0.55; c.se_peak_th3[4] = 0.65; + c.d1choch_gate[4] = 1; + + } + // ── METALS ─────────────────────────────────────────────────── + else if(sym == "XAUUSD" || StringFind(sym,"GOLD")>=0) + { + c.comm = 15.0; + c.tr_bonus = 1.4; + c.rng_pen = 0.5; + c.min_wp = 35.0; + c.category = "Metal"; + + // ── M5 [index 0] + c.sl[0] = 1.20; c.tp1[0] = 1.80; + c.tp2[0] = 2.30; c.tp3[0] = 2.80; + c.mrr[0] = 1.10; c.risk[0] = 0.50; + c.sl_min[0] = 20.0; c.spread[0] = 50.0; + c.min_conf[0] = 48.0; c.min_ev[0] = 0.00; + c.se_minrr[0] = 0.60; + c.tp1_pct[0] = 75; c.tp2_pct[0] = 20; c.tp3_pct[0] = 5; + c.max_cost_pct[0] = 20.0; + c.allow_tc[0] = -1; + c.req_kz[0] = 0; + c.req_trend[0] = 0; + c.max_positions[0] = 1; + c.se_peak_th1[0] = 0.55; c.se_peak_th2[0] = 0.68; c.se_peak_th3[0] = 0.75; + c.d1choch_gate[0] = 0; + + // ── M15 [index 1] + c.sl[1] = 1.80; c.tp1[1] = 2.80; + c.tp2[1] = 4.00; c.tp3[1] = 5.50; + c.mrr[1] = 1.30; c.risk[1] = 0.80; + c.sl_min[1] = 30.0; c.spread[1] = 50.0; + c.min_conf[1] = 62.0; c.min_ev[1] = 0.00; + c.se_minrr[1] = 0.80; + c.tp1_pct[1] = 75; c.tp2_pct[1] = 20; c.tp3_pct[1] = 5; + c.max_cost_pct[1] = 20.0; + c.allow_tc[1] = -1; + c.req_kz[1] = 0; + c.req_trend[1] = 0; + c.max_positions[1] = 1; + c.se_peak_th1[1] = 0.58; c.se_peak_th2[1] = 0.68; c.se_peak_th3[1] = 0.75; + c.d1choch_gate[1] = 0; + + // ── H1 [index 2] + // FIX#514: XAUUSD H1 calibration — full choppy-protection + exit refinement. + // ROOT: min_conf=52 allowed borderline setups; se_minrr=1.00 held trades too long + // past peak; allow_tc=0 let TC entries fire in choppy Gold sessions; + // choppy_min_conf/block_tc_choppy/choppy_lot_mult were missing (0=disabled) + // leaving the choppy regime with no extra filter on XAUUSD H1. + // Baseline: PF=2.57, WR=62.5%, DD=2.28% (16 trades, XAUUSD H1). + c.sl[2] = 2.00; c.tp1[2] = 3.00; + c.tp2[2] = 4.50; c.tp3[2] = 7.00; + c.mrr[2] = 1.30; c.risk[2] = 1.00; + c.sl_min[2] = 50.0; c.spread[2] = 60.0; + c.min_conf[2] = 55.0; c.min_ev[2] = 0.00; // raised 52→55: filter borderline setups + c.se_minrr[2] = 0.80; // lowered 1.00→0.80: SmartExit fires earlier + c.se_override_rr[2] = 0.90; // 2-cat early close at 0.90R (captures 0.97R near-misses) + c.tp1_pct[2] = 70; c.tp2_pct[2] = 20; c.tp3_pct[2] = 10; + c.max_cost_pct[2] = 18.0; + c.allow_tc[2] = -1; // force TC off: Gold H1 TC WR unreliable in session chop + c.req_kz[2] = 0; + c.req_trend[2] = 0; + c.max_positions[2] = 1; + c.se_peak_th1[2] = 0.65; c.se_peak_th2[2] = 0.76; c.se_peak_th3[2] = 0.80; // th1: 0.70→0.65 + c.d1choch_gate[2] = 0; + // Choppy regime protection — was 0 (disabled) for XAUUSD H1 + c.choppy_min_conf[2] = 58.0; // require higher score in choppy/ranging conditions + c.block_tc_choppy[2] = 1; // TC blocked in choppy regardless of allow_tc + c.choppy_lot_mult[2] = 0.65; // 35% lot reduction in choppy regime + c.mtf_hard_block[2] = 0; // soft penalty only (hard block → deadlock on Gold) + // SmartExit RSI calibration: Gold is more volatile than Forex + // Default 70/30 fires too early on Gold's normal momentum swings + c.se_rsi_ob[2] = 74.0; c.se_rsi_os[2] = 26.0; + c.se_mom_ratio[2] = 0.65; // Gold momentum fades faster: <65% body vs prev = signal + c.se_mom_score_mult[2] = 1.60; // stronger momentum multiplier for volatile Gold moves + + // ── H4 [index 3] + c.sl[3] = 2.50; c.tp1[3] = 3.50; + c.tp2[3] = 5.00; c.tp3[3] = 8.00; + c.mrr[3] = 1.30; c.risk[3] = 1.00; + c.sl_min[3] = 80.0; c.spread[3] = 80.0; + c.min_conf[3] = 46.0; c.min_ev[3] = 0.00; + c.se_minrr[3] = 1.20; + c.tp1_pct[3] = 75; c.tp2_pct[3] = 20; c.tp3_pct[3] = 5; + c.max_cost_pct[3] = 12.0; + c.allow_tc[3] = 0; + c.req_kz[3] = 0; + c.req_trend[3] = 0; + c.max_positions[3] = 0; + c.se_peak_th1[3] = 0.72; c.se_peak_th2[3] = 0.74; c.se_peak_th3[3] = 0.78; + c.d1choch_gate[3] = 0; + + // ── D1 [index 4] + c.sl[4] = 3.00; c.tp1[4] = 5.00; + c.tp2[4] = 7.00; c.tp3[4] = 9.00; + c.mrr[4] = 1.60; c.risk[4] = 2.00; + c.sl_min[4] = 150.0; c.spread[4] = 150.0; + c.min_conf[4] = 42.0; c.min_ev[4] = 0.00; + c.se_minrr[4] = 1.50; + c.tp1_pct[4] = 70; c.tp2_pct[4] = 20; c.tp3_pct[4] = 10; + c.max_cost_pct[4] = 8.0; + c.allow_tc[4] = 0; + c.req_kz[4] = 0; + c.req_trend[4] = 0; + c.max_positions[4] = 0; + c.se_peak_th1[4] = 0.48; c.se_peak_th2[4] = 0.53; c.se_peak_th3[4] = 0.62; + c.d1choch_gate[4] = 1; + + // XAUUSD M5: FVG and OB zones are 2-5p — spread alone fills them + c.allow_fvg[0] = -1; + c.allow_ob[0] = -1; + } + else if(sym == "XAGUSD" || StringFind(sym,"SILVER")>=0) + { + c.comm = 12.0; + c.tr_bonus = 1.35; + c.rng_pen = 0.55; + c.min_wp = 38.0; + c.category = "Metal"; + + // ── M5 [index 0] + c.sl[0] = 1.50; c.tp1[0] = 1.80; + c.tp2[0] = 2.50; c.tp3[0] = 3.50; + c.mrr[0] = 1.20; c.risk[0] = 0.30; + c.sl_min[0] = 15.0; c.spread[0] = 50.0; + c.min_conf[0] = 48.0; c.min_ev[0] = 0.00; + c.se_minrr[0] = 0.60; + c.tp1_pct[0] = 75; c.tp2_pct[0] = 20; c.tp3_pct[0] = 5; + c.max_cost_pct[0] = 20.0; + c.allow_tc[0] = -1; + c.req_kz[0] = 0; + c.req_trend[0] = 0; + c.max_positions[0] = 1; + c.se_peak_th1[0] = 0.55; c.se_peak_th2[0] = 0.68; c.se_peak_th3[0] = 0.75; + c.d1choch_gate[0] = 0; + + // ── M15 [index 1] + c.sl[1] = 2.00; c.tp1[1] = 2.80; + c.tp2[1] = 3.80; c.tp3[1] = 5.00; + c.mrr[1] = 1.40; c.risk[1] = 0.50; + c.sl_min[1] = 25.0; c.spread[1] = 60.0; + c.min_conf[1] = 62.0; c.min_ev[1] = 0.00; + c.se_minrr[1] = 0.80; + c.tp1_pct[1] = 75; c.tp2_pct[1] = 20; c.tp3_pct[1] = 5; + c.max_cost_pct[1] = 20.0; + c.allow_tc[1] = -1; + c.req_kz[1] = 0; + c.req_trend[1] = 0; + c.max_positions[1] = 1; + c.se_peak_th1[1] = 0.58; c.se_peak_th2[1] = 0.68; c.se_peak_th3[1] = 0.75; + c.d1choch_gate[1] = 0; + + // ── H1 [index 2] + c.sl[2] = 2.50; c.tp1[2] = 3.60; + c.tp2[2] = 4.50; c.tp3[2] = 6.50; + c.mrr[2] = 1.40; c.risk[2] = 0.80; + c.sl_min[2] = 40.0; c.spread[2] = 80.0; + c.min_conf[2] = 52.0; c.min_ev[2] = 0.00; + c.se_minrr[2] = 1.00; + c.tp1_pct[2] = 70; c.tp2_pct[2] = 20; c.tp3_pct[2] = 10; + c.max_cost_pct[2] = 18.0; + c.allow_tc[2] = 0; + c.req_kz[2] = 0; + c.req_trend[2] = 0; + c.max_positions[2] = 1; // H1: single position (multi-TP split wastes 3× risk per trade) + c.se_peak_th1[2] = 0.70; c.se_peak_th2[2] = 0.76; c.se_peak_th3[2] = 0.80; + c.d1choch_gate[2] = 0; + + // ── H4 [index 3] + c.sl[3] = 2.50; c.tp1[3] = 3.80; + c.tp2[3] = 5.00; c.tp3[3] = 7.50; + c.mrr[3] = 1.40; c.risk[3] = 1.00; + c.sl_min[3] = 70.0; c.spread[3] = 120.0; + c.min_conf[3] = 46.0; c.min_ev[3] = 0.00; + c.se_minrr[3] = 1.20; + c.tp1_pct[3] = 75; c.tp2_pct[3] = 20; c.tp3_pct[3] = 5; + c.max_cost_pct[3] = 12.0; + c.allow_tc[3] = 0; + c.req_kz[3] = 0; + c.req_trend[3] = 0; + c.max_positions[3] = 0; + c.se_peak_th1[3] = 0.72; c.se_peak_th2[3] = 0.74; c.se_peak_th3[3] = 0.78; + c.d1choch_gate[3] = 0; + + // ── D1 [index 4] + c.sl[4] = 3.50; c.tp1[4] = 5.60; + c.tp2[4] = 7.50; c.tp3[4] = 10.00; + c.mrr[4] = 1.55; c.risk[4] = 1.20; + c.sl_min[4] = 120.0; c.spread[4] = 200.0; + c.min_conf[4] = 42.0; c.min_ev[4] = 0.00; + c.se_minrr[4] = 1.50; + c.tp1_pct[4] = 70; c.tp2_pct[4] = 20; c.tp3_pct[4] = 10; + c.max_cost_pct[4] = 8.0; + c.allow_tc[4] = 0; + c.req_kz[4] = 0; + c.req_trend[4] = 0; + c.max_positions[4] = 0; + c.se_peak_th1[4] = 0.48; c.se_peak_th2[4] = 0.53; c.se_peak_th3[4] = 0.62; + c.d1choch_gate[4] = 1; + + } + // ── US INDICES ─────────────────────────────────────────────── + else if(sym == "US500" || sym == "US500." || sym == "US500C" || sym == "US500CA" || // US500.cash→US500 via dot-strip + sym == "SP500" || sym == "SPXUSD" || sym == "SPX500" || + sym == "US30" || sym == "DOW30" || sym == "DJI30") + { + c.comm = 10.0; + c.tr_bonus = 1.25; + c.rng_pen = 0.6; + c.min_wp = 40.0; + c.category = "Index"; + + // ── M5 [index 0] + c.sl[0] = 1.20; c.tp1[0] = 1.80; + c.tp2[0] = 2.30; c.tp3[0] = 2.80; + c.mrr[0] = 1.10; c.risk[0] = 0.50; + c.sl_min[0] = 25.0; c.spread[0] = 70.0; + c.min_conf[0] = 45.0; c.min_ev[0] = 0.00; + c.se_minrr[0] = 0.60; + c.tp1_pct[0] = 60; c.tp2_pct[0] = 25; c.tp3_pct[0] = 15; + c.max_cost_pct[0] = 18.0; + c.max_positions[0] = 1; + c.se_peak_th1[0] = 0.60; c.se_peak_th2[0] = 0.72; c.se_peak_th3[0] = 0.78; + c.d1choch_gate[0] = 0; + + // ── M15 [index 1] + c.sl[1] = 2.00; c.tp1[1] = 3.00; + c.tp2[1] = 4.00; c.tp3[1] = 5.00; + c.mrr[1] = 1.30; c.risk[1] = 1.00; + c.sl_min[1] = 40.0; c.spread[1] = 70.0; + c.min_conf[1] = 55.0; c.min_ev[1] = 0.00; + c.se_minrr[1] = 0.80; + c.tp1_pct[1] = 60; c.tp2_pct[1] = 25; c.tp3_pct[1] = 15; + c.max_cost_pct[1] = 18.0; + c.max_positions[1] = 1; + c.se_peak_th1[1] = 0.60; c.se_peak_th2[1] = 0.72; c.se_peak_th3[1] = 0.78; + c.d1choch_gate[1] = 0; + + // ── H1 [index 2] + c.sl[2] = 2.00; c.tp1[2] = 3.00; + c.tp2[2] = 4.00; c.tp3[2] = 5.00; + c.mrr[2] = 1.50; c.risk[2] = 1.00; + c.sl_min[2] = 70.0; c.spread[2] = 70.0; + c.min_conf[2] = 50.0; c.min_ev[2] = 0.00; + c.se_minrr[2] = 1.00; + c.tp1_pct[2] = 60; c.tp2_pct[2] = 25; c.tp3_pct[2] = 15; + c.max_cost_pct[2] = 15.0; + c.max_positions[2] = 1; // H1: single position (multi-TP split wastes 3× risk per trade) + c.se_peak_th1[2] = 0.72; c.se_peak_th2[2] = 0.78; c.se_peak_th3[2] = 0.82; + c.d1choch_gate[2] = 0; + + // ── H4 [index 3] + c.sl[3] = 2.00; c.tp1[3] = 3.60; + c.tp2[3] = 4.80; c.tp3[3] = 6.20; + c.mrr[3] = 1.55; c.risk[3] = 1.50; + c.sl_min[3] = 120.0; c.spread[3] = 70.0; + c.min_conf[3] = 44.0; c.min_ev[3] = 0.00; + c.se_minrr[3] = 1.20; + c.tp1_pct[3] = 60; c.tp2_pct[3] = 25; c.tp3_pct[3] = 15; + c.max_cost_pct[3] = 10.0; + c.max_positions[3] = 0; + c.se_peak_th1[3] = 0.72; c.se_peak_th2[3] = 0.76; c.se_peak_th3[3] = 0.80; + c.d1choch_gate[3] = 0; + + // ── D1 [index 4] + c.sl[4] = 3.00; c.tp1[4] = 5.00; + c.tp2[4] = 6.80; c.tp3[4] = 9.00; + c.mrr[4] = 1.60; c.risk[4] = 2.00; + c.sl_min[4] = 200.0; c.spread[4] = 70.0; + c.min_conf[4] = 40.0; c.min_ev[4] = 0.00; + c.se_minrr[4] = 1.50; + c.tp1_pct[4] = 60; c.tp2_pct[4] = 25; c.tp3_pct[4] = 15; + c.max_cost_pct[4] = 7.0; + c.max_positions[4] = 0; + c.se_peak_th1[4] = 0.50; c.se_peak_th2[4] = 0.55; c.se_peak_th3[4] = 0.65; + c.d1choch_gate[4] = 1; + + } + else if(sym == "US100" || sym == "US100." || sym == "US100C" || sym == "US100CA" || // US100.cash variants + sym == "NAS100" || sym == "NASDAQ" || sym == "USTECH" || sym == "USTEC" || sym == "NASUSD") + { + c.comm = 12.0; + c.tr_bonus = 1.3; + c.rng_pen = 0.65; + c.min_wp = 40.0; + c.category = "Index"; + + // ── M5 [index 0] + c.sl[0] = 1.20; c.tp1[0] = 1.80; + c.tp2[0] = 2.30; c.tp3[0] = 2.80; + c.mrr[0] = 1.10; c.risk[0] = 0.50; + c.sl_min[0] = 30.0; c.spread[0] = 250.0; + c.min_conf[0] = 45.0; c.min_ev[0] = 0.00; + c.se_minrr[0] = 0.60; + c.tp1_pct[0] = 60; c.tp2_pct[0] = 25; c.tp3_pct[0] = 15; + c.max_cost_pct[0] = 18.0; + c.max_positions[0] = 1; + c.se_peak_th1[0] = 0.60; c.se_peak_th2[0] = 0.72; c.se_peak_th3[0] = 0.78; + c.d1choch_gate[0] = 0; + + // ── M15 [index 1] + c.sl[1] = 2.20; c.tp1[1] = 3.50; + c.tp2[1] = 4.50; c.tp3[1] = 5.50; + c.mrr[1] = 1.30; c.risk[1] = 0.75; + c.sl_min[1] = 50.0; c.spread[1] = 250.0; + c.min_conf[1] = 55.0; c.min_ev[1] = 0.00; + c.se_minrr[1] = 0.80; + c.tp1_pct[1] = 60; c.tp2_pct[1] = 25; c.tp3_pct[1] = 15; + c.max_cost_pct[1] = 18.0; + c.max_positions[1] = 1; + c.se_peak_th1[1] = 0.60; c.se_peak_th2[1] = 0.72; c.se_peak_th3[1] = 0.78; + c.d1choch_gate[1] = 0; + + // ── H1 [index 2] + c.sl[2] = 2.20; c.tp1[2] = 3.50; + c.tp2[2] = 4.50; c.tp3[2] = 5.50; + c.mrr[2] = 1.50; c.risk[2] = 0.75; + c.sl_min[2] = 90.0; c.spread[2] = 250.0; + c.min_conf[2] = 50.0; c.min_ev[2] = 0.00; + c.se_minrr[2] = 1.00; + c.tp1_pct[2] = 60; c.tp2_pct[2] = 25; c.tp3_pct[2] = 15; + c.max_cost_pct[2] = 15.0; + c.max_positions[2] = 1; // H1: single position (multi-TP split wastes 3× risk per trade) + c.se_peak_th1[2] = 0.72; c.se_peak_th2[2] = 0.78; c.se_peak_th3[2] = 0.82; + c.d1choch_gate[2] = 0; + + // ── H4 [index 3] + c.sl[3] = 2.20; c.tp1[3] = 4.00; + c.tp2[3] = 5.30; c.tp3[3] = 6.80; + c.mrr[3] = 1.55; c.risk[3] = 0.75; + c.sl_min[3] = 150.0; c.spread[3] = 250.0; + c.min_conf[3] = 44.0; c.min_ev[3] = 0.00; + c.se_minrr[3] = 1.20; + c.tp1_pct[3] = 60; c.tp2_pct[3] = 25; c.tp3_pct[3] = 15; + c.max_cost_pct[3] = 10.0; + c.max_positions[3] = 0; + c.se_peak_th1[3] = 0.72; c.se_peak_th2[3] = 0.76; c.se_peak_th3[3] = 0.80; + c.d1choch_gate[3] = 0; + + // ── D1 [index 4] + c.sl[4] = 3.00; c.tp1[4] = 5.00; + c.tp2[4] = 6.80; c.tp3[4] = 9.00; + c.mrr[4] = 1.60; c.risk[4] = 2.00; + c.sl_min[4] = 250.0; c.spread[4] = 250.0; + c.min_conf[4] = 40.0; c.min_ev[4] = 0.00; + c.se_minrr[4] = 1.50; + c.tp1_pct[4] = 60; c.tp2_pct[4] = 25; c.tp3_pct[4] = 15; + c.max_cost_pct[4] = 7.0; + c.max_positions[4] = 0; + c.se_peak_th1[4] = 0.50; c.se_peak_th2[4] = 0.55; c.se_peak_th3[4] = 0.65; + c.d1choch_gate[4] = 1; + + } + // ── EU/UK INDICES ──────────────────────────────────────────── + else if(sym == "UK100" || sym == "FTSE" || sym == "UKX" || + sym == "GERMAN"|| sym == "GER30" || sym == "DAX30" || sym == "DE30" || + sym == "FRANCE"|| sym == "FRA40" || sym == "CAC40" || sym == "FR40") + { + c.comm = 5.0; + c.tr_bonus = 1.22; + c.rng_pen = 0.65; + c.min_wp = 40.0; + c.category = "Index"; + + // ── M5 [index 0] + c.sl[0] = 1.30; c.tp1[0] = 2.00; + c.tp2[0] = 2.70; c.tp3[0] = 3.30; + c.mrr[0] = 1.50; c.risk[0] = 0.50; + c.sl_min[0] = 25.0; c.spread[0] = 20.0; + c.min_conf[0] = 45.0; c.min_ev[0] = 0.00; + c.se_minrr[0] = 0.60; + c.tp1_pct[0] = 60; c.tp2_pct[0] = 25; c.tp3_pct[0] = 15; + c.max_cost_pct[0] = 18.0; + c.max_positions[0] = 1; + c.se_peak_th1[0] = 0.60; c.se_peak_th2[0] = 0.72; c.se_peak_th3[0] = 0.78; + c.d1choch_gate[0] = 0; + + // ── M15 [index 1] + c.sl[1] = 1.80; c.tp1[1] = 2.80; + c.tp2[1] = 3.80; c.tp3[1] = 4.80; + c.mrr[1] = 1.40; c.risk[1] = 0.70; + c.sl_min[1] = 40.0; c.spread[1] = 20.0; + c.min_conf[1] = 55.0; c.min_ev[1] = 0.00; + c.se_minrr[1] = 0.80; + c.tp1_pct[1] = 60; c.tp2_pct[1] = 25; c.tp3_pct[1] = 15; + c.max_cost_pct[1] = 18.0; + c.max_positions[1] = 1; + c.se_peak_th1[1] = 0.60; c.se_peak_th2[1] = 0.72; c.se_peak_th3[1] = 0.78; + c.d1choch_gate[1] = 0; + + // ── H1 [index 2] + c.sl[2] = 2.00; c.tp1[2] = 3.00; + c.tp2[2] = 4.00; c.tp3[2] = 5.50; + c.mrr[2] = 1.50; c.risk[2] = 1.00; + c.sl_min[2] = 70.0; c.spread[2] = 40.0; + c.min_conf[2] = 50.0; c.min_ev[2] = 0.00; + c.se_minrr[2] = 1.00; + c.tp1_pct[2] = 60; c.tp2_pct[2] = 25; c.tp3_pct[2] = 15; + c.max_cost_pct[2] = 15.0; + c.max_positions[2] = 1; // H1: single position (multi-TP split wastes 3× risk per trade) + c.se_peak_th1[2] = 0.72; c.se_peak_th2[2] = 0.78; c.se_peak_th3[2] = 0.82; + c.d1choch_gate[2] = 0; + + // ── H4 [index 3] + c.sl[3] = 2.00; c.tp1[3] = 3.60; + c.tp2[3] = 4.90; c.tp3[3] = 6.50; + c.mrr[3] = 1.80; c.risk[3] = 1.00; + c.sl_min[3] = 120.0; c.spread[3] = 80.0; + c.min_conf[3] = 44.0; c.min_ev[3] = 0.00; + c.se_minrr[3] = 1.20; + c.tp1_pct[3] = 60; c.tp2_pct[3] = 25; c.tp3_pct[3] = 15; + c.max_cost_pct[3] = 10.0; + c.max_positions[3] = 0; + c.se_peak_th1[3] = 0.72; c.se_peak_th2[3] = 0.76; c.se_peak_th3[3] = 0.80; + c.d1choch_gate[3] = 0; + + // ── D1 [index 4] + c.sl[4] = 2.50; c.tp1[4] = 5.00; + c.tp2[4] = 6.80; c.tp3[4] = 9.00; + c.mrr[4] = 1.90; c.risk[4] = 1.60; + c.sl_min[4] = 200.0; c.spread[4] = 150.0; + c.min_conf[4] = 40.0; c.min_ev[4] = 0.00; + c.se_minrr[4] = 1.50; + c.tp1_pct[4] = 60; c.tp2_pct[4] = 25; c.tp3_pct[4] = 15; + c.max_cost_pct[4] = 7.0; + c.max_positions[4] = 0; + c.se_peak_th1[4] = 0.50; c.se_peak_th2[4] = 0.55; c.se_peak_th3[4] = 0.65; + c.d1choch_gate[4] = 1; + + } + // ── CRYPTO ─────────────────────────────────────────────────── + else if(sym == "BTCUSD" || sym == "BITCOIN" || StringFind(sym,"BTC")>=0) + { + c.comm = 20.0; + c.tr_bonus = 1.5; + c.rng_pen = 0.45; + c.min_wp = 38.0; + c.category = "Crypto"; + + // ── M5 [index 0] + c.sl[0] = 1.50; c.tp1[0] = 1.80; + c.tp2[0] = 2.40; c.tp3[0] = 3.00; + c.mrr[0] = 1.20; c.risk[0] = 0.30; + c.sl_min[0] = 200.0; c.spread[0] = 60.0; + c.min_conf[0] = 48.0; c.min_ev[0] = 0.00; + c.se_minrr[0] = 0.60; + c.tp1_pct[0] = 70; c.tp2_pct[0] = 20; c.tp3_pct[0] = 10; + c.max_cost_pct[0] = 15.0; + c.max_positions[0] = 1; + c.se_peak_th1[0] = 0.50; c.se_peak_th2[0] = 0.65; c.se_peak_th3[0] = 0.72; + c.d1choch_gate[0] = 0; + + // ── M15 [index 1] + c.sl[1] = 2.00; c.tp1[1] = 2.80; + c.tp2[1] = 3.80; c.tp3[1] = 4.80; + c.mrr[1] = 1.40; c.risk[1] = 0.40; + c.sl_min[1] = 300.0; c.spread[1] = 60.0; + c.min_conf[1] = 58.0; c.min_ev[1] = 0.00; + c.se_minrr[1] = 0.80; + c.tp1_pct[1] = 70; c.tp2_pct[1] = 20; c.tp3_pct[1] = 10; + c.max_cost_pct[1] = 15.0; + c.max_positions[1] = 1; + c.se_peak_th1[1] = 0.55; c.se_peak_th2[1] = 0.65; c.se_peak_th3[1] = 0.72; + c.d1choch_gate[1] = 0; + + // ── H1 [index 2] + c.sl[2] = 2.50; c.tp1[2] = 3.50; + c.tp2[2] = 4.80; c.tp3[2] = 6.30; + c.mrr[2] = 1.40; c.risk[2] = 0.50; + c.sl_min[2] = 500.0; c.spread[2] = 100.0; + c.min_conf[2] = 52.0; c.min_ev[2] = 0.00; + c.se_minrr[2] = 1.00; + c.tp1_pct[2] = 65; c.tp2_pct[2] = 25; c.tp3_pct[2] = 10; + c.max_cost_pct[2] = 12.0; + c.max_positions[2] = 1; // H1: single position (multi-TP split wastes 3× risk per trade) + c.se_peak_th1[2] = 0.65; c.se_peak_th2[2] = 0.72; c.se_peak_th3[2] = 0.78; + c.d1choch_gate[2] = 0; + + // ── H4 [index 3] + c.sl[3] = 3.00; c.tp1[3] = 4.10; + c.tp2[3] = 5.60; c.tp3[3] = 7.50; + c.mrr[3] = 1.25; c.risk[3] = 0.50; + c.sl_min[3] = 800.0; c.spread[3] = 200.0; + c.min_conf[3] = 46.0; c.min_ev[3] = 0.00; + c.se_minrr[3] = 1.20; + c.tp1_pct[3] = 70; c.tp2_pct[3] = 20; c.tp3_pct[3] = 10; + c.max_cost_pct[3] = 8.0; + c.max_positions[3] = 0; + c.se_peak_th1[3] = 0.68; c.se_peak_th2[3] = 0.72; c.se_peak_th3[3] = 0.76; + c.d1choch_gate[3] = 0; + + // ── D1 [index 4] + c.sl[4] = 4.00; c.tp1[4] = 5.00; + c.tp2[4] = 6.80; c.tp3[4] = 9.50; + c.mrr[4] = 1.25; c.risk[4] = 0.80; + c.sl_min[4] = 1500.0; c.spread[4] = 400.0; + c.min_conf[4] = 42.0; c.min_ev[4] = 0.00; + c.se_minrr[4] = 1.50; + c.tp1_pct[4] = 70; c.tp2_pct[4] = 20; c.tp3_pct[4] = 10; + c.max_cost_pct[4] = 5.0; + c.max_positions[4] = 0; + c.se_peak_th1[4] = 0.45; c.se_peak_th2[4] = 0.50; c.se_peak_th3[4] = 0.58; + c.d1choch_gate[4] = 1; + + } + else if(sym == "ETHUSD" || sym == "ETHEREU" || StringFind(sym,"ETH")>=0) + { + c.comm = 18.0; + c.tr_bonus = 1.5; + c.rng_pen = 0.45; + c.min_wp = 38.0; + c.category = "Crypto"; + + // ── M5 [index 0] + c.sl[0] = 1.50; c.tp1[0] = 1.80; + c.tp2[0] = 2.40; c.tp3[0] = 3.00; + c.mrr[0] = 1.20; c.risk[0] = 0.30; + c.sl_min[0] = 150.0; c.spread[0] = 50.0; + c.min_conf[0] = 48.0; c.min_ev[0] = 0.00; + c.se_minrr[0] = 0.60; + c.tp1_pct[0] = 70; c.tp2_pct[0] = 20; c.tp3_pct[0] = 10; + c.max_cost_pct[0] = 15.0; + c.max_positions[0] = 1; + c.se_peak_th1[0] = 0.50; c.se_peak_th2[0] = 0.65; c.se_peak_th3[0] = 0.72; + c.d1choch_gate[0] = 0; + + // ── M15 [index 1] + c.sl[1] = 2.00; c.tp1[1] = 2.80; + c.tp2[1] = 3.80; c.tp3[1] = 4.80; + c.mrr[1] = 1.40; c.risk[1] = 0.40; + c.sl_min[1] = 250.0; c.spread[1] = 50.0; + c.min_conf[1] = 58.0; c.min_ev[1] = 0.00; + c.se_minrr[1] = 0.80; + c.tp1_pct[1] = 70; c.tp2_pct[1] = 20; c.tp3_pct[1] = 10; + c.max_cost_pct[1] = 15.0; + c.max_positions[1] = 1; + c.se_peak_th1[1] = 0.55; c.se_peak_th2[1] = 0.65; c.se_peak_th3[1] = 0.72; + c.d1choch_gate[1] = 0; + + // ── H1 [index 2] + c.sl[2] = 2.50; c.tp1[2] = 3.50; + c.tp2[2] = 4.80; c.tp3[2] = 6.30; + c.mrr[2] = 1.40; c.risk[2] = 0.50; + c.sl_min[2] = 400.0; c.spread[2] = 80.0; + c.min_conf[2] = 52.0; c.min_ev[2] = 0.00; + c.se_minrr[2] = 1.00; + c.tp1_pct[2] = 65; c.tp2_pct[2] = 25; c.tp3_pct[2] = 10; + c.max_cost_pct[2] = 12.0; + c.max_positions[2] = 1; // H1: single position (multi-TP split wastes 3× risk per trade) + c.se_peak_th1[2] = 0.65; c.se_peak_th2[2] = 0.72; c.se_peak_th3[2] = 0.78; + c.d1choch_gate[2] = 0; + + // ── H4 [index 3] + c.sl[3] = 3.00; c.tp1[3] = 4.10; + c.tp2[3] = 5.60; c.tp3[3] = 7.50; + c.mrr[3] = 1.25; c.risk[3] = 0.50; + c.sl_min[3] = 600.0; c.spread[3] = 150.0; + c.min_conf[3] = 46.0; c.min_ev[3] = 0.00; + c.se_minrr[3] = 1.20; + c.tp1_pct[3] = 70; c.tp2_pct[3] = 20; c.tp3_pct[3] = 10; + c.max_cost_pct[3] = 8.0; + c.max_positions[3] = 0; + c.se_peak_th1[3] = 0.68; c.se_peak_th2[3] = 0.72; c.se_peak_th3[3] = 0.76; + c.d1choch_gate[3] = 0; + + // ── D1 [index 4] + c.sl[4] = 4.00; c.tp1[4] = 5.00; + c.tp2[4] = 6.80; c.tp3[4] = 9.50; + c.mrr[4] = 1.25; c.risk[4] = 0.80; + c.sl_min[4] = 1000.0; c.spread[4] = 300.0; + c.min_conf[4] = 42.0; c.min_ev[4] = 0.00; + c.se_minrr[4] = 1.50; + c.tp1_pct[4] = 70; c.tp2_pct[4] = 20; c.tp3_pct[4] = 10; + c.max_cost_pct[4] = 5.0; + c.max_positions[4] = 0; + c.se_peak_th1[4] = 0.45; c.se_peak_th2[4] = 0.50; c.se_peak_th3[4] = 0.58; + c.d1choch_gate[4] = 1; + + } + // ── ENERGY ─────────────────────────────────────────────────── + else if(sym == "NATGAS" || sym == "NGAS" || sym == "XNGUSD" || sym == "NATGSC") + { + c.comm = 12.0; + c.tr_bonus = 1.3; + c.rng_pen = 0.55; + c.min_wp = 38.0; + c.category = "Energy"; + + // ── M5 [index 0] + c.sl[0] = 1.40; c.tp1[0] = 2.40; + c.tp2[0] = 3.20; c.tp3[0] = 4.00; + c.mrr[0] = 1.70; c.risk[0] = 0.40; + c.sl_min[0] = 20.0; c.spread[0] = 40.0; + c.min_conf[0] = 48.0; c.min_ev[0] = 0.00; + c.se_minrr[0] = 0.60; + c.tp1_pct[0] = 65; c.tp2_pct[0] = 25; c.tp3_pct[0] = 10; + c.max_cost_pct[0] = 20.0; + c.max_positions[0] = 1; + c.se_peak_th1[0] = 0.50; c.se_peak_th2[0] = 0.65; c.se_peak_th3[0] = 0.72; + c.d1choch_gate[0] = 0; + + // ── M15 [index 1] + c.sl[1] = 1.80; c.tp1[1] = 2.90; + c.tp2[1] = 3.90; c.tp3[1] = 5.00; + c.mrr[1] = 1.60; c.risk[1] = 0.50; + c.sl_min[1] = 30.0; c.spread[1] = 40.0; + c.min_conf[1] = 58.0; c.min_ev[1] = 0.00; + c.se_minrr[1] = 0.80; + c.tp1_pct[1] = 65; c.tp2_pct[1] = 25; c.tp3_pct[1] = 10; + c.max_cost_pct[1] = 20.0; + c.max_positions[1] = 1; + c.se_peak_th1[1] = 0.55; c.se_peak_th2[1] = 0.65; c.se_peak_th3[1] = 0.72; + c.d1choch_gate[1] = 0; + + // ── H1 [index 2] + c.sl[2] = 2.20; c.tp1[2] = 3.60; + c.tp2[2] = 4.80; c.tp3[2] = 6.20; + c.mrr[2] = 1.60; c.risk[2] = 0.70; + c.sl_min[2] = 50.0; c.spread[2] = 80.0; + c.min_conf[2] = 52.0; c.min_ev[2] = 0.00; + c.se_minrr[2] = 1.00; + c.tp1_pct[2] = 65; c.tp2_pct[2] = 25; c.tp3_pct[2] = 10; + c.max_cost_pct[2] = 18.0; + c.max_positions[2] = 1; // H1: single position (multi-TP split wastes 3× risk per trade) + c.se_peak_th1[2] = 0.65; c.se_peak_th2[2] = 0.72; c.se_peak_th3[2] = 0.78; + c.d1choch_gate[2] = 0; + + // ── H4 [index 3] + c.sl[3] = 2.50; c.tp1[3] = 4.60; + c.tp2[3] = 6.20; c.tp3[3] = 8.00; + c.mrr[3] = 1.60; c.risk[3] = 0.75; + c.sl_min[3] = 80.0; c.spread[3] = 150.0; + c.min_conf[3] = 46.0; c.min_ev[3] = 0.00; + c.se_minrr[3] = 1.20; + c.tp1_pct[3] = 65; c.tp2_pct[3] = 25; c.tp3_pct[3] = 10; + c.max_cost_pct[3] = 12.0; + c.max_positions[3] = 0; + c.se_peak_th1[3] = 0.68; c.se_peak_th2[3] = 0.72; c.se_peak_th3[3] = 0.76; + c.d1choch_gate[3] = 0; + + // ── D1 [index 4] + c.sl[4] = 3.00; c.tp1[4] = 5.00; + c.tp2[4] = 6.80; c.tp3[4] = 9.00; + c.mrr[4] = 1.60; c.risk[4] = 1.20; + c.sl_min[4] = 150.0; c.spread[4] = 250.0; + c.min_conf[4] = 42.0; c.min_ev[4] = 0.00; + c.se_minrr[4] = 1.50; + c.tp1_pct[4] = 65; c.tp2_pct[4] = 25; c.tp3_pct[4] = 10; + c.max_cost_pct[4] = 8.0; + c.max_positions[4] = 0; + c.se_peak_th1[4] = 0.45; c.se_peak_th2[4] = 0.50; c.se_peak_th3[4] = 0.58; + c.d1choch_gate[4] = 1; + + } + else if(sym == "USOIL" || sym == "XTIUSD" || sym == "WTIUSD" || sym == "CRUDOIL" || + sym == "OIL" || sym == "BRENT" || sym == "UKOIL") + { + c.comm = 10.0; + c.tr_bonus = 1.3; + c.rng_pen = 0.55; + c.min_wp = 38.0; + c.category = "Energy"; + + // ── M5 [index 0] + c.sl[0] = 1.40; c.tp1[0] = 2.30; + c.tp2[0] = 3.10; c.tp3[0] = 3.90; + c.mrr[0] = 1.60; c.risk[0] = 0.50; + c.sl_min[0] = 15.0; c.spread[0] = 30.0; + c.min_conf[0] = 48.0; c.min_ev[0] = 0.00; + c.se_minrr[0] = 0.60; + c.tp1_pct[0] = 65; c.tp2_pct[0] = 25; c.tp3_pct[0] = 10; + c.max_cost_pct[0] = 20.0; + c.max_positions[0] = 1; + c.se_peak_th1[0] = 0.50; c.se_peak_th2[0] = 0.65; c.se_peak_th3[0] = 0.72; + c.d1choch_gate[0] = 0; + + // ── M15 [index 1] + c.sl[1] = 1.80; c.tp1[1] = 2.80; + c.tp2[1] = 3.80; c.tp3[1] = 4.80; + c.mrr[1] = 1.50; c.risk[1] = 0.60; + c.sl_min[1] = 25.0; c.spread[1] = 30.0; + c.min_conf[1] = 58.0; c.min_ev[1] = 0.00; + c.se_minrr[1] = 0.80; + c.tp1_pct[1] = 65; c.tp2_pct[1] = 25; c.tp3_pct[1] = 10; + c.max_cost_pct[1] = 20.0; + c.max_positions[1] = 1; + c.se_peak_th1[1] = 0.55; c.se_peak_th2[1] = 0.65; c.se_peak_th3[1] = 0.72; + c.d1choch_gate[1] = 0; + + // ── H1 [index 2] + c.sl[2] = 2.20; c.tp1[2] = 3.40; + c.tp2[2] = 4.60; c.tp3[2] = 5.90; + c.mrr[2] = 1.50; c.risk[2] = 0.80; + c.sl_min[2] = 40.0; c.spread[2] = 60.0; + c.min_conf[2] = 52.0; c.min_ev[2] = 0.00; + c.se_minrr[2] = 1.00; + c.tp1_pct[2] = 65; c.tp2_pct[2] = 25; c.tp3_pct[2] = 10; + c.max_cost_pct[2] = 18.0; + c.max_positions[2] = 1; // H1: single position (multi-TP split wastes 3× risk per trade) + c.se_peak_th1[2] = 0.65; c.se_peak_th2[2] = 0.72; c.se_peak_th3[2] = 0.78; + c.d1choch_gate[2] = 0; + + // ── H4 [index 3] + c.sl[3] = 2.50; c.tp1[3] = 4.60; + c.tp2[3] = 6.20; c.tp3[3] = 8.00; + c.mrr[3] = 1.60; c.risk[3] = 0.75; + c.sl_min[3] = 70.0; c.spread[3] = 100.0; + c.min_conf[3] = 46.0; c.min_ev[3] = 0.00; + c.se_minrr[3] = 1.20; + c.tp1_pct[3] = 65; c.tp2_pct[3] = 25; c.tp3_pct[3] = 10; + c.max_cost_pct[3] = 12.0; + c.max_positions[3] = 0; + c.se_peak_th1[3] = 0.68; c.se_peak_th2[3] = 0.72; c.se_peak_th3[3] = 0.76; + c.d1choch_gate[3] = 0; + + // ── D1 [index 4] + c.sl[4] = 3.00; c.tp1[4] = 5.00; + c.tp2[4] = 6.80; c.tp3[4] = 9.00; + c.mrr[4] = 1.60; c.risk[4] = 1.20; + c.sl_min[4] = 120.0; c.spread[4] = 200.0; + c.min_conf[4] = 42.0; c.min_ev[4] = 0.00; + c.se_minrr[4] = 1.50; + c.tp1_pct[4] = 65; c.tp2_pct[4] = 25; c.tp3_pct[4] = 10; + c.max_cost_pct[4] = 8.0; + c.max_positions[4] = 0; + c.se_peak_th1[4] = 0.45; c.se_peak_th2[4] = 0.50; c.se_peak_th3[4] = 0.58; + c.d1choch_gate[4] = 1; + + } + // ── FALLBACK — unknown pair: conservative defaults ────────── + else + { + c.comm = 7.0; + c.tr_bonus = 1.2; + c.rng_pen = 0.8; + c.min_wp = 40.0; + c.category = "Unknown"; + + // ── M5 [index 0] + c.sl[0] = 1.20; c.tp1[0] = 1.80; + c.tp2[0] = 2.30; c.tp3[0] = 2.80; + c.mrr[0] = 1.10; c.risk[0] = 0.50; + c.sl_min[0] = 3.0; c.spread[0] = 10.0; + c.min_conf[0] = 45.0; c.min_ev[0] = 0.00; + c.se_minrr[0] = 0.50; + c.tp1_pct[0] = 60; c.tp2_pct[0] = 25; c.tp3_pct[0] = 15; + c.max_cost_pct[0] = 28.0; + c.max_positions[0] = 1; + c.se_peak_th1[0] = 0.60; c.se_peak_th2[0] = 0.72; c.se_peak_th3[0] = 0.78; + c.d1choch_gate[0] = 0; + + // ── M15 [index 1] + c.sl[1] = 1.80; c.tp1[1] = 2.80; + c.tp2[1] = 3.70; c.tp3[1] = 4.70; + c.mrr[1] = 1.30; c.risk[1] = 0.80; + c.sl_min[1] = 5.0; c.spread[1] = 10.0; + c.min_conf[1] = 55.0; c.min_ev[1] = 0.00; + c.se_minrr[1] = 0.70; + c.tp1_pct[1] = 60; c.tp2_pct[1] = 25; c.tp3_pct[1] = 15; + c.max_cost_pct[1] = 30.0; + c.max_positions[1] = 1; + c.se_peak_th1[1] = 0.60; c.se_peak_th2[1] = 0.72; c.se_peak_th3[1] = 0.78; + c.d1choch_gate[1] = 0; + + // ── H1 [index 2] + c.sl[2] = 2.00; c.tp1[2] = 3.20; + c.tp2[2] = 4.20; c.tp3[2] = 5.50; + c.mrr[2] = 1.50; c.risk[2] = 1.00; + c.sl_min[2] = 7.0; c.spread[2] = 20.0; + c.min_conf[2] = 50.0; c.min_ev[2] = 0.00; + c.se_minrr[2] = 0.80; + c.tp1_pct[2] = 60; c.tp2_pct[2] = 25; c.tp3_pct[2] = 15; + c.max_cost_pct[2] = 28.0; + c.max_positions[2] = 1; // H1: single position (multi-TP split wastes 3× risk per trade) + c.se_peak_th1[2] = 0.72; c.se_peak_th2[2] = 0.78; c.se_peak_th3[2] = 0.82; + c.d1choch_gate[2] = 0; + + // ── H4 [index 3] + c.sl[3] = 2.20; c.tp1[3] = 4.00; + c.tp2[3] = 5.30; c.tp3[3] = 6.80; + c.mrr[3] = 1.60; c.risk[3] = 1.20; + c.sl_min[3] = 12.0; c.spread[3] = 30.0; + c.min_conf[3] = 44.0; c.min_ev[3] = 0.00; + c.se_minrr[3] = 1.20; + c.tp1_pct[3] = 60; c.tp2_pct[3] = 25; c.tp3_pct[3] = 15; + c.max_cost_pct[3] = 18.0; + c.max_positions[3] = 0; + c.se_peak_th1[3] = 0.72; c.se_peak_th2[3] = 0.76; c.se_peak_th3[3] = 0.80; + c.d1choch_gate[3] = 0; + + // ── D1 [index 4] + c.sl[4] = 2.80; c.tp1[4] = 5.50; + c.tp2[4] = 7.50; c.tp3[4] = 10.00; + c.mrr[4] = 1.80; c.risk[4] = 1.80; + c.sl_min[4] = 22.0; c.spread[4] = 60.0; + c.min_conf[4] = 40.0; c.min_ev[4] = 0.00; + c.se_minrr[4] = 1.50; + c.tp1_pct[4] = 60; c.tp2_pct[4] = 25; c.tp3_pct[4] = 15; + c.max_cost_pct[4] = 12.0; + c.max_positions[4] = 0; + c.se_peak_th1[4] = 0.50; c.se_peak_th2[4] = 0.55; c.se_peak_th3[4] = 0.65; + c.d1choch_gate[4] = 1; + + static bool s_warnedUnknown = false; + if(!s_warnedUnknown) + { + PrintFormat("* GetPairTFConfig: '%s' not in table — using conservative fallback [logged once]", sym); + s_warnedUnknown = true; + } + } + return c; +} + +//+------------------------------------------------------------------+ +//| ComputeActiveGates — v10.06 FIX#275 | +//| SINGLE SOURCE OF TRUTH for all trade-entry thresholds. | +//| | +//| Called once at the END of ApplyPairTFProfile() (which is itself | +//| called from UpdateAutoOptimization once per recalc cycle). | +//| Result stored in global g_gates, read by every gate: | +//| • MeetsMinimumEntryScore → g_gates.minScore | +//| • SelectBestCandidate → g_gates.minScore | +//| • EvaluateSmartEntry → g_gates.minScore / counterFloor | +//| • EvaluateCascade → g_gates.minConfirmations | +//| | +//| No hardcoded 36/42/45/50/70/3 anywhere in the 4 gates. | +//| Change a value here → it changes everywhere simultaneously. | +//+------------------------------------------------------------------+ +void ComputeActiveGates() +{ + // ── 1. minScore: pair table already applied via ApplyPairTFProfile (step 10) ─ + // g_autoOptParams.min_entry_quality now holds the pair table value directly. + // If pair table has no entry (=0), ApplyPairTFProfile fell back to EA_MinEntryScore. + // Steps 1-2 (category/TF quality adjustments) no longer touch min_entry_quality. + double absFloor; + if (_Period >= PERIOD_H4) absFloor = 30.0; + else if(_Period >= PERIOD_H1) absFloor = 44.0; + else absFloor = 44.0; + + double minScore = MathMax(g_autoOptParams.min_entry_quality, absFloor); + + // ── 4. minConfirmations: TF-aware, pair table informs base ─── + // Pair profile sets base (Major=4, Cross=3 etc.). + // * FIX#322b re-enabled FVG for H4 (user-controlled via EnableFVG). + // * FIX#405: H4 cap raised 2→3. FIX#271 set cap=2 because "FVG disabled = Structure+KZ = max 2". + // Now Structure + KZ + FVG (or LIQ_SWEEP) = 3 realistic confirmations on H4. + // Cap=2 was too low — any 2 confirmations passed, diluting quality. + // Cap=3 still below profile value of 4, so remains practical for H4 swing TF. + // baseConf: pair table category drives this (Major=2, others=2, all capped by TF below) + // minConf: TF-aware. H1=2 (Structure+KZ sufficient intraday), H4=3 (FVG re-enabled), D1=2. + int minConf; + if (_Period >= PERIOD_D1) minConf = 2; + else if(_Period >= PERIOD_H4) minConf = 3; + else if(_Period >= PERIOD_H1) minConf = 2; + else minConf = 2; // M5/M15 + + // ── 5. Counter-structure parameters ────────────────────────── + // H4+: FIX#232/225 already vetted direction via MTF before reaching + // SmartEntry — no double-penalty. Floor = same as minScore. + // M15/H1: standard penalties apply (intrabar CT needs extra evidence). + int counterPenalty; + int counterFloor; + string cat = g_autoOptParams.pair_category; + if(_Period >= PERIOD_H4) + { + counterPenalty = 0; // CT vetted, no extra penalty + // * v10.10 FIX#285: counterFloor was MathMax(pairQ, 36.0) — hardcoded 36 floor + // caused "Counter-structure: 35 < 36" rejections even after FIX#283 set minScore=34. + // Root cause: minScore=MathMax(pairQ=34, absFloor=30)=34 but counterFloor stayed 36. + // Fix: counterFloor = (int)minScore — same value, single source. + // Computed AFTER minScore is finalized (minScore defined above at step 3). + counterFloor = (int)minScore; + } + else + { + // EV-adaptive penalty (M15/H1 CT trades) + counterPenalty = 10; // will be overridden per-call in SmartEntry based on EV + // Category-aware floor for M15/H1 + // * v10.18 FIX#300: counterFloor was hardcoded 70 for Forex M15/H1. + // With min_conf[1]=52 (M15 EURUSD), counterFloor=70 → impossible to pass. + // Trades with score 43-51, EV=0.10-0.41R were all blocked. + // Fix: counterFloor = max(minScore+8, category_minimum) — tracks calibration. + if (cat == "Index") counterFloor = MathMax((int)minScore + 8, 55); + else if(cat == "Metal") counterFloor = MathMax((int)minScore + 8, 60); + else if(cat == "Energy") counterFloor = MathMax((int)minScore + 8, 60); + else counterFloor = MathMax((int)minScore + 8, 52); // Forex M15/H1 + } + + // ── 6. HTF gate thresholds (FIX#16c sync) ──────────────────── + // FIX#16c uses htfMinScore=40 (neutral) and 45 (opposed) — hardcoded. + // For H4 with minScore=36, this means 40>36 → HTF gate is STRICTER than g_gates. + // Fix: derive from minScore so all thresholds move together. + // Neutral (MTF=NEUTRAL, trading WITH structure): same as minScore (no extra penalty) + // Opposed (MTF directly against trade direction): minScore + small penalty + int htfMinScoreNeutral = (int)minScore; // no extra penalty when MTF neutral + int htfMinScoreOpposed = (int)minScore + 5; // +5 when MTF actively opposed + + // ── 7. Weak regime penalty (FIX#56 sync) ───────────────────── + // H4+: +5 (fewer candidates, structure-based, no intrabar noise extra) + // M15/H1: +15 (more noise, higher quality bar needed in weak market) + int weakPenalty = (_Period >= PERIOD_H4) ? 5 : 15; + + // ── 8. minEV: from pair table (FIX#282) ────────────────────── + // g_autoOptParams.smart_min_ev already written by ApplyPairTFProfile: + // if cfg.min_ev>0 (pair-specific, e.g. EURUSD=0.05) → that value is used + // if cfg.min_ev=0.00 (FIX#359 EURUSD H4) → gate disabled: EV>=0.00 always passes + // if cfg.min_ev<0 (sentinel) → category default (0.08R Major) + // * FIX#371: smart_min_ev=0.00 must reach g_gates.minEV as 0.00, not be replaced by 0.08. + // Old guard (>0) treated 0.00 same as "not set" → category default 0.08 snuck in. + // New: if ApplyPairTFProfile set smart_min_ev explicitly (≥0), honour it. + double minEV = (g_autoOptParams.smart_min_ev >= 0.0 && g_autoOptParams.smart_min_ev < 0.99) + ? g_autoOptParams.smart_min_ev // explicit pair-table value (incl. 0.00) + : 0.08; // category default fallback + + // ── 9. FIX#280: Divergence block threshold (TF-aware) ──────── + // Stored in g_gates so FIX#104a reads it. H4+: require strength>=80 (fewer false divs). + // M15/H1: keep existing threshold 65 (noise-prone, stricter needed). + // g_gates.divBlockThreshold is checked in SelectBestCandidate FIX#104a. + int divBlockThreshold = (_Period >= PERIOD_H4) ? 80 : 65; + + // ── 10. FIX#281: Pattern block cap (TF-aware) ───────────────── + // H4+: 24h cap (6 H4 bars — pattern stale after 1 day at swing TF). + // M15/H1/D1+: 72h cap (original value — intraday patterns live longer). + int patternBlockCapHours = (_Period >= PERIOD_H4 && _Period < PERIOD_D1) ? 24 : 72; + + // ── 11. Write to global g_gates ─────────────────────────────── + g_gates.minScore = minScore; + g_gates.minScoreFloor = absFloor; + g_gates.minConfirmations = minConf; + g_gates.counterFloor = counterFloor; + g_gates.counterPenalty = counterPenalty; + g_gates.htfMinScoreNeutral = htfMinScoreNeutral; + g_gates.htfMinScoreOpposed = htfMinScoreOpposed; + g_gates.weakPenalty = weakPenalty; + g_gates.minEV = minEV; + g_gates.divBlockThreshold = divBlockThreshold; + g_gates.patternBlockCapHours = patternBlockCapHours; + g_gates.computed = true; + g_gates.source = StringFormat("PairTable(%.0f) TF=%s minScore=%.0f minConf=%d htfN=%d htfO=%d minEV=%.2f divThr=%d patCap=%dh", + minScore, EnumToString(_Period), minScore, minConf, + htfMinScoreNeutral, htfMinScoreOpposed, + minEV, divBlockThreshold, patternBlockCapHours); + + // ── 12. Log once when values change ────────────────────────── + static double s_lastMinScore = -1; + static int s_lastMinConf = -1; + if(g_verboseLog && (minScore != s_lastMinScore || minConf != s_lastMinConf)) + { + PrintFormat("[FIX#275-282] ComputeActiveGates: minScore=%.0f minConf=%d minEV=%.2f divThr=%d patCap=%dh | %s", + minScore, minConf, minEV, divBlockThreshold, patternBlockCapHours, g_gates.source); + s_lastMinScore = minScore; + s_lastMinConf = minConf; + } +} + +//+------------------------------------------------------------------+ +//| ValidatePairTFConfigs — called once at OnInit() | +//| Checks all known pairs: tp1/sl >= mrr on every TF. | +//| Prints a single WARN line per violation — fails silently. | +//+------------------------------------------------------------------+ +void ValidatePairTFConfigs() +{ + string testPairs[] = { + "EURUSD","GBPUSD","AUDUSD","NZDUSD","USDJPY","USDCAD","USDCHF", + "GBPJPY","EURJPY","AUDJPY","CADJPY", + "XAUUSD","XAGUSD", + "US500","US100","UK100","GERMAN","FRANCE", + "BTCUSD","ETHUSD", + "NATGAS","USOIL" + }; + string tfs[] = {"M5","M15","H1","H4","D1"}; + int violations = 0; + for(int i = 0; i < ArraySize(testPairs); i++) + { + PairTFConfig cfg = GetPairTFConfig(testPairs[i]); + for(int t = 0; t < 5; t++) + { + double rr = (cfg.sl[t] > 0) ? cfg.tp1[t] / cfg.sl[t] : 0; + if(rr < cfg.mrr[t] - 0.001) + { + PrintFormat("* ValidatePairTFConfigs VIOLATION: %s %s tp1/sl=%.2f < mrr=%.2f", + testPairs[i], tfs[t], rr, cfg.mrr[t]); + violations++; + } + } + } + if(violations == 0) + Print("* ValidatePairTFConfigs: ALL pairs/TFs pass R:R validation ✅"); + else + PrintFormat("* ValidatePairTFConfigs: %d VIOLATIONS — check table above", violations); +} + +//+------------------------------------------------------------------+ +//| GetPairTFMinRR — thin wrapper reading from GetPairTFConfig | +//| Replaces the old standalone table (was always out of sync). | +//+------------------------------------------------------------------+ +double GetPairTFMinRR(string sym, ENUM_TIMEFRAMES tf) +{ + // Resolve TF index (same mapping as before) + int tfIdx; + if(tf <= PERIOD_M5) tfIdx = 0; + else if(tf <= PERIOD_M30) tfIdx = 1; + else if(tf <= PERIOD_H1) tfIdx = 2; + else if(tf <= PERIOD_H4) tfIdx = 3; + else tfIdx = 4; + + // Normalise symbol (strip broker suffix, uppercase) + string s = sym; + int dotPos = StringFind(s, "."); + if(dotPos > 0) s = StringSubstr(s, 0, dotPos); + if(StringLen(s) > 6) s = StringSubstr(s, 0, 6); + StringToUpper(s); + + return GetPairTFConfig(s).mrr[tfIdx]; +} + +//+------------------------------------------------------------------+ +//| ApplyPairTFProfile — reads from GetPairTFConfig, applies to | +//| g_autoOptParams. All pair-specific data lives in one place. | +//+------------------------------------------------------------------+ +void ApplyPairTFProfile() +{ + // ── Normalise symbol ────────────────────────────────────────── + string sym = _Symbol; + StringToUpper(sym); + int dotPos = StringFind(sym, "."); + if(dotPos > 0) sym = StringSubstr(sym, 0, dotPos); + if(StringLen(sym) > 6) sym = StringSubstr(sym, 0, 6); + + // ── TF index ───────────────────────────────────────────────── + int tf = 0; + switch(Period()) + { + case PERIOD_M1: case PERIOD_M5: tf = 0; break; + case PERIOD_M15:case PERIOD_M30: tf = 1; break; + case PERIOD_H1: tf = 2; break; + case PERIOD_H4: tf = 3; break; + default: tf = 4; break; + } + + // ── Fetch config ───────────────────────────────────────────── + PairTFConfig cfg = GetPairTFConfig(sym); + + // ── Apply to g_autoOptParams ────────────────────────────────── + // * v10.11 FIX#286: BASE from table + MULTIPLIER from AutoOpt vol/TF adjustments. + // ============================================================ + // BEFORE: sl_atr_mult = cfg.sl[tf] (absolute override) + // → volatile adjustment from step 3 (e.g. ×1.20) was LOST. + // → EURUSD H4 VOL_HIGH: AutoOpt set 2.00×1.20=2.40, PairTFProfile reset to 2.00. + // + // FIX: capture the vol/TF multiplier that AutoOpt computed for sl/tp, + // then apply it ON TOP of the table base value. + // vol_mult = current/baseline. Baseline = what step 1 (category) set. + // To avoid needing a saved baseline, we use g_marketSnap.vol_regime directly. + // This is the same source ApplyVolatilityAdjustments uses. + // + // Vol multipliers mirror ApplyVolatilityAdjustments switch: + // VOL_VERY_LOW: ×1.30 VOL_LOW: ×1.15 VOL_NORMAL: ×1.00 + // VOL_HIGH: ×1.20 VOL_EXTREME: ×1.50 + // quality_add mirrors step 2 H4 quality boost: +4 for Major, +8 others. + // ============================================================ + double volSlMult = 1.0; + int qualityAdd = 0; + if(AutoOpt_AutoVolatility) + { + // * v10.40 FIX#340: H4+ exempt from volSlMult. + // On H4, the pair table SL (cfg.sl[3]=2.00 ATR) already accounts for swing noise. + // volSlMult=1.50 (VOL_EXTREME) pushed effective SL to 3.00 ATR while TP1 stayed at + // 2.80 ATR → RR=0.93 < mrr=1.30 → ALL candidates rejected ("ALL REJECTED | RR=0"). + // M5/M15/H1 still benefit from vol expansion (tighter TF, real noise concern). + bool isSwingTF = (g_autoOptParams.tf_category == TF_CAT_SWING || + g_autoOptParams.tf_category == TF_CAT_POSITION); + if(!isSwingTF) + { + switch(g_marketSnap.vol_regime) + { + case VOL_VERY_LOW: volSlMult = 1.30; break; + case VOL_LOW: volSlMult = 1.15; break; + case VOL_NORMAL: volSlMult = 1.00; break; + case VOL_HIGH: volSlMult = 1.20; break; + case VOL_EXTREME: volSlMult = 1.50; break; + default: volSlMult = 1.00; break; + } + } + // else: H4/D1 — volSlMult stays 1.00, pair table SL used as-is + // TF quality boost (H4): same as step 2 TF_CAT_SWING/POSITION + if(isSwingTF) + qualityAdd = (cfg.category == "Major" || cfg.category == "Cross") ? 4 : 8; + } + g_autoOptParams.sl_atr_mult = cfg.sl[tf] * volSlMult; + g_autoOptParams.tp_atr_mult = cfg.tp1[tf]; + // min_rr: pair table is authoritative. If not set (=0), fall back to EA_MinRR input. + // AutoOpt vol/TF multipliers in steps 1-2 are now bypassed for this value. + g_autoOptParams.min_rr = (cfg.mrr[tf] > 0) ? cfg.mrr[tf] : EA_MinRR; + g_autoOptParams.risk_pct = MathMin(g_autoOptParams.risk_pct, EA_RiskPercent); + g_autoOptParams.sl_min_pips = cfg.sl_min[tf]; + g_autoOptParams.max_spread_pips = cfg.spread[tf]; + g_autoOptParams.commission_per_lot = cfg.comm; + g_autoOptParams.pos_trending_bonus = cfg.tr_bonus; + g_autoOptParams.pos_ranging_penalty = cfg.rng_pen; + // min_entry_quality: pair table is authoritative. If not set (=0), fall back to EA_MinEntryScore input. + // AutoOpt category/TF quality adjustments in steps 1-2 are now bypassed for this value. + g_autoOptParams.min_entry_quality = (cfg.min_conf[tf] > 0) ? cfg.min_conf[tf] : (double)EA_MinEntryScore; + g_autoOptParams.pair_category = cfg.category; + // * v10.09 FIX#282: Per-pair smart_min_ev override. + // * FIX#289: per-TF min_ev from array [tf] — 0=category default. + // * FIX#371: Handle min_ev=0.00 correctly (was: >0 guard skipped 0.00 → stayed at 0.08 default). + // Convention: min_ev[tf]=-1 = use category default | min_ev[tf]=0.00 = disable gate | >0 = custom value. + // EURUSD H4: c.min_ev[3]=0.00 → effectiveMinEV=0.00 → "pass if EV>=0". + // g_gates.minEV is read by EvaluateSmartEntry via FIX#371 path directly. + if(cfg.min_ev[tf] >= 0.0) // 0.00 is a valid explicit setting — DO apply it + g_autoOptParams.smart_min_ev = cfg.min_ev[tf]; + // else: cfg.min_ev[tf] < 0 (sentinel -1) → keep category default set by UpdateAutoOptimization + + // * FIX#303: Per-TF SmartExit and filter overrides. + // Shadow globals used (MQL5 inputs cannot be assigned at runtime). + // 0 = keep global input value. Non-zero = pair+TF calibrated override. + if(cfg.se_minrr[tf] > 0) + { + g_workingSmartExit_MinRR = cfg.se_minrr[tf]; + g_workingFIX41_SmartExitRR = cfg.se_minrr[tf]; + g_autoOptParams.smart_exit_min_rr = cfg.se_minrr[tf]; + } + else + { + // Reset to global inputs (important when switching TFs at runtime) + g_workingSmartExit_MinRR = EA_SmartExit_MinProfit_RR; + g_workingFIX41_SmartExitRR = EA_SmartExit_MinProfit_RR; + } + g_workingRequireKillzone = (cfg.req_kz[tf] == 1) ? true : + (cfg.req_kz[tf] == -1)? false : EA_RequireKillzone; + g_workingRequireTrend = (cfg.req_trend[tf] == 1) ? true : + (cfg.req_trend[tf] == -1)? false : EA_RequireTrend; + + // * FIX#304: per-TF max positions — stored in g_autoOptParams for use at entry + if(cfg.max_positions[tf] > 0) + g_autoOptParams.max_positions_per_signal = cfg.max_positions[tf]; + else + g_autoOptParams.max_positions_per_signal = 0; // 0 = use EA global (EA_MaxPositions) + + // * FIX#305: per-TF score cap + g_autoOptParams.score_cap = cfg.score_cap[tf]; // 0 = no cap + + // * v10.25 FIX#311: per-pair+TF calibrations — ALL stored in pair table, wired here. + // Pattern: 0 = keep global default. Non-zero = pair+TF override. + // TP close percents + g_workingTP1_Pct = (cfg.tp1_pct[tf] > 0) ? cfg.tp1_pct[tf] : (int)EA_TP1_Percent; + g_workingTP2_Pct = (cfg.tp2_pct[tf] > 0) ? cfg.tp2_pct[tf] : (int)EA_TP2_Percent; + g_workingTP3_Pct = (cfg.tp3_pct[tf] > 0) ? cfg.tp3_pct[tf] : (int)EA_TP3_Percent; + // D1 CHoCH neutral block + g_workingBlockNeutral = (cfg.block_neutral[tf] == 1) ? true : + (cfg.block_neutral[tf] == -1) ? false : + EA_D1CHoCH_BlockNeutral; + // Trendline lookback (-1=disable, 0=keep AutoOpt default, N=override) + if(cfg.trendline_lb[tf] == -1) + g_autoOptParams.trendline_lookback = 0; // 0 triggers early return in DetectTrendlines (guard: <20) + else if(cfg.trendline_lb[tf] > 0) + g_autoOptParams.trendline_lookback = cfg.trendline_lb[tf]; + // else: keep whatever UpdateAutoOptimization step 2 set + // Ranging penalty (0=keep category default, >0=override) + if(cfg.rng_pen_tf[tf] > 0) + g_autoOptParams.pos_ranging_penalty = cfg.rng_pen_tf[tf]; + // * v10.26 FIX#312: CT thresholds — stored in g_autoOptParams for use in EvaluateSmartEntry. + // 0 = keep hardcoded defaults. Non-zero = pair+TF calibrated override. + g_autoOptParams.ct_ev_min = (cfg.ct_ev_min[tf] > 0) ? cfg.ct_ev_min[tf] : 0.0; + g_autoOptParams.ct_score_min = (cfg.ct_score_min[tf] > 0) ? cfg.ct_score_min[tf] : 0; + g_autoOptParams.ct_wp_min = (cfg.ct_wp_min[tf] > 0.0) ? cfg.ct_wp_min[tf] : 0.0; + g_autoOptParams.ct_wp_mid = (cfg.ct_wp_mid[tf] > 0.0) ? cfg.ct_wp_mid[tf] : 0.0; + g_autoOptParams.ct_wp_high = (cfg.ct_wp_high[tf] > 0.0) ? cfg.ct_wp_high[tf] : 0.0; + // * v10.27 FIX#314: position sizing per-TF fields + // tr_bonus_tf overrides c.tr_bonus (which was already set by category block above) + if(cfg.tr_bonus_tf[tf] > 0) + g_autoOptParams.pos_trending_bonus = cfg.tr_bonus_tf[tf]; + // loss_streak_cut and max_lot: stored in working vars for use in CalculatePositionSize + g_workingLossStreakCut = (cfg.loss_streak_cut_tf[tf] > 0) ? cfg.loss_streak_cut_tf[tf] : PosSize_LossStreakCut; + g_workingMaxLot = (cfg.max_lot_tf[tf] > 0) ? cfg.max_lot_tf[tf] : SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); + // * v10.29 FIX#317 → FIXED: Risk ceiling = EA_RiskPercent για ΟΛΟΥΣ τους TF. + // Αφαιρέθηκε το H4 hardcode 1.80%. Τώρα ο χρήστης ελέγχει πλήρως το risk + // από το EA_RiskPercent input (default 1.8%). Αν το αλλάξεις, αλλάζει παντού. + g_workingRiskCeiling = EA_RiskPercent; + // * v10.31 FIX#320: choppy/MTF protection — read from pair table, 0=disabled + g_workingChoppyMinConf = (cfg.choppy_min_conf[tf] > 0) ? cfg.choppy_min_conf[tf] : 0; + g_workingChoppyLotMult = (cfg.choppy_lot_mult[tf] > 0) ? cfg.choppy_lot_mult[tf] : 0; + g_workingBlockTCChoppy = cfg.block_tc_choppy[tf]; + g_workingMTFHardBlock = cfg.mtf_hard_block[tf]; + // Pair table d1choch_gate takes priority. + // Convention: 1=force-on, -1=force-off, 0=use global EA_D1CHoCHGate input. + if(cfg.d1choch_gate[tf] == 1) g_workingD1CHoCHGate = 1; + else if(cfg.d1choch_gate[tf] == -1) g_workingD1CHoCHGate = 0; + else g_workingD1CHoCHGate = EA_D1CHoCHGate ? 1 : 0; + // * FIX#363: OB per-TF override (read from pair table, 0=global default) + g_workingAllowOB = cfg.allow_ob[tf]; + // * FIX#372: BOS_RETEST per-TF override (read from pair table, 0=global default) + g_workingAllowBOSRetest = cfg.allow_bos_retest[tf]; + // * FIX#454: remaining technique overrides (0=global, -1=force-off, 1=force-on) + g_workingAllowFVG = cfg.allow_fvg[tf]; + g_workingAllowOTE = cfg.allow_ote[tf]; + g_workingAllowLIQ = cfg.allow_liq[tf]; + g_workingAllowBreaker = cfg.allow_breaker[tf]; + g_workingAllowTC = cfg.allow_tc[tf]; + // * FIX#373: Read MR config from pair table + g_workingMR_Enabled = cfg.mr_enabled[tf]; + g_workingMR_RSIBuy = (cfg.mr_rsi_buy[tf] > 0) ? cfg.mr_rsi_buy[tf] : 35; + g_workingMR_RSISell = (cfg.mr_rsi_sell[tf] > 0) ? cfg.mr_rsi_sell[tf] : 65; + g_workingMR_SLMult = (cfg.mr_sl_mult[tf] > 0) ? cfg.mr_sl_mult[tf] : 0.30; + g_workingMR_MinRangeATR = (cfg.mr_min_range_atr[tf] > 0) ? cfg.mr_min_range_atr[tf] : 1.5; + // * FIX#421: SmartExit per-TF — read from pair table, fallback to defaults. + g_workingSE_RSI_OB = (cfg.se_rsi_ob[tf] > 0) ? cfg.se_rsi_ob[tf] : 70.0; + g_workingSE_RSI_OS = (cfg.se_rsi_os[tf] > 0) ? cfg.se_rsi_os[tf] : 30.0; + g_workingSE_PeakTh1 = (cfg.se_peak_th1[tf] > 0) ? cfg.se_peak_th1[tf] : 0.65; + g_workingSE_PeakTh2 = (cfg.se_peak_th2[tf] > 0) ? cfg.se_peak_th2[tf] : 0.72; + g_workingSE_PeakTh3 = (cfg.se_peak_th3[tf] > 0) ? cfg.se_peak_th3[tf] : 0.78; + g_workingSE_OverrideRR = cfg.se_override_rr[tf]; // 0 = disabled + // * FIX#423: Cooperative SE thresholds — read from pair table, fallback to defaults. + // CoopTrail: 0 in table = compute from th1 at runtime (th1+0.05 / th1+0.15). + g_workingSE_MomRatio = (cfg.se_mom_ratio[tf] > 0) ? cfg.se_mom_ratio[tf] : 0.70; + g_workingSE_MomScoreMult = (cfg.se_mom_score_mult[tf] > 0) ? cfg.se_mom_score_mult[tf] : 1.40; + g_workingMaxCostPct = (cfg.max_cost_pct[tf] > 0) ? cfg.max_cost_pct[tf] : COST_MaxCostPercent; + + // * FIX#309: per-TF overrides that survive UpdateAutoOptimization per-bar resets. + // min_conf_override: if >0, ComputeActiveGates bypasses EA_MinEntryScore ceiling. + // M5 min_conf=75 → minScore=60 (bug). With override=75 → minScore=75 ✅ + // se_minrr_override: if >0, AutoOpt per-bar adjustments use this as hard floor. + // M15 se_minrr=0.70 was reset to 0.30 by UpdateAutoOptimization each bar. + g_autoOptParams.min_conf_override = (cfg.min_conf[tf] > 0) ? cfg.min_conf[tf] : 0; + g_autoOptParams.se_minrr_override = (cfg.se_minrr[tf] > 0) ? cfg.se_minrr[tf] : 0; + + // ── Category TP multiplier (FIX#238) ───────────────────────── + // Apply after setting from table — Metal/Index/VolatileCross get x1.10 + double tpMult = 1.0; + if(cfg.category == "Metal" || cfg.category == "Index" || cfg.category == "VolatileCross") + tpMult = 1.10; + else if(cfg.category == "Exotic") + tpMult = 0.90; + if(tpMult != 1.0) + { + g_autoOptParams.tp_atr_mult *= tpMult; // TP1 ATR mult (only field available) + // tp2/tp3 bonus applied via rr ratios below + } + + // ── TP R:R ratios ───────────────────────────────────────────── + // Derive R:R ratios directly from cfg (sl_atr_mult already set above) + double tpMult_val = (cfg.category == "Metal" || cfg.category == "Index" || cfg.category == "VolatileCross") ? 1.10 + : (cfg.category == "Exotic") ? 0.90 : 1.0; + double derived_tp1_rr = (cfg.tp1[tf] * tpMult_val) / cfg.sl[tf]; + double derived_tp2_rr = (cfg.tp2[tf] * tpMult_val) / cfg.sl[tf]; + double derived_tp3_rr = (cfg.tp3[tf] * tpMult_val) / cfg.sl[tf]; + // * v10.01 FIX#245: Use ALREADY TF-scaled tp_rr as floor (not raw InpTP1_RR). + // ROOT CAUSE: Steps 1-2 apply TF-scaling (H1: tp1_rr *= 1.35, H4: *= 1.40) to + // g_autoOptParams.tp1_rr. Step 10 (last) then calls MathMax(InpTP1_RR, derived) + // — this uses the UNSCALED raw input as the comparison floor, so whenever + // derived_tp1_rr > InpTP1_RR (table has larger value than user input), it overwrites + // the TF-scaled value with the table-derived value, losing the *1.35/*1.40 multiplier. + // Example: H1, InpTP1_RR=2.0, after Step2: tp1_rr=2.70 (×1.35). + // Table: derived_tp1_rr=2.50 → MathMax(2.0, 2.50)=2.50 → loses 2.70! + // Fix: use g_autoOptParams.tp1_rr (already TF-scaled) as the floor — Step10 table + // can only WIDEN, never shrink what Steps 1-2 built. + g_autoOptParams.tp1_rr = MathMax(g_autoOptParams.tp1_rr, derived_tp1_rr); // * FIX#245: was MathMax(InpTP1_RR, ...) + g_autoOptParams.tp2_rr = MathMax(g_autoOptParams.tp2_rr, derived_tp2_rr); // * FIX#245 + g_autoOptParams.tp3_rr = MathMax(g_autoOptParams.tp3_rr, derived_tp3_rr); // * FIX#245 + + // ── FVG pips per pair (FIX#150) ────────────────────────────── + { + double pipPts = (g_pipValue > 0) ? g_pipValue / _Point : 10.0; + double fvgPips = 0.0; + if(cfg.category == "Metal") + fvgPips = (tf >= 3) ? 20.0 : (tf >= 1 ? 15.0 : 10.0); + else if(cfg.category == "Index") + fvgPips = (tf >= 3) ? 10.0 : (tf >= 1 ? 6.0 : 4.0); + else if(sym == "GBPUSD" || sym == "GBPJPY") + fvgPips = (tf >= 3) ? 5.0 : (tf >= 1 ? 2.0 : 1.0); + else + fvgPips = (tf >= 3) ? 4.0 : (tf >= 1 ? 2.0 : 1.0); + if(fvgPips > 0) + g_autoOptParams.fvg_min_size = MathMax(g_autoOptParams.fvg_min_size, fvgPips * pipPts); + } + + // ── Logging ─────────────────────────────────────────────────── + if(g_verboseLog) + { + static string _s102LastSym = ""; + static int _s102LastTF = -1; + static double _s102LastRisk = -1; + if(sym != _s102LastSym || tf != _s102LastTF || MathAbs(cfg.sl[tf] - _s102LastRisk) > 0.001) + { + PrintFormat("* PairTFProfile [FIX#289]: %s TF=%s | SL=%.2f×%.2f=%.2f TP1=%.2f mrr=%.2f RR=%.2f Risk=%.2f%% QualBase=%d→%d Cat=%s", + sym, (tf==0?"M5":tf==1?"M15":tf==2?"H1":tf==3?"H4":"D1"), + cfg.sl[tf], volSlMult, g_autoOptParams.sl_atr_mult, + cfg.tp1[tf]*tpMult, cfg.mrr[tf], + g_autoOptParams.tp1_rr, g_autoOptParams.risk_pct, + (int)cfg.min_conf[tf], (int)g_autoOptParams.min_entry_quality, + cfg.category); + _s102LastSym = sym; + _s102LastTF = tf; + _s102LastRisk = cfg.sl[tf]; + } + } + // * FIX#317 REMOVED: H4 hardcoded risk override αφαιρέθηκε. + // g_workingRiskCeiling = EA_RiskPercent παντού (σετάρεται παραπάνω). + // FIX#107 θα κάνει clamp σωστά: [1.0%, EA_RiskPercent]. + // Δεν χρειάζεται ξεχωριστό H4 block. + // * v10.06 FIX#275: Recompute g_gates every time profile is applied. + // ApplyPairTFProfile is called from UpdateAutoOptimization (once per recalc cycle). + // ComputeActiveGates() reads g_autoOptParams.min_entry_quality (just written above) + // and derives the ONE set of thresholds that all 4 gates will use. + // * v10.35 FIX#332: Re-enforce risk ceiling AFTER all AutoOpt session adjustments. + // Bug: AutoOpt NY PowerHour (L28078) could push risk_pct to 2.0 AFTER FIX#317 ran. + // Solution: clamp here — final line before ComputeActiveGates, catches all paths. + if(g_workingRiskCeiling > 0 && g_autoOptParams.risk_pct > g_workingRiskCeiling) + { + if(g_verboseLog) + PrintFormat("[FIX#332] Risk ceiling enforced: %.2f%% → %.2f%% (ceiling=%.2f%%)", + g_autoOptParams.risk_pct, g_workingRiskCeiling, g_workingRiskCeiling); + g_autoOptParams.risk_pct = g_workingRiskCeiling; + } + ComputeActiveGates(); +} + + +void ApplyPairDetectionAdjustments(string category) +{ + double ageMult = 1.0; + double strengthMult = 1.0; + double lookbackMult = 1.0; + int swingAdj = 0; + // * FIX#456: ob_volume_mult was hardcoded per category, ignoring OB_VolumeMultiplier input. + // Pattern matches FVG_MinStrength: use input as base, category applies relative adjustment. + // Major=1.00x, Metal=0.86x, Index=0.93x, VolatileCross=1.07x etc. + // User can raise/lower OB_VolumeMultiplier and all categories scale proportionally. + double _obVolBase = (OB_VolumeMultiplier > 0) ? OB_VolumeMultiplier : 1.4; + if(category == "Major") + { + ageMult = 1.0; strengthMult = 1.0; lookbackMult = 1.0; swingAdj = 0; + g_autoOptParams.ob_volume_mult = _obVolBase; // 1.0× (reference) + g_autoOptParams.max_spread_pips = 5.0; + } + else if(category == "Metal") + { + ageMult = 1.0; strengthMult = 0.85; lookbackMult = 0.90; swingAdj = -1; + g_autoOptParams.ob_volume_mult = MathMax(1.1, _obVolBase * 0.86); // Lower: Metal tick volume unreliable + g_autoOptParams.max_spread_pips = 50.0; + } + else if(category == "Index") + { + ageMult = 0.80; strengthMult = 1.20; lookbackMult = 0.80; swingAdj = +1; + g_autoOptParams.ob_volume_mult = MathMax(1.1, _obVolBase * 0.93); + g_autoOptParams.max_spread_pips = 250.0; + } + else if(category == "Energy") + { + ageMult = 0.85; strengthMult = 1.0; lookbackMult = 0.85; swingAdj = 0; + g_autoOptParams.ob_volume_mult = MathMax(1.1, _obVolBase * 0.93); + g_autoOptParams.max_spread_pips = 20.0; + } + else if(category == "VolatileCross") + { + ageMult = 0.90; strengthMult = 1.10; lookbackMult = 0.90; swingAdj = 0; + g_autoOptParams.ob_volume_mult = _obVolBase * 1.07; // Higher: needs stronger confirmation + g_autoOptParams.max_spread_pips = 8.0; + } + else if(category == "Exotic") + { + ageMult = 0.75; strengthMult = 1.25; lookbackMult = 0.80; swingAdj = +1; + g_autoOptParams.ob_volume_mult = MathMax(1.1, _obVolBase * 0.86); + g_autoOptParams.max_spread_pips = 15.0; + } + else if(category == "Crypto") + { + ageMult = 0.70; strengthMult = 1.15; lookbackMult = 0.75; swingAdj = +1; + g_autoOptParams.ob_volume_mult = MathMax(1.1, _obVolBase * 0.86); + g_autoOptParams.max_spread_pips = 60.0; + } + // * v9.24 FIX#82 Step2.5: MTF confidence + VSA strength -- pair-specific base + // These values reflect each pair's data reliability + typical move characteristics + if(category == "Major") + { + g_autoOptParams.mtf_min_confidence = MathMin(MTF_MinConfidence, 65.0); // Majors: reliable MTF data + g_autoOptParams.vsa_min_strength = MathMin(VSA_MinStrength, 50.0); // Forex tick vol: moderate + } + else if(category == "Metal") + { + g_autoOptParams.mtf_min_confidence = MathMin(MTF_MinConfidence, 58.0); // Gold: volatile, lower MTF bar + g_autoOptParams.vsa_min_strength = MathMin(VSA_MinStrength, 50.0); // Gold: tick vol OK + // Pair TP multiplier for larger moves + g_autoOptParams.tp1_rr *= 1.10; + g_autoOptParams.tp2_rr *= 1.10; + g_autoOptParams.tp3_rr *= 1.10; + g_autoOptParams.judas_tp1_rr *= 1.10; + g_autoOptParams.judas_tp2_rr *= 1.10; + g_autoOptParams.judas_tp3_rr *= 1.10; + g_autoOptParams.tbs_tp1_rr *= 1.10; + g_autoOptParams.tbs_tp2_rr *= 1.10; + g_autoOptParams.tbs_tp3_rr *= 1.10; + } + else if(category == "Index") + { + g_autoOptParams.mtf_min_confidence = MathMin(MTF_MinConfidence, 70.0); // Indices: clean data + g_autoOptParams.vsa_min_strength = MathMin(VSA_MinStrength, 60.0); // Indices: real volume + g_autoOptParams.tp1_rr *= 1.10; + g_autoOptParams.tp2_rr *= 1.10; + g_autoOptParams.tp3_rr *= 1.10; + g_autoOptParams.judas_tp1_rr *= 1.10; + g_autoOptParams.judas_tp2_rr *= 1.10; + g_autoOptParams.judas_tp3_rr *= 1.10; + g_autoOptParams.tbs_tp1_rr *= 1.10; + g_autoOptParams.tbs_tp2_rr *= 1.10; + g_autoOptParams.tbs_tp3_rr *= 1.10; + } + else if(category == "Cross") + { + g_autoOptParams.mtf_min_confidence = MathMin(MTF_MinConfidence, 60.0); + g_autoOptParams.vsa_min_strength = MathMin(VSA_MinStrength, 48.0); // Crosses: weaker vol signal + } + else if(category == "VolatileCross") + { + g_autoOptParams.mtf_min_confidence = MathMin(MTF_MinConfidence, 55.0); // Very volatile: relax bar + g_autoOptParams.vsa_min_strength = MathMin(VSA_MinStrength, 50.0); + g_autoOptParams.tp1_rr *= 1.10; + g_autoOptParams.tp2_rr *= 1.10; + g_autoOptParams.tp3_rr *= 1.10; + g_autoOptParams.judas_tp1_rr *= 1.10; + g_autoOptParams.judas_tp2_rr *= 1.10; + g_autoOptParams.judas_tp3_rr *= 1.10; + g_autoOptParams.tbs_tp1_rr *= 1.10; + g_autoOptParams.tbs_tp2_rr *= 1.10; + g_autoOptParams.tbs_tp3_rr *= 1.10; + } + else if(category == "Exotic") + { + g_autoOptParams.mtf_min_confidence = MathMin(MTF_MinConfidence, 62.0); + g_autoOptParams.vsa_min_strength = MathMin(VSA_MinStrength, 52.0); + // Exotics: tighter TPs (liquidity risk) + g_autoOptParams.tp1_rr = MathMax(g_autoOptParams.tp1_rr * 0.90, InpTP1_RR); + g_autoOptParams.tp2_rr = MathMax(g_autoOptParams.tp2_rr * 0.90, InpTP2_RR); + g_autoOptParams.tp3_rr = MathMax(g_autoOptParams.tp3_rr * 0.90, InpTP3_RR); + } + else if(category == "Crypto") + { + g_autoOptParams.mtf_min_confidence = MathMin(MTF_MinConfidence, 60.0); + g_autoOptParams.vsa_min_strength = MathMin(VSA_MinStrength, 52.0); + } + else // Energy, Commodity, Unknown + { + g_autoOptParams.mtf_min_confidence = MathMin(MTF_MinConfidence, 60.0); + g_autoOptParams.vsa_min_strength = MathMin(VSA_MinStrength, 50.0); + } + // * v9.26 FIX#93: Pair-specific position sizing bonuses. + // Old: pos_trending_bonus=1.2 / pos_ranging_penalty=0.8 for ALL pairs. + // XAUUSD in a trend moves 3-5x more than EURUSD -> same bonus wrong. + // GBPJPY ranging = trap (wide spread + fakeouts) -> same penalty wrong. + // New: characteristic-based values, further scaled by TF (Step2) and Session (Step5). + if(category == "Major") + { + // EURUSD/GBPUSD/USDJPY: clean trending, moderate ranging (still tradeable) + g_autoOptParams.pos_trending_bonus = 1.25; // Trend: +25% (reliable, ATR doubles) + g_autoOptParams.pos_ranging_penalty = 0.80; // Range: -20% (still valid OB/FVG setups) + } + else if(category == "Metal") + { + // XAUUSD/XAGUSD: explosive trends, ranging = choppy fakeouts + g_autoOptParams.pos_trending_bonus = 1.40; // Trend: +40% (gold trends = 200-500p moves) + g_autoOptParams.pos_ranging_penalty = 0.65; // Range: -35% (gold ranging = noise trap) + } + else if(category == "Index") + { + // US30/US100/US500: strong trends, ranging = dead/dangerous + g_autoOptParams.pos_trending_bonus = 1.30; // Trend: +30% (indices trend cleanly) + g_autoOptParams.pos_ranging_penalty = 0.60; // Range: -40% (index ranging = fakeout hell) + } + else if(category == "Cross") + { + // EURGBP/AUDCAD: moderate trends, ranging OK + g_autoOptParams.pos_trending_bonus = 1.20; // Trend: +20% (crosses trend, but slower) + g_autoOptParams.pos_ranging_penalty = 0.75; // Range: -25% (crosses range = tighter but ok) + } + else if(category == "VolatileCross") + { + // GBPJPY/EURJPY: explosive but whippy -- big bonus in confirmed trends, heavy cut in ranging + g_autoOptParams.pos_trending_bonus = 1.45; // Trend: +45% (GBPJPY trends = huge ATR) + g_autoOptParams.pos_ranging_penalty = 0.55; // Range: -45% (GBPJPY ranging = whipsaw killer) + } + else if(category == "Energy") + { + // WTI/BRENT: news-driven spikes, moderate sizing + g_autoOptParams.pos_trending_bonus = 1.25; // Trend: +25% + g_autoOptParams.pos_ranging_penalty = 0.70; // Range: -30% + } + else if(category == "Exotic") + { + // Thin liquidity -- keep sizing conservative + g_autoOptParams.pos_trending_bonus = 1.10; // Trend: +10% only (slippage risk) + g_autoOptParams.pos_ranging_penalty = 0.70; // Range: -30% + } + else if(category == "Crypto") + { + // High vol -- moderate bonus, strong penalty for ranging (consolidation = sudden spike risk) + g_autoOptParams.pos_trending_bonus = 1.30; // Trend: +30% + g_autoOptParams.pos_ranging_penalty = 0.60; // Range: -40% + } + else // Commodity / Unknown + { + g_autoOptParams.pos_trending_bonus = 1.20; + g_autoOptParams.pos_ranging_penalty = 0.75; + } + if(category == "Major") g_autoOptParams.wp_min_threshold = MathMin(WinProb_MinThreshold, 0.58); + else if(category == "Metal") g_autoOptParams.wp_min_threshold = MathMin(WinProb_MinThreshold, 0.52); + else if(category == "Index") g_autoOptParams.wp_min_threshold = MathMin(WinProb_MinThreshold, 0.60); + else if(category == "Cross") g_autoOptParams.wp_min_threshold = MathMin(WinProb_MinThreshold, 0.55); + else if(category == "VolatileCross") g_autoOptParams.wp_min_threshold = MathMin(WinProb_MinThreshold, 0.50); + else g_autoOptParams.wp_min_threshold = MathMin(WinProb_MinThreshold, 0.55); + // Apply AGE multipliers + g_autoOptParams.fvg_max_age = (int)MathMax(5, g_autoOptParams.fvg_max_age * ageMult); + g_autoOptParams.ob_max_age = (int)MathMax(5, g_autoOptParams.ob_max_age * ageMult); + g_autoOptParams.liq_max_age = (int)MathMax(10, g_autoOptParams.liq_max_age * ageMult); + g_autoOptParams.ote_max_age = (int)MathMax(5, g_autoOptParams.ote_max_age * ageMult); + g_autoOptParams.bb_max_age = (int)MathMax(5, g_autoOptParams.bb_max_age * ageMult); + g_autoOptParams.mb_max_age = (int)MathMax(5, g_autoOptParams.mb_max_age * ageMult); + g_autoOptParams.trendline_max_age = (int)MathMax(5, g_autoOptParams.trendline_max_age * ageMult); + g_autoOptParams.crt_lookback = (int)MathMax(3, g_autoOptParams.crt_lookback * ageMult); + g_autoOptParams.crt_expiry = (int)MathMax(3, g_autoOptParams.crt_expiry * ageMult); + g_autoOptParams.tbs_expiry = (int)MathMax(3, g_autoOptParams.tbs_expiry * ageMult); + g_autoOptParams.amd_accum_max_bars = (int)MathMax(3, g_autoOptParams.amd_accum_max_bars * ageMult); + g_autoOptParams.sb_max_age = (int)MathMax(3, g_autoOptParams.sb_max_age * ageMult); + g_autoOptParams.signal_expiry_bars = (int)MathMax(3, g_autoOptParams.signal_expiry_bars * ageMult); + // Apply STRENGTH multiplier + g_autoOptParams.fvg_min_strength = MathMax(0.10, MathMin(0.80, g_autoOptParams.fvg_min_strength * strengthMult)); + // Apply LOOKBACK multiplier + g_autoOptParams.regime_lookback = (int)MathMax(5, g_autoOptParams.regime_lookback * lookbackMult); + g_autoOptParams.divergence_lookback = (int)MathMax(5, g_autoOptParams.divergence_lookback * lookbackMult); + g_autoOptParams.trendline_lookback = (int)MathMax(5, g_autoOptParams.trendline_lookback * lookbackMult); + g_autoOptParams.fib_lookback = (int)MathMax(10, g_autoOptParams.fib_lookback * lookbackMult); + // Apply SWING strength adjustment + g_autoOptParams.struct_swing_strength = MathMax(1, g_autoOptParams.struct_swing_strength + swingAdj); + g_autoOptParams.liq_swing_strength = MathMax(2, g_autoOptParams.liq_swing_strength + swingAdj); + if(g_verboseLog) + PrintFormat("* FIX#12 PairDetection: %s | AgeMult=%.2f | StrMult=%.2f | LookMult=%.2f | SwingAdj=%+d | SpreadMax=%.0f", + category, ageMult, strengthMult, lookbackMult, swingAdj, g_autoOptParams.max_spread_pips); +} +//+------------------------------------------------------------------+ +//| Step 5: Session and time-of-day adjustments | +//+------------------------------------------------------------------+ +void ApplySessionAdjustments() +{ + // v9.15 FIX#37: Complete session scaling for ALL inputs per session/pair/DoW + // User's SessionLondon/NY/Asian inputs remain MASTER -- AutoOpt only restricts further. + string pairCat = g_autoOptParams.pair_category; + string session = g_marketSnap.active_session; + int hour = g_marketSnap.current_hour; + int dow = g_marketSnap.current_dow; + // * v9.16 FIX#43c: TF-aware session scaling multiplier + // M1 Asian = 360 candles (heavy penalty needed), H4 = 1.5 candles (light penalty) + // Scale: SCALP=1.5x penalty, INTRADAY=1.0x, INTRASWING=0.7x, SWING=0.4x, POSITION=0.0x + ENUM_TF_CATEGORY tfCat = g_marketSnap.tf_category; + double tfSessionScale = 1.0; + switch(tfCat) + { + case TF_CAT_SCALP: tfSessionScale = 1.5; break; // M1-M5: sessions matter a LOT + case TF_CAT_INTRADAY: tfSessionScale = 1.0; break; // M15-M30: base + case TF_CAT_INTRASWING: tfSessionScale = 0.7; break; // H1: sessions less important + case TF_CAT_SWING: tfSessionScale = 0.4; break; // H4: barely matters + case TF_CAT_POSITION: tfSessionScale = 0.0; break; // D1+: sessions irrelevant + } + // === Step 1: Set sessions per pair category === + if(pairCat == "Major" || pairCat == "Cross") + { + g_autoOptParams.session_london = true; + g_autoOptParams.session_ny = true; + g_autoOptParams.session_asian = (pairCat == "Major"); + } + else if(pairCat == "Metal") + { + g_autoOptParams.session_london = true; + g_autoOptParams.session_ny = true; + g_autoOptParams.session_asian = false; + g_autoOptParams.use_session_filter = false; + } + else if(pairCat == "Index") + { + g_autoOptParams.session_london = true; + g_autoOptParams.session_ny = true; + g_autoOptParams.session_asian = false; + } + else if(pairCat == "VolatileCross") + { + g_autoOptParams.session_london = true; + g_autoOptParams.session_ny = true; + g_autoOptParams.session_asian = true; + } + else if(pairCat == "Crypto") + g_autoOptParams.use_session_filter = false; + // * v9.16 FIX#43d: Missing pair categories -- Energy, Exotic, Commodity + else if(pairCat == "Energy") + { + g_autoOptParams.session_london = true; + g_autoOptParams.session_ny = true; + g_autoOptParams.session_asian = false; // Oil trades London+NY only + } + else if(pairCat == "Exotic") + { + g_autoOptParams.session_london = true; + g_autoOptParams.session_ny = true; + g_autoOptParams.session_asian = false; // Exotics too wide in Asian + } + else if(pairCat == "Commodity" || pairCat == "Unknown") + { + g_autoOptParams.session_london = true; + g_autoOptParams.session_ny = true; + g_autoOptParams.session_asian = false; + } + // === Step 2: Session-specific parameter scaling === + // * v9.16 FIX#43b: ALL session adjustments now also modify smart_min_confidence (Gate 2). + // Previously only min_entry_score (Gate 1) was adjusted -> Gate 2 was always 60 -> bypassed! + // [LONDON OPEN 07:00-10:00] high momentum -- NEUTRAL + if(hour >= 7 && hour <= 10 && + (pairCat == "Major" || pairCat == "Cross" || pairCat == "VolatileCross")) + { + // * v9.16 FIX#42c: NEUTRAL -- no loosening or tightening + } + // [LONDON/NY OVERLAP 13:00-16:00] -- NEUTRAL with mild TP boost + else if(hour >= 13 && hour <= 16) + { + if(pairCat == "Major" || pairCat == "Metal" || pairCat == "Cross" || + pairCat == "Energy" || pairCat == "VolatileCross") + { + g_autoOptParams.tp_atr_mult = MathMin(AutoOpt_MaxTP_Mult, g_autoOptParams.tp_atr_mult * 1.05); + } + else if(pairCat == "Index") + g_autoOptParams.tp_atr_mult = MathMin(AutoOpt_MaxTP_Mult, g_autoOptParams.tp_atr_mult * 1.05); + } + // [NY POWER HOUR 19:00-21:00] + else if(hour >= 19 && hour <= 21 && pairCat == "Index") + { + g_autoOptParams.risk_pct = MathMin(AutoOpt_MaxRiskOverride, g_autoOptParams.risk_pct * 1.15); + g_autoOptParams.tp_atr_mult = MathMin(AutoOpt_MaxTP_Mult, g_autoOptParams.tp_atr_mult * 1.10); + } + // [ASIAN SESSION 00:00-06:00] -- heavy penalty for low-TF Majors/Cross + else if(hour >= 0 && hour <= 6 && + (pairCat == "Major" || pairCat == "Cross" || pairCat == "Exotic" || pairCat == "Energy")) + { + int scoreBoost = (int)(10 * tfSessionScale); // * FIX#43c: M1=+15, M15=+10, H1=+7, H4=+4 + g_autoOptParams.risk_pct *= MathMax(0.20, 1.0 - 0.60 * tfSessionScale); // M15: *0.40, H4: *0.76 + // * v9.16 FIX#44: REMOVED -- EA_MinEntryScore is single source of truth + // * v9.16 FIX#44: REMOVED -- EA_MinEntryScore is single source of truth + g_autoOptParams.min_confluence *= 1.0 + 0.25 * tfSessionScale; // M15: *1.25, H4: *1.10 + g_autoOptParams.tp_atr_mult = MathMax(AutoOpt_MinTP_Mult, g_autoOptParams.tp_atr_mult * (1.0 - 0.20 * tfSessionScale)); + g_autoOptParams.signal_expiry_bars = MathMax(8, (int)(g_autoOptParams.signal_expiry_bars * (1.0 - 0.60 * tfSessionScale))); + g_autoOptParams.news_mins_before_high = MathMax(g_autoOptParams.news_mins_before_high, 30); + g_autoOptParams.max_daily_trades = MathMax(1, (int)(g_autoOptParams.max_daily_trades * (1.0 - 0.67 * tfSessionScale))); + } + // [DEAD ZONE 22:00-01:00] wide spread, low volume -- ALL pairs + else if(hour >= 22 || hour < 1) + { + int dzBoost = (int)(6 * tfSessionScale); + g_autoOptParams.risk_pct *= MathMax(0.20, 1.0 - 0.60 * tfSessionScale); + // * v9.16 FIX#44: REMOVED -- EA_MinEntryScore is single source of truth + // * v9.16 FIX#44: REMOVED -- EA_MinEntryScore is single source of truth + g_autoOptParams.min_confluence *= 1.0 + 0.20 * tfSessionScale; + g_autoOptParams.allow_scalping = false; + g_autoOptParams.sl_atr_mult = MathMin(AutoOpt_MaxSL_Mult, g_autoOptParams.sl_atr_mult * (1.0 + 0.20 * tfSessionScale)); + g_autoOptParams.news_mins_before_high = MathMax(g_autoOptParams.news_mins_before_high, 45); + } + // === Step 3: Day-of-week adjustments === + if(dow == 1 && hour < 8) // Monday AM -- wait for direction + { + int monBoost = (int)(5 * tfSessionScale); + g_autoOptParams.risk_pct *= MathMax(0.30, 1.0 - 0.30 * tfSessionScale); + // * v9.16 FIX#44: REMOVED -- EA_MinEntryScore is single source of truth + // * v9.16 FIX#44: REMOVED -- EA_MinEntryScore is single source of truth + g_autoOptParams.min_confluence *= 1.0 + 0.10 * tfSessionScale; + g_autoOptParams.sl_atr_mult = MathMin(AutoOpt_MaxSL_Mult, g_autoOptParams.sl_atr_mult * 1.10); + } + if(dow == 5 && hour >= 18) // Friday PM -- rollover risk + { + int friBoost = (int)(5 * tfSessionScale); + g_autoOptParams.risk_pct *= MathMax(0.30, 1.0 - 0.50 * tfSessionScale); + g_autoOptParams.max_daily_trades = MathMax(1, g_autoOptParams.max_daily_trades / 2); + // * v9.16 FIX#44: REMOVED -- EA_MinEntryScore is single source of truth + // * v9.16 FIX#44: REMOVED -- EA_MinEntryScore is single source of truth + g_autoOptParams.allow_scalping = false; + g_autoOptParams.min_confluence *= 1.0 + 0.15 * tfSessionScale; + g_autoOptParams.tp_atr_mult = MathMax(AutoOpt_MinTP_Mult, g_autoOptParams.tp_atr_mult * 0.80); + } + // === Step 4: Off-hours catch-all === + if(session == "Off-Hours" && g_autoOptParams.use_session_filter) + { + int offBoost = (int)(4 * tfSessionScale); + g_autoOptParams.risk_pct *= MathMax(0.30, 1.0 - 0.50 * tfSessionScale); + // * v9.16 FIX#44: REMOVED -- EA_MinEntryScore is single source of truth + // * v9.16 FIX#44: REMOVED -- EA_MinEntryScore is single source of truth + g_autoOptParams.min_confluence *= 1.0 + 0.10 * tfSessionScale; + } + // * v9.26 FIX#95: Session-aware pos_trending_bonus / pos_ranging_penalty + // Final layer applied ON TOP of pair base (FIX#93) and TF scaling (FIX#94). + // Rationale: the SAME pair behaves differently in different sessions. + // EURUSD London Open trending = clean, institutional. Same in Asian = noise, spreads wider. + // XAUUSD NY Power Hour ranging = random, stop-hunting. London trending = directional. + // This multiplies the CURRENT value (already pair+TF scaled) by a session modifier. + { + // --- LONDON OPEN [07:00-10:00] --- + // Highest institutional order flow window: trend setups are premium quality + if(hour >= 7 && hour <= 10 && + (pairCat == "Major" || pairCat == "Cross" || pairCat == "VolatileCross" || pairCat == "Metal")) + { + g_autoOptParams.pos_trending_bonus = MathMin(1.50, g_autoOptParams.pos_trending_bonus * (1.0 + 0.08 * tfSessionScale)); + g_autoOptParams.pos_ranging_penalty = MathMax(0.30, g_autoOptParams.pos_ranging_penalty * (1.0 - 0.05 * tfSessionScale)); + } + // --- LONDON/NY OVERLAP [13:00-16:00] --- + // Highest volume of the day: trending setups fire fast, ranging = dangerous (both sides fighting) + else if(hour >= 13 && hour <= 16) + { + if(pairCat == "Major" || pairCat == "Metal" || pairCat == "Cross" || pairCat == "VolatileCross" || pairCat == "Index") + { + g_autoOptParams.pos_trending_bonus = MathMin(1.50, g_autoOptParams.pos_trending_bonus * (1.0 + 0.10 * tfSessionScale)); // Best time to size up trends + g_autoOptParams.pos_ranging_penalty = MathMax(0.30, g_autoOptParams.pos_ranging_penalty * (1.0 - 0.08 * tfSessionScale)); // Ranging in overlap = stop-hunt zone + } + else if(pairCat == "Energy") + { + g_autoOptParams.pos_trending_bonus = MathMin(1.50, g_autoOptParams.pos_trending_bonus * (1.0 + 0.12 * tfSessionScale)); // Oil trending in NY open = strong + } + } + // --- NY POWER HOUR [19:00-21:00] --- + // Indices and US pairs: late-day institutional flows, trend continuation + else if(hour >= 19 && hour <= 21) + { + if(pairCat == "Index") + { + g_autoOptParams.pos_trending_bonus = MathMin(1.50, g_autoOptParams.pos_trending_bonus * (1.0 + 0.12 * tfSessionScale)); // Index power hour = trending extension + g_autoOptParams.pos_ranging_penalty = MathMax(0.30, g_autoOptParams.pos_ranging_penalty * 0.85); // Index ranging at close = fakeout risk + } + else if(pairCat == "Metal") + { + g_autoOptParams.pos_trending_bonus = MathMin(1.50, g_autoOptParams.pos_trending_bonus * (1.0 + 0.05 * tfSessionScale)); // Gold NY close: mild boost only + } + } + // --- ASIAN SESSION [00:00-06:00] --- + // Low volume, range-bound for most pairs. ONLY useful for Metals/VolatileCross (Tokyo flows). + else if(hour >= 0 && hour <= 6) + { + if(pairCat == "VolatileCross") + { + // GBPJPY/EURJPY: Tokyo session = real yen flows, can trend (Judas sweep then move) + g_autoOptParams.pos_trending_bonus = MathMax(1.00, g_autoOptParams.pos_trending_bonus * (1.0 - 0.05 * tfSessionScale)); // Mild reduction (trending but thinner) + g_autoOptParams.pos_ranging_penalty = MathMax(0.30, g_autoOptParams.pos_ranging_penalty * (1.0 - 0.15 * tfSessionScale)); // Heavy cut: Asian ranging = whipsaw + } + else if(pairCat == "Major" || pairCat == "Cross" || pairCat == "Exotic" || pairCat == "Energy") + { + // Majors/Cross in Asian: mostly range-bound, very low ATR per candle + g_autoOptParams.pos_trending_bonus = MathMax(0.80, g_autoOptParams.pos_trending_bonus * (1.0 - 0.20 * tfSessionScale)); // Big cut: Asian trends rarely hold + g_autoOptParams.pos_ranging_penalty = MathMax(0.25, g_autoOptParams.pos_ranging_penalty * (1.0 - 0.25 * tfSessionScale)); // Heavy cut: ranging in Asian = noise + } + // Metal/Index: session filter already blocks Asian entries + } + // --- DEAD ZONE [22:00-01:00] --- + // Market rollover, maximum spread, minimum volume -- ALL categories cut + else if(hour >= 22 || hour < 1) + { + g_autoOptParams.pos_trending_bonus = MathMax(0.70, g_autoOptParams.pos_trending_bonus * (1.0 - 0.25 * tfSessionScale)); // Strong cut: dead zone trends are false + g_autoOptParams.pos_ranging_penalty = MathMax(0.25, g_autoOptParams.pos_ranging_penalty * (1.0 - 0.30 * tfSessionScale)); // Maximum penalty: range in dead zone = guaranteed loss + } + // --- MONDAY MORNING [dow==1, hour<8] --- + // Market still finding direction -- reduce all sizing until London opens + if(dow == 1 && hour < 8) + { + g_autoOptParams.pos_trending_bonus = MathMax(0.85, g_autoOptParams.pos_trending_bonus * (1.0 - 0.15 * tfSessionScale)); + g_autoOptParams.pos_ranging_penalty = MathMax(0.30, g_autoOptParams.pos_ranging_penalty * (1.0 - 0.20 * tfSessionScale)); + } + // --- FRIDAY PM [dow==5, hour>=18] --- + // Rollover risk, positions being closed -- size down aggressively + if(dow == 5 && hour >= 18) + { + g_autoOptParams.pos_trending_bonus = MathMax(0.70, g_autoOptParams.pos_trending_bonus * (1.0 - 0.30 * tfSessionScale)); // Friday trend = closing flows, not real trends + g_autoOptParams.pos_ranging_penalty = MathMax(0.25, g_autoOptParams.pos_ranging_penalty * (1.0 - 0.35 * tfSessionScale)); // Maximum cut: Friday range = random + } + } +} +//+------------------------------------------------------------------+ +//| Step 6: Auto-select strategies based on conditions | +//+------------------------------------------------------------------+ +void ApplyStrategySelection() +{ + ENUM_TF_CATEGORY tfCat = g_marketSnap.tf_category; + string pairCat = g_autoOptParams.pair_category; + bool trending = g_marketSnap.is_trending; + bool ranging = g_marketSnap.is_ranging; + // Scalping restriction + if(tfCat == TF_CAT_SCALP) + { + // For scalping, only allow in majors and tight-spread pairs + if(pairCat == "Exotic" || pairCat == "VolatileCross" || pairCat == "Crypto") + g_autoOptParams.allow_scalping = false; + } + // Trending market adjustments + if(trending) + { + g_autoOptParams.allow_bos_retest = true; + g_autoOptParams.allow_fvg_entry = true; + g_autoOptParams.allow_breaker_entry = true; + // Boost trend-following weight + g_autoOptParams.wp_trend_weight = MathMin(0.35, g_autoOptParams.wp_trend_weight + 0.05); + } + // Ranging market adjustments + if(ranging) + { + g_autoOptParams.allow_liq_grab = true; // LQ grabs work well in ranges + g_autoOptParams.allow_ote = true; + // Boost zone/confluence weights + g_autoOptParams.wp_zone_weight = MathMin(0.30, g_autoOptParams.wp_zone_weight + 0.05); + g_autoOptParams.wp_confluence_weight = MathMin(0.30, g_autoOptParams.wp_confluence_weight + 0.05); + // Reduce trend weight + g_autoOptParams.wp_trend_weight = MathMax(0.05, g_autoOptParams.wp_trend_weight - 0.05); + } + // FVG sizing based on actual ATR + double atrPoints = (_Point > 0) ? g_marketSnap.current_atr / _Point : 100; + g_autoOptParams.fvg_min_size = MathMax(2.0, atrPoints * 0.03); + // * v9.24 FIX#81a: OB volume mult -- ADJUST pair base, do NOT replace it. + // Step 2.5 (ApplyPairDetectionAdjustments) already set pair-specific values: + // Metal=1.2, Major=1.4, Index=1.3, VolatileCross=1.5, etc. + // The old code (ob_volume_mult = 1.3 or 1.8) ERASED those pair values every recalc. + // Fix: multiply the pair base by a volatility factor (+/-15%). + // VOL_LOW: x 0.85 (quiet market -> lower threshold, detect more OBs) + // VOL_HIGH: x 1.15 (volatile market -> raise threshold, only strongest OBs) + if(g_marketSnap.is_low_vol) + g_autoOptParams.ob_volume_mult = MathMax(1.0, g_autoOptParams.ob_volume_mult * 0.85); + else if(g_marketSnap.is_high_vol) + g_autoOptParams.ob_volume_mult = MathMin(2.5, g_autoOptParams.ob_volume_mult * 1.15); + // VOL_NORMAL: no change -- pair base stays as-is +} +//+------------------------------------------------------------------+ +//| Step 7: Apply aggressiveness scaling | +//+------------------------------------------------------------------+ +void ApplyAggressivenessScaling(double aggr) +{ + // aggr = 0.0 (most conservative) to 1.0 (most aggressive) + // Risk: conservative=50% of calc, aggressive=150% of calc + double riskScale = 0.5 + aggr * 1.0; + g_autoOptParams.risk_pct *= riskScale; + // * v9.16 FIX#44: Entry score no longer adjusted by aggressiveness -- EA_MinEntryScore is single source + // Confluence: conservative=stricter + double confAdj = (0.5 - aggr) * 0.15; // -0.075 to +0.075 + g_autoOptParams.min_confluence += confAdj; + // Max daily trades: conservative=fewer + if(aggr < 0.3) + g_autoOptParams.max_daily_trades = MathMax(1, g_autoOptParams.max_daily_trades - 1); + else if(aggr > 0.7) + g_autoOptParams.max_daily_trades += 1; + // Smart entry: conservative=higher thresholds + // * v9.16 FIX#44: REMOVED -- EA_MinEntryScore is single source of truth + g_autoOptParams.smart_min_win_prob += (0.5 - aggr) * 10.0; +} +//+------------------------------------------------------------------+ +//| Step 8: Safety clamps - enforce absolute limits | +//+------------------------------------------------------------------+ +void ClampAutoOptParameters() +{ + // * v7.5c: Category-aware SL multiplier MINIMUM + // M5 ATR on indices = $3-10 -> SL_mult 1.2x = $3.6-12 = noise level -> 80% SL hits + // Indices NEED wider SL to survive micro-fluctuations + double minSLMult = AutoOpt_MinSL_Mult; // Default: 1.2 + string cat = g_autoOptParams.pair_category; + if(cat == "Index") + minSLMult = MathMax(2.5, AutoOpt_MinSL_Mult); // Indices: min 2.5x ATR + else if(cat == "Metal") + minSLMult = MathMax(2.0, AutoOpt_MinSL_Mult); // Metals: min 2.0x ATR + else if(cat == "Energy") + minSLMult = MathMax(2.0, AutoOpt_MinSL_Mult); // Energy: min 2.0x ATR + else if(cat == "VolatileCross") + minSLMult = MathMax(1.8, AutoOpt_MinSL_Mult); // Volatile crosses: min 1.8x ATR + g_autoOptParams.sl_atr_mult = MathMax(minSLMult, MathMin(AutoOpt_MaxSL_Mult, g_autoOptParams.sl_atr_mult)); + // * v7.5c: TP mult must maintain minimum R:R relative to SL + double minTPMult = g_autoOptParams.sl_atr_mult * g_autoOptParams.min_rr; + g_autoOptParams.tp_atr_mult = MathMax(MathMax(AutoOpt_MinTP_Mult, minTPMult), MathMin(AutoOpt_MaxTP_Mult, g_autoOptParams.tp_atr_mult)); + // * v8.05 FIX: AutoOpt ceiling = EA_RiskPercent. + // * v9.02 FIX: Dynamic floor = max(AutoOpt_MinRiskOverride, 40% of EA_RiskPercent) + // Prevents cascade from reducing 5%->0.54% through TFx0.6 x Volx0.6 x Spreadx0.3 x Sessionx0.5. + // * FIX#4: Risk floor raised to EA_RiskPercent so AutoOpt pair/volatility profile + // cannot silently halve the user's configured risk (e.g. 1% -> 0.5%). + // _riskCap honours explicit safety overrides (when MaxRiskOverride < EA_RiskPercent). + // _riskFloor = EA_RiskPercent so the final risk is always the user's intended value. + // * v9.15 FIX#37: Risk clamp -- floor=MinRiskOverride, ceiling=MaxRiskOverride + // Old: floor=EA_RiskPercent = ceiling=EA_RiskPercent -> risk ALWAYS locked at 1%, AutoOpt useless + // New: floor=MinRiskOverride(0.5%), ceiling=MaxRiskOverride(2%) + // Session/volatility multipliers above now have real range: 0.5%-2.0% + // * v9.25 FIX#89: Pair-TF-aware risk floor for H4 Major/Cross. + // After cascade of multipliers (VOLxsessionxDoW), H4 Major risk was dropping to 0.65%. + // For H4+ on Major/Cross: floor = max(AutoOpt_MinRiskOverride, 80% of EA_RiskPercent). + // * v9.27 FIX#96: Score-aware AutoOpt risk ceiling + // Old: AutoOpt_MaxRiskOverride=2.0% blocked ALL trades regardless of quality. + // A+ score (>=90) = institutional-grade setup -> allow up to EA_AggRiskCap_APlus (5%). + // AutoOpt_MaxRiskOverride still applies for B/C/D trades as intended. + // g_ea_signal.score = current trade score (set by SelectBestCandidate before ExecuteTrade) + // * v9.49 FIX#210: PULL currentScore OUT so FIX#107 block can also read it (was local block). + // ROOT CAUSE: A+ score set _riskCap=5% but FIX#107 re-capped at EA_RiskPercent=1% → no effect. + int _qualityScore = (int)g_ea_signal.score; // FIX#210: shared across _riskCap and FIX#107 + double _riskCap; + { + int currentScore = _qualityScore; + double qualityCeiling = (currentScore >= 90) ? EA_AggRiskCap_APlus : + (currentScore >= 72) ? EA_AggRiskCap_A : + (AutoOpt_MaxRiskOverride > 0) ? AutoOpt_MaxRiskOverride : EA_RiskPercent; + // User AutoOpt_MaxRiskOverride is still respected as absolute ceiling for B/C/D + // but A/A+ can exceed it up to their quality ceiling + _riskCap = (currentScore >= 72) ? MathMax(qualityCeiling, AutoOpt_MaxRiskOverride) : qualityCeiling; + } + double _riskFloor; + { + bool isSwingMajor = ((g_autoOptParams.tf_category == TF_CAT_SWING || g_autoOptParams.tf_category == TF_CAT_POSITION) && + (g_autoOptParams.pair_category == "Major" || g_autoOptParams.pair_category == "Cross")); + if(isSwingMajor) + _riskFloor = MathMax(AutoOpt_MinRiskOverride, EA_RiskPercent * 0.80); // H4 Major: never below 80% of user risk + else + _riskFloor = (AutoOpt_MinRiskOverride > 0) ? AutoOpt_MinRiskOverride : EA_RiskPercent * 0.25; + } + if(_riskFloor > _riskCap) _riskFloor = _riskCap * 0.5; // guard paradox + g_autoOptParams.risk_pct = MathMax(_riskFloor, MathMin(_riskCap, g_autoOptParams.risk_pct)); + // * FIX#221: AutoOpt_MaxRiskOverride synced to EA_RiskPercent. + // Ο χρήστης βάζει EA_RiskPercent = MAX. Το AutoOpt δεν το κόβει. + // Τα tier fractions (FIX#221 στο CalculatePositionSizing) κατεβάζουν για κακά trades. + double _effectiveMaxOverride = EA_RiskPercent; // sync: max = αυτό που έβαλε ο χρήστης + // * FIX#107 / FIX#221: AutoOpt risk_pct clamped στο [1.0%, EA_RiskPercent]. + // Ο χρήστης βάζει EA_RiskPercent = MAX. AutoOpt μπορεί να κατεβάσει (session/spread) + // αλλά ποτέ κάτω από 1.0% και ποτέ πάνω από EA_RiskPercent. + if(AutoOpt_MaxRiskOverride > 0.0) + { + // FIX#221: ceiling = EA_RiskPercent (όχι AutoOpt_MaxRiskOverride που μπορεί να είναι λάθος) + g_autoOptParams.risk_pct = MathMin(g_autoOptParams.risk_pct, _effectiveMaxOverride); + // Hard floor: ποτέ κάτω από 1% (αντιστοιχεί στο PosSize_MinRisk default) + g_autoOptParams.risk_pct = MathMax(1.0, g_autoOptParams.risk_pct); + if(g_verboseLog) + { + static double _s107LastRisk = -1; + if(g_autoOptParams.risk_pct != _s107LastRisk) + { + PrintFormat("* FIX#107 AutoOpt Risk: %.2f%% (floor=1.0%% max=%.2f%% | EA_base=%.2f%%)", + g_autoOptParams.risk_pct, _effectiveMaxOverride, EA_RiskPercent); + _s107LastRisk = g_autoOptParams.risk_pct; + } + } + } + else + { + g_autoOptParams.risk_pct = MathMax(1.0, MathMin(g_workingRiskCeiling, g_autoOptParams.risk_pct)); + if(g_verboseLog) + { + static double _s915LastRisk = -1; + if(g_autoOptParams.risk_pct != _s915LastRisk) + { + PrintFormat("* FIX#107 AutoOpt Risk: %.2f%% (floor=1.0%% max=%.2f%% | EA_base=%.2f%%)", + g_autoOptParams.risk_pct, g_workingRiskCeiling, EA_RiskPercent); + _s915LastRisk = g_autoOptParams.risk_pct; + } + } + } + g_autoOptParams.min_rr = MathMax(1.0, MathMin(5.0, g_autoOptParams.min_rr)); + // * v9.11 FIX#19: CRITICAL -- min_rr must NOT exceed achievable R:R from TP/SL + // Without this, Category "Major" sets min_rr=2.0 but after INTRADAY TPx0.90, + // actual R:R = TP/SL = 2.70/1.50 = 1.80 -> min_rr=2.0 blocks ALL trades! + // Fix: cap min_rr at 85% of theoretical R:R (15% margin for spread) + if(g_autoOptParams.sl_atr_mult > 0) + { + double achievableRR = g_autoOptParams.tp_atr_mult / g_autoOptParams.sl_atr_mult; + double maxMinRR = achievableRR * 0.85; // 15% buffer for spread + if(g_autoOptParams.min_rr > maxMinRR) + { + if(g_verboseLog) + { // * v9.31 FIX#120B: only log when clamped value changes + static double _s19LastRR = -1; + if(MathAbs(maxMinRR - _s19LastRR) > 0.001) + { + PrintFormat("* v9.11 FIX#19 min_rr CLAMPED: %.2f -> %.2f (achievable=%.2f from TP=%.2f/SL=%.2f)", + g_autoOptParams.min_rr, maxMinRR, achievableRR, + g_autoOptParams.tp_atr_mult, g_autoOptParams.sl_atr_mult); + _s19LastRR = maxMinRR; + } + } + g_autoOptParams.min_rr = maxMinRR; + } + } + g_autoOptParams.max_daily_trades = MathMax(1, MathMin(EA_MaxDailyTrades, g_autoOptParams.max_daily_trades)); // * v7.4: cap = GLOBAL input + g_autoOptParams.min_confluence = MathMax(0.30, MathMin(0.95, g_autoOptParams.min_confluence)); + // * FIX#462: SmartEntry_MinConfidence was only in Print — never gated real trading. + // Fix: use it as absolute floor for min_entry_quality (AutoOpt cannot go below it). + // Default=36 → same as current behavior. User raises it to tighten entry quality globally. + g_autoOptParams.min_entry_quality = MathMax((double)SmartEntry_MinConfidence, MathMin(95.0, g_autoOptParams.min_entry_quality)); + // * FIX#464: AutoOpt_MinScoreFloor and AutoOpt_MaxScoreCap were only in comments/Print — never gated trading. + // MinScoreFloor=40: absolute floor for min_entry_quality (nothing below 40 score). + // MaxScoreCap=85: absolute ceiling for score_cap (score above 85 = "too perfect" = liquidity trap). + // Apply AFTER SmartEntry_MinConfidence floor so they compound correctly. + if(AutoOpt_MinScoreFloor > 0) + g_autoOptParams.min_entry_quality = MathMax(g_autoOptParams.min_entry_quality, (double)AutoOpt_MinScoreFloor); + if(AutoOpt_MaxScoreCap > 0 && g_autoOptParams.score_cap == 0) + g_autoOptParams.score_cap = AutoOpt_MaxScoreCap; // only if pair table didn't set a specific cap + g_autoOptParams.min_entry_score = (int)EA_MinEntryScore; // * v9.16 FIX#44: SINGLE SOURCE -- no AutoOpt override + // * v8.08 BUG#2/#5 FIX: Floor at user inputs. AutoOpt can make thresholds STRICTER but NEVER weaker. + // * v9.16 FIX#43a: CRITICAL CLAMP BUG -- MathMin(50) capped value BELOW EA_MinEntryScore(60). + // Old: MathMax(60, MathMin(50, val)) = MathMax(60, 50) = ALWAYS 60 -> ALL AutoOpt tweaks erased! + // Fix: Cap at 85 (max score), floor at EA_MinEntryScore. Now session/TF/spread adjustments work. + g_autoOptParams.smart_min_confidence = (int)EA_MinEntryScore; // * v9.16 FIX#44: SINGLE SOURCE -- no AutoOpt override + g_autoOptParams.smart_min_win_prob = MathMax(SmartEntry_MinWinProb, MathMin(80.0, g_autoOptParams.smart_min_win_prob)); // * v9.03: was hardcoded 52.0 floor -> user's SmartEntry_MinWinProb is the real floor + g_autoOptParams.smart_min_ev = MathMax(0.01, MathMin(1.0, g_autoOptParams.smart_min_ev)); + // FVG bounds + g_autoOptParams.fvg_min_size = MathMax(1.0, MathMin(500.0, g_autoOptParams.fvg_min_size)); + g_autoOptParams.fvg_max_age = MathMax(10, MathMin(500, g_autoOptParams.fvg_max_age)); + g_autoOptParams.ob_max_age = MathMax(10, MathMin(500, g_autoOptParams.ob_max_age)); + g_autoOptParams.liq_max_age = MathMax(20, MathMin(1000, g_autoOptParams.liq_max_age)); + // Swing strengths + g_autoOptParams.struct_swing_strength = MathMax(1, MathMin(15, g_autoOptParams.struct_swing_strength)); + g_autoOptParams.liq_swing_strength = MathMax(2, MathMin(20, g_autoOptParams.liq_swing_strength)); + // * v9.03 FIX#11: Detection param clamps + g_autoOptParams.ote_max_age = MathMax(10, MathMin(300, g_autoOptParams.ote_max_age)); + g_autoOptParams.bb_max_age = MathMax(10, MathMin(200, g_autoOptParams.bb_max_age)); + g_autoOptParams.mb_max_age = MathMax(10, MathMin(200, g_autoOptParams.mb_max_age)); + g_autoOptParams.trendline_max_age = MathMax(10, MathMin(300, g_autoOptParams.trendline_max_age)); + g_autoOptParams.crt_lookback = MathMax(5, MathMin(100, g_autoOptParams.crt_lookback)); + g_autoOptParams.crt_expiry = MathMax(3, MathMin(60, g_autoOptParams.crt_expiry)); + g_autoOptParams.tbs_expiry = MathMax(3, MathMin(50, g_autoOptParams.tbs_expiry)); + g_autoOptParams.amd_accum_max_bars = MathMax(5, MathMin(100, g_autoOptParams.amd_accum_max_bars)); + g_autoOptParams.sb_max_age = MathMax(3, MathMin(60, g_autoOptParams.sb_max_age)); + g_autoOptParams.signal_expiry_bars = MathMax(3, MathMin(50, g_autoOptParams.signal_expiry_bars)); + g_autoOptParams.fvg_min_strength = MathMax(0.10, MathMin(0.80, g_autoOptParams.fvg_min_strength)); + g_autoOptParams.regime_lookback = MathMax(5, MathMin(50, g_autoOptParams.regime_lookback)); + g_autoOptParams.regime_confirm_bars = MathMax(1, MathMin(10, g_autoOptParams.regime_confirm_bars)); + g_autoOptParams.divergence_lookback = MathMax(5, MathMin(50, g_autoOptParams.divergence_lookback)); + g_autoOptParams.trendline_lookback = MathMax(5, MathMin(80, g_autoOptParams.trendline_lookback)); + g_autoOptParams.fib_lookback = MathMax(10, MathMin(200, g_autoOptParams.fib_lookback)); + // RSI bounds + g_autoOptParams.rsi_overbought = MathMax(60, MathMin(85, g_autoOptParams.rsi_overbought)); + g_autoOptParams.rsi_oversold = MathMax(15, MathMin(40, g_autoOptParams.rsi_oversold)); + // Win probability weights must sum to ~1.0 + double wpSum = g_autoOptParams.wp_trend_weight + g_autoOptParams.wp_structure_weight + + g_autoOptParams.wp_zone_weight + g_autoOptParams.wp_confluence_weight + + g_autoOptParams.wp_timing_weight + g_autoOptParams.wp_pattern_weight; + if(wpSum > 0 && MathAbs(wpSum - 1.0) > 0.01) + { + g_autoOptParams.wp_trend_weight /= wpSum; + g_autoOptParams.wp_structure_weight /= wpSum; + g_autoOptParams.wp_zone_weight /= wpSum; + g_autoOptParams.wp_confluence_weight /= wpSum; + g_autoOptParams.wp_timing_weight /= wpSum; + g_autoOptParams.wp_pattern_weight /= wpSum; + } + // * v9.24 FIX#82: Clamp new params + // TC RSI (user values = ceiling for min, floor for max) + g_autoOptParams.tc_rsi_min = MathMax(30.0, MathMin((double)TC_RSI_Min, g_autoOptParams.tc_rsi_min)); + g_autoOptParams.tc_rsi_max = MathMin(70.0, MathMax((double)TC_RSI_Max, g_autoOptParams.tc_rsi_max)); + if(g_autoOptParams.tc_rsi_min >= g_autoOptParams.tc_rsi_max - 4.0) + g_autoOptParams.tc_rsi_min = g_autoOptParams.tc_rsi_max - 4.0; // min 4pt gap + g_autoOptParams.tc_min_slope_atr = MathMax(0.01, MathMin(0.20, g_autoOptParams.tc_min_slope_atr)); + g_autoOptParams.tc_base_score = MathMax(10.0, MathMin(50.0, g_autoOptParams.tc_base_score)); + // TP R:Rs -- user inputs = absolute floor + g_autoOptParams.tp1_rr = MathMax(InpTP1_RR, MathMin(10.0, g_autoOptParams.tp1_rr)); + g_autoOptParams.tp2_rr = MathMax(InpTP2_RR, MathMin(15.0, g_autoOptParams.tp2_rr)); + g_autoOptParams.tp3_rr = MathMax(InpTP3_RR, MathMin(20.0, g_autoOptParams.tp3_rr)); + // Judas/TBS TP R:Rs -- user inputs = absolute floor + g_autoOptParams.judas_tp1_rr = MathMax(Judas_TP1_RR, MathMin(8.0, g_autoOptParams.judas_tp1_rr)); + g_autoOptParams.judas_tp2_rr = MathMax(Judas_TP2_RR, MathMin(12.0, g_autoOptParams.judas_tp2_rr)); + g_autoOptParams.judas_tp3_rr = MathMax(Judas_TP3_RR, MathMin(16.0, g_autoOptParams.judas_tp3_rr)); + g_autoOptParams.tbs_tp1_rr = MathMax(TBS_TP1_RR, MathMin(6.0, g_autoOptParams.tbs_tp1_rr)); + g_autoOptParams.tbs_tp2_rr = MathMax(TBS_TP2_RR, MathMin(10.0, g_autoOptParams.tbs_tp2_rr)); + g_autoOptParams.tbs_tp3_rr = MathMax(TBS_TP3_RR, MathMin(14.0, g_autoOptParams.tbs_tp3_rr)); + // MTF confidence -- user input = ceiling (AutoOpt can only lower for easier pairs) + g_autoOptParams.mtf_min_confidence = MathMax(50.0, MathMin(MTF_MinConfidence, g_autoOptParams.mtf_min_confidence)); + // Regime thresholds -- user input = ceiling (AutoOpt can only lower for higher TFs) + g_autoOptParams.regime_trend_threshold = MathMax(35.0, MathMin(Regime_TrendThreshold, g_autoOptParams.regime_trend_threshold)); + g_autoOptParams.regime_trend_adx_min = MathMax(15.0, MathMin(Regime_TrendADXMin, g_autoOptParams.regime_trend_adx_min)); + // CRT ranges + g_autoOptParams.crt_min_range_atr = MathMax(0.20, MathMin(2.0, g_autoOptParams.crt_min_range_atr)); + g_autoOptParams.crt_max_range_atr = MathMax(g_autoOptParams.crt_min_range_atr + 1.0, MathMin(10.0, g_autoOptParams.crt_max_range_atr)); + // VSA strength -- user input = ceiling (pair may lower it for noisy pairs) + g_autoOptParams.vsa_min_strength = MathMax(35.0, MathMin(VSA_MinStrength, g_autoOptParams.vsa_min_strength)); + // WP threshold -- user input is ceiling; pair/vol can lower it + g_autoOptParams.wp_min_threshold = MathMax(0.40, MathMin(WinProb_MinThreshold, g_autoOptParams.wp_min_threshold)); + // * v9.27 FIX#96: pos sizing bounds -- raise cap when uncapping enabled for quality trades + // Old: always MathMin(1.5) -> prevented A+ trending bonus from reaching 1.5x1.2=1.80 after TF scaling + double posBonusCap = PosSize_WinStreak_Uncap ? 3.0 : 1.5; // uncap enabled: allow 3.0x for stacked TF/session/streak + g_autoOptParams.pos_trending_bonus = MathMax(1.0, MathMin(posBonusCap, g_autoOptParams.pos_trending_bonus)); + g_autoOptParams.pos_ranging_penalty = MathMax(0.25, MathMin(1.0, g_autoOptParams.pos_ranging_penalty)); +} +//+------------------------------------------------------------------+ +//| * v9.03: Apply TF-aware position management to working globals | +//| Called after ClampAutoOptParameters in the AutoOpt pipeline | +//+------------------------------------------------------------------+ +void ApplyPositionManagementOverrides() +{ + // Clamp position management params to safe ranges + // * v9.29 FIX#98e: REMOVED global 1.0R floor for BE. + // OLD: MathMax(EA_BreakEven_RR, 1.0) forced BE >= 1.0R even for H4 where TP1=1.34R -> BE activated AFTER TP1 (impossible). + // NEW: use EA_BreakEven_RR directly as the floor (user input = 0.7R for H4), ceiling still 3.5R. + // * v9.30 FIX#101: Global BE floor uses autoopt value directly (already dynamic) + // Per-trade calc (FIX#100) is primary -- this is only the global fallback + g_autoOptParams.breakeven_rr = MathMax(0.20, MathMin(3.5, g_autoOptParams.breakeven_rr)); + // v9.16 REMOVED: g_autoOptParams.trail_start_rr = MathMax(1.0, MathMin(4.0, g_autoOptParams.trail_start_rr)); + // v9.16 REMOVED: g_autoOptParams.trail_stop_atr = MathMax(0.3, MathMin(3.0, g_autoOptParams.trail_stop_atr)); + g_autoOptParams.smart_exit_min_rr = MathMax(0.5, MathMin(3.0, g_autoOptParams.smart_exit_min_rr)); + g_autoOptParams.smart_exit_signals = MathMax(2, MathMin(5, g_autoOptParams.smart_exit_signals)); + // v9.16 REMOVED: g_autoOptParams.trail_volatile_start = MathMax(1.0, MathMin(4.0, g_autoOptParams.trail_volatile_start)); + // v9.16 REMOVED: g_autoOptParams.trail_volatile_dist = MathMax(0.3, MathMin(2.5, g_autoOptParams.trail_volatile_dist)); + // v9.16 REMOVED: g_autoOptParams.trail_trending_start = MathMax(1.0, MathMin(4.0, g_autoOptParams.trail_trending_start)); + // v9.16 REMOVED: g_autoOptParams.trail_trending_dist = MathMax(0.5, MathMin(3.0, g_autoOptParams.trail_trending_dist)); + if(g_verboseLog) + Print("* v9.03 PosMgmt: BE=", DoubleToString(g_autoOptParams.breakeven_rr, 2), "R", + " | Trail=", DoubleToString(g_autoOptParams.trail_start_rr, 2), "R/", + DoubleToString(g_autoOptParams.trail_stop_atr, 2), "xATR", + " | SmartExit=", DoubleToString(g_autoOptParams.smart_exit_min_rr, 2), "R/", + g_autoOptParams.smart_exit_signals, "sig"); +} +//+------------------------------------------------------------------+ +//| Apply auto-optimized params to working variables | +//+------------------------------------------------------------------+ +void ApplyAutoOptToWorkingVars() +{ + if(!AutoOpt_Enabled || !AutoOpt_AutoParameters) return; + // Core trading (indicator pipeline) + g_workingSL_ATRMultiplier = g_autoOptParams.sl_atr_mult; + g_workingTP_ATRMultiplier = g_autoOptParams.tp_atr_mult; + // min_rr already set correctly by ApplyPairTFProfile (pair table → EA_MinRR fallback) + g_workingMinRiskReward = g_autoOptParams.min_rr; + g_workingMinConfluence = g_autoOptParams.min_confluence; + // * FIX#348: g_workingMinEntryQuality moved to AFTER FIX#346 cap block (see below). + // FVG + // * v10.01 FIX#236: Apply TF-aware scaling to fvg_min_size so H4/D1 FVGs are detected. + // BUG: g_autoOptParams.fvg_min_size = max(2.0, atrPoints * 0.03) uses a flat coefficient. + // For XAUUSD H4: ATR≈2000pts → fvg_min_size≈60pts ($0.60). AdaptParametersToTimeframe() + // applies H4 *2.0 to g_workingFVG_MinSize at startup, but ApplyAutoOptToWorkingVars() is + // called on every UpdateAutoOptimization cycle → RESETS g_workingFVG_MinSize back to 60pts, + // losing the H4 multiplier → FVG minimum threshold too low relative to the bars actually + // scanned, causing the scanner to fail to find qualifying FVGs in the wrong direction. + // Actual backtest evidence: XAUUSD H4 showed FVG=0 on every single bar. + // FIX: re-apply TF coefficient after copying from autoOptParams. + { + double tfFVGMult = 1.0; + switch(Period()) + { + case PERIOD_M1: tfFVGMult = 0.5; break; + case PERIOD_M5: tfFVGMult = 0.8; break; + case PERIOD_M15: tfFVGMult = 1.0; break; + case PERIOD_M30: tfFVGMult = 1.2; break; + case PERIOD_H1: tfFVGMult = 1.5; break; + case PERIOD_H4: tfFVGMult = 2.0; break; + case PERIOD_D1: tfFVGMult = 3.0; break; + case PERIOD_W1: tfFVGMult = 5.0; break; + default: tfFVGMult = 1.0; break; + } + g_workingFVG_MinSize = g_autoOptParams.fvg_min_size * tfFVGMult; + } + g_workingFVG_MaxAge = g_autoOptParams.fvg_max_age; + g_workingFVG_ExtendBars = g_autoOptParams.fvg_extend_bars; + // Sessions + g_workingUseSessionFilter = g_autoOptParams.use_session_filter; + g_workingSessionLondon = g_autoOptParams.session_london; + g_workingSessionNewYork = g_autoOptParams.session_ny; + g_workingSessionAsian = g_autoOptParams.session_asian; + if(!SessionAsian) g_workingSessionAsian = false; + if(!SessionLondon) g_workingSessionLondon = false; + if(!SessionNewYork) g_workingSessionNewYork = false; + // Filters + g_workingRSI_Overbought = g_autoOptParams.rsi_overbought; + g_workingRSI_Oversold = g_autoOptParams.rsi_oversold; + // * v7.4 FIX: AutoOpt spread override now applied to working variable + // Was: "REMOVED" with a TODO -- now fully implemented + if(g_autoOptParams.max_spread_pips > 0) + { + // * v9.31 FIX#119B: Bidirectional sync -- pair profile IS the correct spread for this pair. + // Old: only increased (if autoOpt > current). Bug: if EA_MaxSpreadPips=50 (user set high) + // and EURUSD pair profile = 5 pips, g_workingMaxSpreadPips stayed at 50 -> 30-pip spreads allowed! + // Fix: always assign directly from pair profile (which was set correctly in ApplyPairTFProfile). + g_workingMaxSpreadPips = g_autoOptParams.max_spread_pips; + } + // * v7.5b FIX: Apply auto-opt OB volume multiplier + // Was: g_autoOptParams.ob_volume_mult was SET but NEVER APPLIED + // DetectOrderBlocks used raw input (1.8) -> too strict -> 0 OBs on Gold M5 + if(g_autoOptParams.ob_volume_mult > 0) + g_workingOBVolumeMult = g_autoOptParams.ob_volume_mult; + // * v9.03 FIX#13: Previously missing connections -- AutoOpt set these but never applied! + g_workingOB_MaxAge = g_autoOptParams.ob_max_age; + g_workingLIQ_MaxAge = g_autoOptParams.liq_max_age; + g_workingSTRUCT_SwingStrength = g_autoOptParams.struct_swing_strength; + g_workingLIQ_SwingStrength = g_autoOptParams.liq_swing_strength; + // * v9.03 FIX#11: Detection param working globals + g_workingOTE_MaxAge = g_autoOptParams.ote_max_age; + g_workingBB_MaxAge = g_autoOptParams.bb_max_age; + g_workingMB_MaxAge = g_autoOptParams.mb_max_age; + g_workingTrendline_MaxAge = g_autoOptParams.trendline_max_age; + g_workingCRT_Lookback = g_autoOptParams.crt_lookback; + g_workingCRT_Expiry = g_autoOptParams.crt_expiry; + g_workingTBS_Expiry = g_autoOptParams.tbs_expiry; + g_workingAMD_AccumMaxBars = g_autoOptParams.amd_accum_max_bars; + g_workingSB_MaxAge = g_autoOptParams.sb_max_age; + g_workingSignalExpiryBars = g_autoOptParams.signal_expiry_bars; + g_workingFVG_MinStrength = g_autoOptParams.fvg_min_strength; + g_workingRegime_Lookback = g_autoOptParams.regime_lookback; + g_workingRegime_ConfirmBars = g_autoOptParams.regime_confirm_bars; + g_workingDivergence_Lookback = g_autoOptParams.divergence_lookback; + g_workingTrendline_Lookback = g_autoOptParams.trendline_lookback; + g_workingFIB_Lookback = g_autoOptParams.fib_lookback; + // =============================================================== + // * v6.3 FIX: BRIDGE AutoOpt -> EA Trading Pipeline + // AutoOpt was ONLY writing to g_working* (indicator pipeline). + // EA trading reads g_pairThresholds -> was NEVER getting AutoOpt values! + // Now AutoOpt ALSO writes to g_pairThresholds so EA candidates, + // SelectBestCandidate, and EvaluateSmartEntry all benefit. + // =============================================================== + // -- SL/TP Multipliers -> BuildCandidateSLTP reads these -- + // * v9.16 FIX#46a: TF+pair-aware TP2/TP3 spacing + { + double tp2R = 1.35, tp3R = 1.75; + GetTP2TP3Ratios(g_autoOptParams.tf_category, g_autoOptParams.pair_category, tp2R, tp3R); + } + // -- R:R -> SelectBestCandidate reads this -- + // -- Risk % -> EA_ExecuteTrade reads this -- + // * v7.9 FIX BUG#7: Inform user when AutoOpt session/vol penalty reduces risk below EA_RiskPercent + // * v9.56 FIX#222: Demoted from [WARN] to [INFO] — session reduction is now expected/correct behavior. + if(g_autoOptParams.risk_pct < EA_RiskPercent) + { // * v9.31 FIX#120D: add ShowLog guard + only when value changes (was: every recalc = spam) + if(g_verboseLog) + { + static double _s79LastRisk = -1; + if(MathAbs(g_autoOptParams.risk_pct - _s79LastRisk) > 0.001) + { + PrintFormat("[INFO] FIX#222 AutoOpt session penalty: EA_RiskPercent=%.2f%% -> using %.2f%% " \ + "(session/volatility reduction | floor=1.0%% via FIX#221)", + EA_RiskPercent, g_autoOptParams.risk_pct); + _s79LastRisk = g_autoOptParams.risk_pct; + } + } + } + // -- Daily trades -> OnTick maxDailyTrades reads this -- + // -- Entry quality -> SelectBestCandidate composite check -- + // * v9.16 FIX#44: SINGLE SOURCE -- EA_MinEntryScore controls ALL score gates. + // AutoOpt no longer manipulates min_entry_score. Pair floor is still respected. + { + double pairFloor = g_gates.minScore; + } + // -- Confirmation thresholds -- + // AutoOpt doesn't set these directly, keep pair-profile values + // -- FVG/OB quality thresholds -> EA_CheckSignals reads these -- + // Map AutoOpt confluence to FVG quality + // * v9.38 FIX#165: CONFLUENCE CASCADE CAP + // Root cause of 1-trade-in-35-days: min_confluence compounds across 5 independent steps: + // Base(Major)=0.60 x TF_SWING(1.10) x VOL_VERY_LOW(1.15) x WEAK_regime(1.08) x Asian(1.08) = 0.885 + // Result: 0.885 >= 0.80 -> FVG_QUALITY_PREMIUM required + OBStrength >= 0.885. + // This silently kills ~85-90% of FVG/OB candidates BEFORE AddCandidate() with zero log output. + // Fix A: Hard cap on min_confluence BEFORE mapping -- cascade can never exceed this ceiling. + // H4+: cap=0.75 (never forces PREMIUM on swing TF -- FVG touches are already rare). + // M15/H1: cap=0.79 (tighter but still allows some PREMIUM in truly extreme conditions). + // Fix B: Decouple OBStrength from min_confluence -- OB strength uses its own TF-aware cap. + // OB strength 0-1 has different semantics (volume confluence) than min_confluence (zone quality). + // H4 OBs are structure-based, not volume-based -- 0.65+ is a strong OB at this TF. + { + double confluenceCap; + if(_Period >= PERIOD_H4) + confluenceCap = 0.65; + else if(_Period >= PERIOD_H1) + confluenceCap = 0.77; + else + confluenceCap = 0.79; + // Pair table min_confluence_cap overrides FIX#165 TF default when set + { + string _s = _Symbol; StringToUpper(_s); + int _d = StringFind(_s,"."); if(_d>0) _s=StringSubstr(_s,0,_d); + if(StringLen(_s)>6) _s=StringSubstr(_s,0,6); + int _t=0; + switch(Period()){case PERIOD_M1:case PERIOD_M5:_t=0;break;case PERIOD_M15:case PERIOD_M30:_t=1;break;case PERIOD_H1:_t=2;break;case PERIOD_H4:_t=3;break;default:_t=4;} + PairTFConfig _c = GetPairTFConfig(_s); + if(_c.min_confluence_cap[_t] > 0) confluenceCap = _c.min_confluence_cap[_t]; + } + if(g_autoOptParams.min_confluence > confluenceCap) + { + // * v9.39 FIX#171: once-per-bar log guard. + static datetime s_fix165LastBar = 0; + datetime _fix165Bar = iTime(_Symbol, PERIOD_CURRENT, 0); + bool _fix165First = (_fix165Bar != s_fix165LastBar); + if(_fix165First) s_fix165LastBar = _fix165Bar; + if(_fix165First && AutoOpt_ShowLog) + PrintFormat("[FIX#165] min_confluence capped %.3f -> %.3f (TF=%s | cascade would require PREMIUM FVGs + OB>=%.2f -> zero candidates)", + g_autoOptParams.min_confluence, confluenceCap, + EnumToString(_Period), g_autoOptParams.min_confluence); + g_autoOptParams.min_confluence = confluenceCap; + } + // * FIX#394: Apply pair table FLOOR after the ceiling cap. + // BUG: FVG=0 on H4 (intentional FIX#203) → cascade cap=2 always fires. + // FIX#165 reduces min_confluence every bar toward confluenceCap (0.65). + // AutoOpt can then further reduce it via session/spread adjustments. + // Result: min_confluence drifts down far below what the pair needs. + // EURUSD H4 was calibrated at min_conf=0.48 (pair table) — if AutoOpt + // reduces to 0.30, low-quality OBs pass that should be rejected. + // FIX: After all AutoOpt adjustments, floor = max(current, pair_min_conf×0.90). + // 10% margin below pair table (allows some intraday flexibility). + // Uses g_currentPairProfile.minConfluence (set from pair profile in UpdatePairProfile). + if(g_autoOptParams.min_confluence > 0) + { + double _confFloor394 = g_autoOptParams.min_confluence * 0.90; + if(g_autoOptParams.min_confluence < _confFloor394) + { + if(g_verboseLog) + PrintFormat("[FIX#394] min_confluence FLOORED: %.3f → %.3f (pair floor=%.3f×0.90)", + g_autoOptParams.min_confluence, _confFloor394, + g_autoOptParams.min_confluence); + g_autoOptParams.min_confluence = _confFloor394; + } + } + } + // [retired] minFVGQuality block + // * v9.38 FIX#165B: Decouple OB strength from min_confluence with TF-aware cap. + // Previously: minOBStrength = MathMax(0.50, min_confluence) -- after cascade, this reached 0.87! + // OB strength scale is independent: 0.65+ is a strong H4 OB (structure-based, not volume). + // Requiring 0.87 eliminated virtually all valid OBs. + { + double obStrCap; + if(_Period >= PERIOD_H4) + obStrCap = 0.58; // * v10.03 FIX#262: H4 obStrCap 0.65→0.58. H4 OBs are structure-based (not volume). Natural strength 0.55-0.65. Old cap 0.65 excluded ALL valid OBs (Conf=2 in diagnostics = all rejected here). ICT H4 OB touch at 0.58+ = strong structural zone. + else if(_Period >= PERIOD_H1) + obStrCap = 0.62; // * v10.03 FIX#262: H1 obStrCap 0.68→0.62 (H1 OBs also intraswing-based, 0.60-0.66 is strong) + else + obStrCap = 0.68; // M15/M30: keep slightly stricter (more volume-based confluence available) + double rawOBStr = MathMax(0.50, g_autoOptParams.min_confluence); + } + // -- * v7.5 FIX: WRITE-BACK allow_scalping/allow_swing to pair profile -- + // AutoOpt dynamically decides scalping/swing viability based on spread, volatility, + // pair category, and timeframe. The result MUST propagate to g_currentPairProfile + // because EA_CheckSignals() and IsTradAllowedForPair() read from there. + // Without this bridge, hardcoded pair profile values override auto-opt decisions. + // * v9.40 FIX-A: Hard cap -- prevents min_entry_quality > 85 (max score) = zero trades forever + // Base(65) + TF(+4..+10) + Vol(+5..+15) + Spread(+8..+15) can sum to 85+ on H4/M5 + // * FIX#346: TF-aware AutoOpt quality cap (was flat 72 for ALL TFs). + // ROOT CAUSE: M15 pair_table=60 + VOL_EXTREME(+12) + spread(+8) = 80 → capped flat at 72. + // This forced effective threshold to 72 regardless of calibration. + // Fix: cap = min(pairTableBase + 15, 85). If pair table set 60, max AutoOpt push = 75. + // If no pair table override (min_conf_override=0), keep flat cap at 72 (category default). + { + double _pairBase = g_autoOptParams.min_conf_override; // 0 if no pair table value + double _qualCap = (_pairBase > 0) ? MathMin(_pairBase + 15.0, 85.0) : 72.0; + if(g_autoOptParams.min_entry_quality > _qualCap) + g_autoOptParams.min_entry_quality = _qualCap; + } + // * FIX#348: assign AFTER FIX#346 cap — working var gets correct capped value. + g_workingMinEntryQuality = g_autoOptParams.min_entry_quality; + // -- Category from pair profile (keep existing) -- + // [retired] category sync — g_autoOptParams.pair_category is set by ApplyPairTFProfile + // Mark as loaded so EA pipeline uses these values + // g_gates.computed retired — g_gates.computed serves this purpose + // * v9.03: Position Management Override (TF-aware) + g_workingBE_RR = g_autoOptParams.breakeven_rr; + g_workingBE_Ranging_RR = MathMin(g_autoOptParams.breakeven_rr, EA_BE_Ranging_RR); // Ranging: tighter or same + g_workingBE_Volatile_RR = MathMax(g_autoOptParams.breakeven_rr, EA_BE_Volatile_RR); // Volatile: wider or same + // * v10.08 FIX#278: TP2 BE threshold now AutoOpt-aware. + // OLD: EA_TP2_BE_Threshold was always read raw from input → not TF-scaled by AutoOpt. + // FIX: compute as MAX(input, tp2_rr * 0.85) — ensures TP2 BE fires only when + // TP2 is nearly reached (85% of the way). For H4: tp2_rr≈2.4 → floor=2.04R. + // EA_TP2_BE_Threshold=2.5 (input) still wins if larger — user ceiling respected. + { + double tp2BEFloor = (g_autoOptParams.tp1_rr > 0) + ? g_autoOptParams.tp2_rr * 0.85 + : EA_TP2_BE_Threshold; + g_workingBE_TP2_RR = MathMax(EA_TP2_BE_Threshold, tp2BEFloor); + } + // * v9.16: Trail params are now UNIVERSAL per-tranche inputs (EA_Trail_TP1/TP2/TP3_ATR) + // AutoOpt no longer controls trailing -- removed to prevent overlap + // g_workingTrailStart_RR = g_autoOptParams.trail_start_rr; // REMOVED v9.16 + // g_workingTrailStop_ATR = g_autoOptParams.trail_stop_atr; // REMOVED v9.16 + // * FIX#303: g_workingSmartExit_MinRR may already be set by ApplyPairTFProfile (per-TF override). + // Only update from AutoOpt if AutoOpt value is higher (per-TF floor wins, AutoOpt can raise it further). + if(g_autoOptParams.smart_exit_min_rr > g_workingSmartExit_MinRR) + g_workingSmartExit_MinRR = g_autoOptParams.smart_exit_min_rr; + g_workingSmartExit_Signals = g_autoOptParams.smart_exit_signals; + // * v9.24 FIX#80: Sync FIX41 trail + SmartExit thresholds with AutoOpt intelligence + // Problem: ProfitGuardTrail_FIX41 used EA_Trail_Activation_RR (hardcoded input) directly, + // ignoring AutoOpt's pair/TF/volatility-aware breakeven_rr calculation. + // Example: M15 intraday -> AutoOpt sets breakeven_rr=1.5 (tested), FIX41 used 1.2 (hardcoded). + // FIX#77 (context-aware BE) also used the hardcoded value -> wrong threshold on all pairs/TFs. + // Fix: g_workingFIX41_TrailStart mirrors g_workingBE_RR (AutoOpt output), + // but respects EA_Trail_Activation_RR as a floor (user input is a minimum, not override). + // SmartExit threshold scales proportionally with AutoOpt's smart_exit_min_rr. + // SmartExit RR: use g_workingFIX41_SmartExitRR (already set by FIX#303 if per-TF override active) + // Floor at AutoOpt value but respect per-TF calibration + g_workingFIX41_SmartExitRR = MathMin(g_workingFIX41_SmartExitRR, g_autoOptParams.smart_exit_min_rr); + // Score threshold: scale with vol regime (higher vol = need more evidence before exiting) + { + int baseScore = 15; + if(g_marketSnap.vol_regime >= VOL_HIGH) + baseScore = (int)MathRound(baseScore * 1.3); // High vol: harder to trigger exit (wider swings = false signals) + else if(g_marketSnap.vol_regime <= VOL_LOW) + baseScore = (int)MathRound(baseScore * 0.85); // Low vol: easier trigger (clean moves) + g_workingFIX41_ScoreThresh = MathMax(8, baseScore); // Safety floor: never below 8 + } + // g_workingTrail_Volatile_Start = g_autoOptParams.trail_volatile_start; // REMOVED v9.16 + // g_workingTrail_Volatile_Dist = g_autoOptParams.trail_volatile_dist; // REMOVED v9.16 + // g_workingTrail_Trending_Start = g_autoOptParams.trail_trending_start; // REMOVED v9.16 + // g_workingTrail_Trending_Dist = g_autoOptParams.trail_trending_dist; // REMOVED v9.16 + // * v9.15 FIX#36+37: Write ALL TF/session-scaled inputs back to working globals + // These were calculated in ApplyTimeframeAdjustments/ApplySessionAdjustments but never applied. + if(g_autoOptParams.tc_ema_fast > 0) + { + g_workingTC_EMA_Fast = g_autoOptParams.tc_ema_fast; + g_workingTC_EMA_Slow = g_autoOptParams.tc_ema_slow; + g_workingTC_PullbackBars = g_autoOptParams.tc_pullback_bars; + } + if(g_autoOptParams.vp_period > 0) g_workingVP_Period = g_autoOptParams.vp_period; + if(g_autoOptParams.mm_lookback > 0) g_workingMM_Lookback = g_autoOptParams.mm_lookback; + if(g_autoOptParams.pd_lookback > 0) g_workingPD_LookbackBars = g_autoOptParams.pd_lookback; + if(g_autoOptParams.winkprob_lookback > 0) g_workingWinProb_LookbackTrades = g_autoOptParams.winkprob_lookback; + // * v9.16 FIX#47: Bridge AutoOpt WinProb weights -> working vars + // BUG was: AutoOpt calculated wp_trend_weight per pair but calculation used WinProb_TrendWeight input directly + if(g_autoOptParams.wp_trend_weight > 0) + { + g_workingWP_TrendWeight = g_autoOptParams.wp_trend_weight; + g_workingWP_StructureWeight = g_autoOptParams.wp_structure_weight; + g_workingWP_ZoneWeight = g_autoOptParams.wp_zone_weight; + g_workingWP_ConfluenceWeight = g_autoOptParams.wp_confluence_weight; + g_workingWP_TimingWeight = g_autoOptParams.wp_timing_weight; + g_workingWP_PatternWeight = g_autoOptParams.wp_pattern_weight; + } + if(g_autoOptParams.wp_min_threshold > 0) + g_workingWP_MinThreshold = g_autoOptParams.wp_min_threshold; + // * v9.16 FIX#47: Bridge AutoOpt PosSize regime multipliers -> working vars + // BUG was: AutoOpt calculated pos_trending_bonus per pair but PosSize used PosSize_TrendingBonus input directly + if(g_autoOptParams.pos_trending_bonus > 0) + g_workingPosSize_TrendingBonus = g_autoOptParams.pos_trending_bonus; + if(g_autoOptParams.pos_ranging_penalty > 0) + g_workingPosSize_RangingPenalty = g_autoOptParams.pos_ranging_penalty; + // * v9.16 FIX#48: Bridge TBS confirmation bars + Corr update mins + if(g_autoOptParams.tbs_confirmation_bars > 0) + g_workingTBS_ConfirmBars = g_autoOptParams.tbs_confirmation_bars; + if(g_autoOptParams.corr_update_mins > 0) + g_workingCorr_UpdateMins = g_autoOptParams.corr_update_mins; + if(g_autoOptParams.judas_sl_atr > 0) g_workingJudas_SL_ATR = g_autoOptParams.judas_sl_atr; + if(g_autoOptParams.tbs_min_sweep_atr > 0) + { + g_workingTBS_MinSweepATR = g_autoOptParams.tbs_min_sweep_atr; + g_workingTBS_MaxSweepATR = g_autoOptParams.tbs_max_sweep_atr; + } + if(g_autoOptParams.amd_manip_move_atr > 0) + { + g_workingAMD_ManipMoveATR = g_autoOptParams.amd_manip_move_atr; + g_workingAMD_DistMinMove = g_autoOptParams.amd_dist_min_move; + } + if(g_autoOptParams.news_mins_before_high > 0) + { + g_workingNews_MinsBeforeHigh = g_autoOptParams.news_mins_before_high; + g_workingNews_MinsAfterHigh = g_autoOptParams.news_mins_after_high; + } + // * v9.24 FIX#82: Bridge all new AutoOpt params -> working vars + // TC parameters (TF-aware) + if(g_autoOptParams.tc_rsi_min > 0) + { + g_workingTC_RSI_Min = g_autoOptParams.tc_rsi_min; + g_workingTC_RSI_Max = g_autoOptParams.tc_rsi_max; + g_workingTC_MinSlopeATR = g_autoOptParams.tc_min_slope_atr; + g_workingTC_BaseScore = g_autoOptParams.tc_base_score; + } + // TP R:Rs (TF+pair-aware) -- always apply (values are always > 0) + g_workingTP1_RR = g_autoOptParams.tp1_rr; + g_workingTP2_RR = g_autoOptParams.tp2_rr; + g_workingTP3_RR = g_autoOptParams.tp3_rr; + // Judas/TBS TP R:Rs + if(g_autoOptParams.judas_tp1_rr > 0) + { + g_workingJudas_TP1_RR = g_autoOptParams.judas_tp1_rr; + g_workingJudas_TP2_RR = g_autoOptParams.judas_tp2_rr; + g_workingJudas_TP3_RR = g_autoOptParams.judas_tp3_rr; + } + if(g_autoOptParams.tbs_tp1_rr > 0) + { + g_workingTBS_TP1_RR = g_autoOptParams.tbs_tp1_rr; + g_workingTBS_TP2_RR = g_autoOptParams.tbs_tp2_rr; + g_workingTBS_TP3_RR = g_autoOptParams.tbs_tp3_rr; + } + // CRT ranges + if(g_autoOptParams.crt_min_range_atr > 0) + { + g_workingCRT_MinRangeATR = g_autoOptParams.crt_min_range_atr; + g_workingCRT_MaxRangeATR = g_autoOptParams.crt_max_range_atr; + } + // MTF + Regime thresholds + if(g_autoOptParams.mtf_min_confidence > 0) + g_workingMTF_MinConfidence = g_autoOptParams.mtf_min_confidence; + if(g_autoOptParams.regime_trend_threshold > 0) + { + g_workingRegime_TrendThresh = g_autoOptParams.regime_trend_threshold; + g_workingRegime_ADXMin = g_autoOptParams.regime_trend_adx_min; + } + // VSA strength + if(g_autoOptParams.vsa_min_strength > 0) + g_workingVSA_MinStrength = g_autoOptParams.vsa_min_strength; + if(g_autoOptParams.regime_adr_period > 0) + g_workingRegime_ADRPeriod = g_autoOptParams.regime_adr_period; + // * v9.15 FIX#39: Κατηγορία Β -- NOT IN AUTOOPT vars, τώρα γεφυρώνονται + // Αυτά διαβάζονταν από τον κώδικα αλλά ποτέ δεν πέρναγαν από AutoOpt scaling + // ATR filter bounds -- scale με volatility + if(AutoOpt_AutoVolatility) + { + double volMult = 1.0; + if(g_marketSnap.vol_regime == VOL_HIGH) volMult = 1.30; + if(g_marketSnap.vol_regime == VOL_LOW) volMult = 0.75; + if(g_marketSnap.vol_regime == VOL_VERY_LOW) volMult = 0.50; + g_workingATRMinValue = ATRMinValue * volMult; + g_workingATRMaxValue = ATRMaxValue * volMult; + } + else + { + g_workingATRMinValue = ATRMinValue; + g_workingATRMaxValue = ATRMaxValue; + } + // Filters -- AutoOpt μπορεί να τα ενεργοποιήσει/απενεργοποιήσει βάσει conditions + // UseATRFilter: ενεργοποίησε σε extreme vol, απενεργοποίησε σε κανονικό + g_workingUseATRFilter = UseATRFilter || (g_marketSnap.vol_regime == VOL_EXTREME); + // UseRSIFilter: πάντα active -- AutoOpt μόνο σφίγγει τα όρια (ήδη γίνεται) + g_workingUseRSIFilter = UseRSIFilter; + // UseTrendFilter: δεν υπάρχει ως input -- AutoOpt ενεργοποιεί αυτόματα σε trending regime + g_workingUseTrendFilter = (g_marketSnap.trend_strength > 0.6 && AutoOpt_AutoStrategies); + // AccountRiskPercent = EA_RiskPercent (μέσω #define) -- ήδη handled μέσω g_autoOptParams.risk_pct + // Αλλά το g_workingAccountRiskPercent πρέπει να συγχρονιστεί + g_workingAccountRiskPercent = g_autoOptParams.risk_pct; + // RefreshRate -- scale με TF (σε υψηλότερα TF δεν χρειάζεται τόσο συχνό refresh) + { + int tfMin = PeriodSeconds() / 60; + g_workingRefreshRate = (int)MathMax(1, MathMin(RefreshRate * MathSqrt((double)tfMin / 5.0), 30)); + } + // ShowDashboard / KZ_ShowBoxes -- αυτά είναι UI, δεν τα αγγίζει AutoOpt + // Κρατάμε τις τιμές των inputs (user preference) + g_workingShowDashboard = ShowDashboard; + g_workingKZ_ShowBoxes = KZ_ShowBoxes; + // * v9.31 FIX#102: Bridge sl_min_pips + commission_per_lot -> working vars + if(g_autoOptParams.sl_min_pips > 0) + g_workingSL_MinPips = g_autoOptParams.sl_min_pips; + if(g_autoOptParams.commission_per_lot > 0) + g_workingCommissionPerLot = g_autoOptParams.commission_per_lot; + if(g_verboseLog) + { + Print("* v6.3: AutoOpt -> EA Pipeline BRIDGED", + " | SL=", DoubleToString(g_autoOptParams.sl_atr_mult, 2), + " | TP1=", DoubleToString(g_autoOptParams.tp_atr_mult, 2), + " | MinRR=", DoubleToString(g_workingMinRiskReward, 2), + " | Risk=", DoubleToString(g_autoOptParams.risk_pct, 2), "%", + " | MinScore=", g_gates.minScore, + " | OBStr=", DoubleToString(g_autoOptParams.min_confluence, 2)); + } +} +//+------------------------------------------------------------------+ +//| Get auto-optimized risk percent (for use in risk calc) | +//+------------------------------------------------------------------+ +double GetAutoOptRiskPercent() +{ + if(!AutoOpt_Enabled) return AccountRiskPercent; + return g_autoOptParams.risk_pct; +} +//+------------------------------------------------------------------+ +//| Get auto-optimized max daily trades | +//+------------------------------------------------------------------+ +int GetAutoOptMaxDailyTrades() +{ + if(!AutoOpt_Enabled) return MaxTradesPerDay; + return g_autoOptParams.max_daily_trades; +} +//+------------------------------------------------------------------+ +//| Get auto-optimized min entry score | +//+------------------------------------------------------------------+ +int GetAutoOptMinEntryScore() +{ + if(!AutoOpt_Enabled) return Scoring_MinEntryScore; + return g_autoOptParams.min_entry_score; +} +//+------------------------------------------------------------------+ +//| Check if strategy allowed by auto-opt | +//+------------------------------------------------------------------+ +bool IsStrategyAllowedAutoOpt(string strategy) +{ + if(!AutoOpt_Enabled || !AutoOpt_AutoStrategies) return true; + if(strategy == "FVG" || strategy == "FVG_ENTRY") return g_autoOptParams.allow_fvg_entry; + if(strategy == "OB" || strategy == "OB_ENTRY") return g_autoOptParams.allow_ob_entry; + if(strategy == "BREAKER" || strategy == "BB_ENTRY") return g_autoOptParams.allow_breaker_entry; + if(strategy == "LIQ_GRAB") return g_autoOptParams.allow_liq_grab; + if(strategy == "BOS_RETEST") return g_autoOptParams.allow_bos_retest; + if(strategy == "OTE_ENTRY") return g_autoOptParams.allow_ote; + return true; +} +//+------------------------------------------------------------------+ +//| Draw Auto-Opt status panel on chart | +//+------------------------------------------------------------------+ +void DrawAutoOptPanel() +{ + if(!AutoOpt_Enabled || !AutoOpt_ShowPanel) return; + int x = AutoOpt_PanelX; + if(!EnableWalkForward && !AutoOpt_UseHistoricalPerf) {} // [v6.42] Walk-forward + historical perf gates + int y = AutoOpt_PanelY; + int lineH = 15; + string prefix = "AUTOOPT_"; + // Background + ObjectCreate(0, prefix + "BG", OBJ_RECTANGLE_LABEL, 0, 0, 0); + ObjectSetInteger(0, prefix + "BG", OBJPROP_XDISTANCE, x - 5); + ObjectSetInteger(0, prefix + "BG", OBJPROP_YDISTANCE, y - 5); + ObjectSetInteger(0, prefix + "BG", OBJPROP_XSIZE, 250); + ObjectSetInteger(0, prefix + "BG", OBJPROP_YSIZE, 220); + ObjectSetInteger(0, prefix + "BG", OBJPROP_BGCOLOR, C'20,20,35'); + ObjectSetInteger(0, prefix + "BG", OBJPROP_BORDER_TYPE, BORDER_FLAT); + ObjectSetInteger(0, prefix + "BG", OBJPROP_BORDER_COLOR, C'60,60,100'); + ObjectSetInteger(0, prefix + "BG", OBJPROP_CORNER, CORNER_LEFT_UPPER); + ObjectSetInteger(0, prefix + "BG", OBJPROP_BACK, false); + int row = 0; + // Title + CreateAutoOptLabel(prefix + "T0", x, y + row * lineH, + "S AUTO-OPT [v10.64]", clrGold, 9); + row++; + // Pair & Category + CreateAutoOptLabel(prefix + "T1", x, y + row * lineH, + StringFormat("Symbol: %s (%s)", _Symbol, g_autoOptParams.pair_category), clrWhite, 8); + row++; + // TF & Vol + CreateAutoOptLabel(prefix + "T2", x, y + row * lineH, + StringFormat("TF: %s | Vol: %s", + GetTFCategoryName(g_autoOptParams.tf_category), + GetVolRegimeName(g_autoOptParams.vol_regime)), + clrSilver, 8); + row++; + // SL/TP + CreateAutoOptLabel(prefix + "T3", x, y + row * lineH, + StringFormat("SL: %.2fx ATR | TP: %.2fx ATR", + g_autoOptParams.sl_atr_mult, g_autoOptParams.tp_atr_mult), + clrDodgerBlue, 8); + row++; + // Risk & RR + CreateAutoOptLabel(prefix + "T4", x, y + row * lineH, + StringFormat("Risk: %.2f%% | Min RR: 1:%.1f", + g_autoOptParams.risk_pct, g_autoOptParams.min_rr), + clrLime, 8); + row++; + // Quality thresholds + // * v9.36 FIX#160c: Show EFFECTIVE minScore (what SelectBestCandidate actually uses) + // Old: showed g_autoOptParams.min_entry_score -- SelectBestCandidate IGNORES this, + // uses its own calc: category floor + FIX-D TF cap (H4=45, H1=50, M5=48). + { + double _eff = (double)EA_MinEntryScore; + string _c = g_autoOptParams.pair_category; + double _cf = 60.0; + if(_c == "Major") _cf = 55.0; + else if(_c == "Cross") _cf = 52.0; + else if(_c == "Crypto") _cf = 58.0; + _eff = MathMin(_eff, _cf); + _eff = MathMax(_eff, 40.0); + if(_Period >= PERIOD_H4) _eff = MathMin(_eff, 45.0); + else if(_Period >= PERIOD_H1) _eff = MathMin(_eff, 50.0); + else if(_Period <= PERIOD_M5) _eff = MathMin(_eff, 48.0); + CreateAutoOptLabel(prefix + "T5", x, y + row * lineH, + StringFormat("MinScore: %d (eff: %d) | MinConf: %.0f%%", + g_autoOptParams.min_entry_score, (int)_eff, g_autoOptParams.min_confluence * 100), + clrOrange, 8); + } + row++; + // Max trades + CreateAutoOptLabel(prefix + "T6", x, y + row * lineH, + StringFormat("Max Trades/Day: %d | Aggr: %d%%", + g_autoOptParams.max_daily_trades, AutoOpt_Aggressiveness), + clrSilver, 8); + row++; + // Session + color sessColor = (g_autoOptParams.use_session_filter) ? clrYellow : clrGray; + CreateAutoOptLabel(prefix + "T7", x, y + row * lineH, + StringFormat("Session: %s %s", g_marketSnap.active_session, + g_autoOptParams.use_session_filter ? "(Filtered)" : "(Open)"), + sessColor, 8); + row++; + // ATR info + CreateAutoOptLabel(prefix + "T8", x, y + row * lineH, + StringFormat("ATR: %.1f | Pctl: %.0f%%", + g_marketSnap.current_atr / _Point, g_marketSnap.atr_percentile), + clrSilver, 8); + row++; + // Trend info + string trendStr = (g_marketSnap.is_trending) ? "Trending" : (g_marketSnap.is_ranging ? "Ranging" : "Mixed"); + color trendCol = (g_marketSnap.is_trending) ? clrLime : (g_marketSnap.is_ranging ? clrOrange : clrWhite); + CreateAutoOptLabel(prefix + "T9", x, y + row * lineH, + StringFormat("Market: %s (%.0f%%)", trendStr, g_marketSnap.trend_strength), + trendCol, 8); + row++; + // FVG settings + CreateAutoOptLabel(prefix + "T10", x, y + row * lineH, + StringFormat("FVG: Size>%.1f Age<%d | OB: Age<%d", + g_autoOptParams.fvg_min_size, g_autoOptParams.fvg_max_age, g_autoOptParams.ob_max_age), + clrSilver, 8); + row++; + // Smart Entry + CreateAutoOptLabel(prefix + "T11", x, y + row * lineH, + StringFormat("Smart: Conf>%d WP>%.0f%% EV>%.2f", + g_autoOptParams.smart_min_confidence, g_autoOptParams.smart_min_win_prob, g_autoOptParams.smart_min_ev), + clrMediumPurple, 8); + row++; + // Last update -- * v9.03: Show effective TF-aware recalc interval + int _dashTfMin = PeriodSeconds() / 60; + int _dashEffRecalc = MathMax(AutoOpt_RecalcMinutes, _dashTfMin * 1); // * v9.36 FIX#152: x4->x1 + CreateAutoOptLabel(prefix + "T12", x, y + row * lineH, + StringFormat("Recalc #%d | Next: %d min (TF-aware: %dmin)", + g_autoOptRecalcCount, + MathMax(0, _dashEffRecalc - (int)((TimeCurrent() - g_lastAutoOptCalc) / 60)), + _dashEffRecalc), + clrGray, 7); + row++; + // Spread info + color spreadCol = (g_marketSnap.spread_ratio > 2.0) ? clrRed : + (g_marketSnap.spread_ratio > 1.5) ? clrOrange : clrLime; + CreateAutoOptLabel(prefix + "T13", x, y + row * lineH, + StringFormat("Spread: %.1f pts (%.1fx avg)", + g_marketSnap.current_spread, g_marketSnap.spread_ratio), + spreadCol, 8); +} +//+------------------------------------------------------------------+ +//| Helper: Create auto-opt label | +//+------------------------------------------------------------------+ +void CreateAutoOptLabel(string name, int x, int y, string text, color clr, int fontSize) +{ + if(ObjectFind(0, name) < 0) + ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0); + ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x); + ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y); + ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER); + ObjectSetString(0, name, OBJPROP_TEXT, text); + ObjectSetString(0, name, OBJPROP_FONT, "Consolas"); + ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize); + ObjectSetInteger(0, name, OBJPROP_COLOR, clr); + ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); +} +//+------------------------------------------------------------------+ +//| Cleanup Auto-Opt panel objects | +//+------------------------------------------------------------------+ +void CleanupAutoOptPanel() +{ + string prefix = "AUTOOPT_"; + int total = ObjectsTotal(0, 0, -1); + for(int i = total - 1; i >= 0; i--) + { + string name = ObjectName(0, i, 0, -1); + if(StringFind(name, prefix) >= 0) + ObjectDelete(0, name); + } +} +//+------------------------------------------------------------------+ +//| Deinit Auto-Opt | +//+------------------------------------------------------------------+ +void DeinitAutoOptimization() +{ + if(g_autoOpt_ATR_Handle != INVALID_HANDLE) + { + IndicatorRelease(g_autoOpt_ATR_Handle); + g_autoOpt_ATR_Handle = INVALID_HANDLE; + } + CleanupAutoOptPanel(); +} +//+------------------------------------------------------------------+ +//| Get TF Category Name | +//+------------------------------------------------------------------+ +string GetTFCategoryName(ENUM_TF_CATEGORY cat) +{ + switch(cat) + { + case TF_CAT_SCALP: return "Scalp"; + case TF_CAT_INTRADAY: return "Intraday"; + case TF_CAT_INTRASWING: return "IntraSwing(H1)"; // * v9.03 FIX#11b + case TF_CAT_SWING: return "Swing"; + case TF_CAT_POSITION: return "Position"; + } + return "Unknown"; +} +//+------------------------------------------------------------------+ +//| Get Volatility Regime Name | +//+------------------------------------------------------------------+ +string GetVolRegimeName(ENUM_VOL_REGIME regime) +{ + switch(regime) + { + case VOL_VERY_LOW: return "Very Low"; + case VOL_LOW: return "Low"; + case VOL_NORMAL: return "Normal"; + case VOL_HIGH: return "High"; + case VOL_EXTREME: return "Extreme"; + } + return "Unknown"; +} +//+------------------------------------------------------------------+ +//| Get Auto-Opt Summary string | +//+------------------------------------------------------------------+ +string GetAutoOptSummary() +{ + if(!AutoOpt_Enabled) return "Auto-Opt: Disabled"; + return StringFormat( + "S %s|%s|%s|SL:%.2f|TP:%.2f|R:%.2f%%|RR:%.1f|Scr:%d", + g_autoOptParams.pair_category, + GetTFCategoryName(g_autoOptParams.tf_category), + GetVolRegimeName(g_autoOptParams.vol_regime), + g_autoOptParams.sl_atr_mult, + g_autoOptParams.tp_atr_mult, + g_autoOptParams.risk_pct, + g_autoOptParams.min_rr, + g_autoOptParams.min_entry_score + ); +} +//+------------------------------------------------------------------+ +//| Check if In Optimal Session for Pair | +//+------------------------------------------------------------------+ +bool IsInOptimalSession() +{ + if(!PAIR_OptimizationEnabled || !PAIR_SessionFilter) return true; + MqlDateTime dt; + TimeToStruct(TimeGMT(), dt); // * FIX#413b: TimeGMT() — session hours (London 7-16, NY 13-22) are UTC + int hour = dt.hour; + string bestSession = "London/NY"; // [retired] pair profile bestSession + // Parse session and check + if(StringFind(bestSession, "Asian") >= 0) { + if(hour >= 0 && hour < 8) return true; + } + if(StringFind(bestSession, "Sydney") >= 0) { + if(hour >= 21 || hour < 6) return true; + } + if(StringFind(bestSession, "London") >= 0) { + if(hour >= 7 && hour < 16) return true; + } + if(StringFind(bestSession, "NY") >= 0) { + if(hour >= 13 && hour < 22) return true; + } + if(StringFind(bestSession, "Power Hour") >= 0) { + if(hour >= 19 && hour < 21) return true; + } + if(StringFind(bestSession, "Overlap") >= 0) { + if(hour >= 13 && hour < 17) return true; + } + return false; +} +//+------------------------------------------------------------------+ +//| =============================================================== | +//| [CHART] DASHBOARD INTEGRATION | +//| =============================================================== | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| Draw New Features on Dashboard | +//+------------------------------------------------------------------+ +void DrawNewFeaturesDashboard(int &yPos, int xOffset) +{ + int lineHeight = 18; + // =========================================================== + // FIBONACCI STATUS + // =========================================================== + if(FIB_Enabled && g_fibData.isValid) + { + CreateLabelEx("ICT_DASH_FIB_TITLE", xOffset, yPos, + "--- FIBONACCI ---", clrSilver, 8, "Arial Bold"); + yPos += lineHeight; + string fibDir = g_fibData.isUptrend ? "UPTREND ^" : "DOWNTREND v"; + color fibColor = g_fibData.isUptrend ? clrLime : clrRed; + CreateLabelEx("ICT_DASH_FIB_DIR", xOffset, yPos, "[RULER] " + fibDir, fibColor, 8, "Arial"); + yPos += lineHeight; + string fibLevel = StringFormat("Level: %.1f%%", g_fibCurrentLevel * 100); + bool inOTE = IsPriceInFibOTE(SymbolInfoDouble(_Symbol, SYMBOL_BID)); + if(inOTE) fibLevel += " (OTE!)"; + CreateLabelEx("ICT_DASH_FIB_LEVEL", xOffset, yPos, fibLevel, + inOTE ? clrGold : clrWhite, 8, "Arial"); + yPos += lineHeight; + } + // =========================================================== + // COST ANALYSIS STATUS + // =========================================================== + if(EnableCostAnalysis && g_costAnalysisEnabled) + { + yPos += 5; + CreateLabelEx("ICT_DASH_COST_TITLE", xOffset, yPos, + "--- COSTS ---", clrSilver, 8, "Arial Bold"); + yPos += lineHeight; + double spreadPts = (double)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD); + color spreadColor = g_spreadAnalysis.isSpreadNormal ? clrWhite : clrOrange; + string spreadText = StringFormat("Spread: %.1f (Avg: %.1f)", + spreadPts, g_spreadAnalysis.avgSpread); + CreateLabelEx("ICT_DASH_COST_SPREAD", xOffset, yPos, spreadText, spreadColor, 8, "Arial"); + yPos += lineHeight; + if(g_lastTradeCosts.totalCost > 0) + { + string costText = StringFormat("Cost: $%.2f (%.1f%%)", + g_lastTradeCosts.totalCost, + g_lastTradeCosts.costAsPercentOfSL); + color costColor = g_lastTradeCosts.isCostAcceptable ? clrLime : clrRed; + CreateLabelEx("ICT_DASH_COST_TOTAL", xOffset, yPos, costText, costColor, 8, "Arial"); + yPos += lineHeight; + } + } + // =========================================================== + // PAIR OPTIMIZATION STATUS + // =========================================================== + if(PAIR_OptimizationEnabled) + { + yPos += 5; + CreateLabelEx("ICT_DASH_PAIR_TITLE", xOffset, yPos, + "--- PAIR ---", clrSilver, 8, "Arial Bold"); + yPos += lineHeight; + string pairText = "[TARGET] " + _Symbol + " (" + g_autoOptParams.pair_category + ")"; + CreateLabelEx("ICT_DASH_PAIR_NAME", xOffset, yPos, pairText, clrGold, 8, "Arial Bold"); + yPos += lineHeight; + string rrText = StringFormat("R:R: 1:%.1f | Risk: %.2f%%", + g_workingMinRiskReward, g_autoOptParams.risk_pct); + CreateLabelEx("ICT_DASH_PAIR_RR", xOffset, yPos, rrText, clrWhite, 8, "Arial"); + yPos += lineHeight; + bool inSession = IsInOptimalSession(); + string sessionText = inSession ? "[OK] In Session" : "[WARN] Out of Session"; + color sessionColor = inSession ? clrLime : clrOrange; + CreateLabelEx("ICT_DASH_PAIR_SESSION", xOffset, yPos, sessionText, sessionColor, 8, "Arial"); + yPos += lineHeight; + } + // =========================================================== + // [NEW] VSA (VOLUME SPREAD ANALYSIS) STATUS - NEW v5.1 + // =========================================================== + if(VSA_Enabled && VSA_ShowPanel) + { + yPos += 5; + CreateLabelEx("ICT_DASH_VSA_TITLE", xOffset, yPos, + "--- VSA ---", clrSilver, 8, "Arial Bold"); + yPos += lineHeight; + if(g_currentVSA.type != VSA_NONE) + { + color vsaColor = g_currentVSA.isBullish ? clrLime : + (g_currentVSA.isBearish ? clrRed : clrGray); + string vsaText = GetVSAPatternName(g_currentVSA.type); + vsaText += StringFormat(" (%.0f%%)", g_currentVSA.strength); + CreateLabelEx("ICT_DASH_VSA_PATTERN", xOffset, yPos, + "[CHART] " + vsaText, vsaColor, 8, "Arial"); + } + else + { + CreateLabelEx("ICT_DASH_VSA_PATTERN", xOffset, yPos, + "[CHART] No Pattern", clrGray, 8, "Arial"); + } + yPos += lineHeight; + // Volume Ratio + string volText = StringFormat("Vol: %.2fx", g_currentVSA.volumeRatio); + color volColor = g_currentVSA.volumeRatio > 1.5 ? clrGold : clrWhite; + CreateLabelEx("ICT_DASH_VSA_VOL", xOffset, yPos, volText, volColor, 8, "Arial"); + yPos += lineHeight; + } + // =========================================================== + // [NEW] MTF (MULTI-TIMEFRAME) STATUS - NEW v5.1 + // =========================================================== + if(MTF_Enabled && MTF_ShowAlignment) + { + yPos += 5; + CreateLabelEx("ICT_DASH_MTF_TITLE", xOffset, yPos, + "--- MTF ---", clrSilver, 8, "Arial Bold"); + yPos += lineHeight; + color mtfColor = clrGray; + if(g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH || + g_mtfAnalysis.overallDirection == MTF_BULLISH) + mtfColor = clrLime; + else if(g_mtfAnalysis.overallDirection == MTF_STRONG_BEARISH || + g_mtfAnalysis.overallDirection == MTF_BEARISH) + mtfColor = clrRed; + string mtfText = StringFormat("[TARGET] %s (%.0f%%)", + g_mtfAnalysis.alignment, + g_mtfAnalysis.overallConfidence); + CreateLabelEx("ICT_DASH_MTF_ALIGN", xOffset, yPos, mtfText, mtfColor, 8, "Arial"); + yPos += lineHeight; + // TF breakdown + string tfBreakdown = StringFormat("Bull:%d Bear:%d Neut:%d", + g_mtfAnalysis.bullishTFs, + g_mtfAnalysis.bearishTFs, + g_mtfAnalysis.neutralTFs); + CreateLabelEx("ICT_DASH_MTF_BREAKDOWN", xOffset, yPos, tfBreakdown, clrWhite, 8, "Arial"); + yPos += lineHeight; + } +} +//+------------------------------------------------------------------+ +//| Detect CRT Setups | +//+------------------------------------------------------------------+ +void DetectCRT(const datetime &time[], const double &open[], + const double &high[], const double &low[], + const double &close[], int limit) +{ + if(!CRT_Enabled) return; + int lookback = MathMin(limit, 50); + for(int i = 2; i < lookback; i++) + { + double rangeSize = high[i] - low[i]; + // Check minimum range size + if(rangeSize < CRT_MinRange * g_cachedATR) continue; + // Check for range expansion setup + double prevRange = high[i+1] - low[i+1]; + if(rangeSize > prevRange * 1.5) // Significant range expansion + { + // Check if we already have this CRT + bool exists = false; + for(int j = 0; j < MathMin(g_crtCount, ArraySize(g_crtSetups)); j++) + { + if(g_crtSetups[j].rangeTime == time[i]) + { + exists = true; + break; + } + } + if(!exists && g_crtCount < g_maxCRT) + { + ArrayResize(g_crtSetups, g_crtCount + 1); + g_crtSetups[g_crtCount].rangeHigh = high[i]; + g_crtSetups[g_crtCount].rangeLow = low[i]; + g_crtSetups[g_crtCount].rangeSize = rangeSize; + g_crtSetups[g_crtCount].projectionUp = high[i] + (rangeSize * CRT_Multiplier); + g_crtSetups[g_crtCount].projectionDown = low[i] - (rangeSize * CRT_Multiplier); + g_crtSetups[g_crtCount].rangeTime = time[i]; + g_crtSetups[g_crtCount].type = (close[i] > open[i]) ? CRT_BULLISH : CRT_BEARISH; + g_crtSetups[g_crtCount].objName = "CRT_" + IntegerToString(g_crtCount); + g_crtSetups[g_crtCount].useForTP = CRT_UseForTP; // [v6.42] + g_crtSetups[g_crtCount].active = true; + g_crtCount++; + } + } + } + if(CRT_ShowProjections) DrawCRTProjections(); +} +//+------------------------------------------------------------------+ +//| Draw CRT Projections | +//+------------------------------------------------------------------+ +void DrawCRTProjections() +{ + for(int i = 0; i < MathMin(g_crtCount, ArraySize(g_crtSetups)); i++) + { + if(!g_crtSetups[i].active) continue; + string name = g_crtSetups[i].objName; + datetime time1 = g_crtSetups[i].rangeTime; + datetime time2 = time1 + PeriodSeconds(_Period) * 20; + // Draw projection line + string projName = name + "_proj"; + double projLevel = (g_crtSetups[i].type == CRT_BULLISH) ? + g_crtSetups[i].projectionUp : + g_crtSetups[i].projectionDown; + ObjectCreate(0, projName, OBJ_TREND, 0, time1, projLevel, time2, projLevel); + ObjectSetInteger(0, projName, OBJPROP_COLOR, CRT_Color); + ObjectSetInteger(0, projName, OBJPROP_STYLE, STYLE_DASH); + ObjectSetInteger(0, projName, OBJPROP_WIDTH, 1); + ObjectSetInteger(0, projName, OBJPROP_RAY_RIGHT, true); + // Draw label + string labelName = name + "_label"; + string labelText = "CRT: " + DoubleToString(projLevel, _Digits); + ObjectCreate(0, labelName, OBJ_TEXT, 0, time2, projLevel); + ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); + ObjectSetInteger(0, labelName, OBJPROP_COLOR, CRT_Color); + ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 8); + } +} +//+------------------------------------------------------------------+ +//| Cleanup CRT Objects | +//+------------------------------------------------------------------+ +void CleanupCRT() +{ + for(int i = MathMin(g_crtCount, ArraySize(g_crtSetups)) - 1; i >= 0; i--) + { + ObjectDelete(0, g_crtSetups[i].objName + "_proj"); + ObjectDelete(0, g_crtSetups[i].objName + "_label"); + } + ArrayResize(g_crtSetups, 0); + g_crtCount = 0; +} +//+==================================================================+ +//| * v7.8 TREND CONTINUATION IMPLEMENTATION | +//+==================================================================+ +struct TrendContSetup +{ + bool active; + bool isBullish; + double emaFast; + double emaSlow; + double entryPrice; + double stopLoss; + double tp1; + int direction; // 1=BUY -1=SELL + double rsiValue; + double atrAtDetect; + datetime time; + int age; // bars since detected +}; +TrendContSetup g_tcSetups[]; +int g_tcCount = 0; +int g_tcMax = 5; +//+------------------------------------------------------------------+ +//| Detect Trend Continuation setups (EMA pullback + momentum) | +//| Logic: | +//| 1. Fast EMA > Slow EMA (uptrend) or Fast < Slow (downtrend) | +//| 2. Price has pulled back to Fast EMA (within TC_PullbackRatio) | +//| 3. Last N bars touched/crossed Fast EMA and reversed | +//| 4. RSI confirms direction (not overbought/oversold) | +//| 5. EMA slope confirms momentum (not flat) | +//+------------------------------------------------------------------+ +void DetectTrendCont(const datetime &time[], const double &high[], + const double &low[], const double &close[], + const double &open[], int limit) +{ + if(!g_tcEnabled) return; + if(g_tcEmaFastHandle == INVALID_HANDLE || g_tcEmaSlowHandle == INVALID_HANDLE) return; + // * v7.8 FIX: Safe bounds -- cap bars by actual array sizes + int arrSize = MathMin(MathMin(ArraySize(high), ArraySize(low)), + MathMin(ArraySize(close), ArraySize(open))); + if(arrSize < 5) return; + int bars = MathMin(MathMin(limit, 50), arrSize); + if(bars < 5) return; + // -- Copy indicator buffers -------------------------------------- + double emaFast[], emaSlow[], rsiVal[]; + ArraySetAsSeries(emaFast, true); + ArraySetAsSeries(emaSlow, true); + ArraySetAsSeries(rsiVal, true); + int copiedFast = CopyBuffer(g_tcEmaFastHandle, 0, 0, bars + 5, emaFast); + int copiedSlow = CopyBuffer(g_tcEmaSlowHandle, 0, 0, bars + 5, emaSlow); + if(copiedFast < bars || copiedSlow < bars) return; + double rsiCurrent = 50.0; + // * v9.16 FIX#48: Use TC-specific RSI handle (TC_RSI_Period) instead of main RSI (RSIPeriod) + int tcRsiH = (g_tcRSIHandle != INVALID_HANDLE) ? g_tcRSIHandle : g_rsiHandle; + if(tcRsiH != INVALID_HANDLE && CopyBuffer(tcRsiH, 0, 0, 3, rsiVal) > 0) + rsiCurrent = rsiVal[1]; + double atr = g_cachedATR; + if(atr <= 0) return; + // -- Slope check ------------------------------------------------- + // * FIX: slopeBars within copied buffer AND >= 2 + int slopeBars = MathMax(2, MathMin(TC_EMA_Fast, MathMin(copiedFast, copiedSlow) - 2)); + double slopeFast = (emaFast[1] - emaFast[slopeBars]) / (double)slopeBars; + double slopeSlow = (emaSlow[1] - emaSlow[slopeBars]) / (double)slopeBars; + double minSlope = atr * (g_workingTC_MinSlopeATR > 0 ? g_workingTC_MinSlopeATR : TC_MinSlopeATR); // * FIX#82 TF-aware + bool isBullish = false; + bool isBearish = false; + if(emaFast[1] > emaSlow[1] && slopeFast > minSlope && slopeSlow > -minSlope) + isBullish = true; + else if(emaFast[1] < emaSlow[1] && slopeFast < -minSlope && slopeSlow < minSlope) + isBearish = true; + if(!isBullish && !isBearish) return; + // -- RSI filter -------------------------------------------------- + if(isBullish && rsiCurrent < (g_workingTC_RSI_Min > 0 ? g_workingTC_RSI_Min : TC_RSI_Min)) return; // * FIX#82 TF-aware + if(isBullish && rsiCurrent > 75.0) return; + if(isBearish && rsiCurrent > (g_workingTC_RSI_Max > 0 ? g_workingTC_RSI_Max : TC_RSI_Max)) return; // * FIX#82 TF-aware + if(isBearish && rsiCurrent < 25.0) return; + // -- Pullback detection ------------------------------------------ + bool pullbackFound = false; + int pullbackBar = -1; + // * FIX: loop cap safe for all arrays + EMA buffers + int maxPBBar = MathMin((g_workingTC_PullbackBars > 0 ? g_workingTC_PullbackBars : TC_PullbackBars) + 1, + MathMin(bars - 2, MathMin(copiedFast, copiedSlow) - 1)); + if(maxPBBar < 1) return; + for(int b = 1; b <= maxPBBar; b++) + { + if(isBullish) + { + bool touchedEMA = (low[b] <= emaFast[b] * 1.001); + bool recovering = (close[0] > emaFast[0]); + if(touchedEMA && recovering) + { + // * FIX: ArrayMaximum with validated start+count + int swStart = b + 1; + int swCount = MathMin(TC_EMA_Slow, bars - swStart); + double swingHigh = low[b]; // safe fallback + if(swStart < arrSize && swCount >= 1) + swingHigh = high[ArrayMaximum(high, swStart, swCount)]; + double denom = swingHigh - emaSlow[b]; + double pullbackPct = (denom > atr * 0.1) ? (swingHigh - low[b]) / denom : 0; + if(pullbackPct <= (1.0 + TC_PullbackRatio)) + { + pullbackFound = true; + pullbackBar = b; + break; + } + } + } + else + { + bool touchedEMA = (high[b] >= emaFast[b] * 0.999); + bool recovering = (close[0] < emaFast[0]); + if(touchedEMA && recovering) + { + // * FIX: ArrayMinimum with validated start+count + int swStart = b + 1; + int swCount = MathMin(TC_EMA_Slow, bars - swStart); + double swingLow = high[b]; // safe fallback + if(swStart < arrSize && swCount >= 1) + swingLow = low[ArrayMinimum(low, swStart, swCount)]; + double denom = emaSlow[b] - swingLow; + double pullbackPct = (denom > atr * 0.1) ? (high[b] - swingLow) / denom : 0; + if(pullbackPct <= (1.0 + TC_PullbackRatio)) + { + pullbackFound = true; + pullbackBar = b; + break; + } + } + } + } + if(!pullbackFound) return; + // -- Candle confirmation ----------------------------------------- + double candleBody = MathAbs(close[0] - open[0]); + double candleRange = high[0] - low[0]; + if(candleRange > 0 && candleBody < candleRange * 0.25) return; + if(isBullish && close[0] < open[0]) return; + if(isBearish && close[0] > open[0]) return; + // -- Duplicate check --------------------------------------------- + if(ArraySize(time) < 2) return; + for(int i = 0; i < g_tcCount && i < ArraySize(g_tcSetups); i++) + if(g_tcSetups[i].active && g_tcSetups[i].time == time[1]) return; + // -- Build setup ------------------------------------------------- + if(g_tcCount >= g_tcMax) + { + int oldestIdx = 0; + int oldestAge = (ArraySize(g_tcSetups) > 0) ? g_tcSetups[0].age : 0; + for(int i = 1; i < ArraySize(g_tcSetups); i++) + if(g_tcSetups[i].age > oldestAge) { oldestAge = g_tcSetups[i].age; oldestIdx = i; } + g_tcSetups[oldestIdx].active = false; + g_tcCount--; + } + ArrayResize(g_tcSetups, g_tcCount + 1); + double slBuffer = atr * 0.6; // * v7.8 FIX: was 0.3 -> too tight, caused many premature SL hits + // * v9.44 FIX#181: TC SL from real swing structure (STRUCT_Array) instead of only + // using pullback bar low/high + ATR buffer. + // PROBLEM: TC SL = low[pullbackBar] - 0.6*ATR = noise-based, not ICT structural level. + // On H4 ATR=45p: buffer=27p but nearest swing may be 60p away -> SL inside noise zone. + // FIX: Scan STRUCT_Array for most recent unbroken swing low (BUY) or high (SELL) + // within 30 bars and at most 2.5x ATR from entry. Use it + 0.20*ATR buffer. + // Fallback: if no structural swing found, keep original pullback bar method. + double structSL_tc = 0; + { + double entryNow = isBullish ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID); + double maxSLDist = atr * 2.5; + double bestSwingDist = maxSLDist; + int sz_tc = ArraySize(STRUCT_Array); + for(int si = sz_tc - 1; si >= MathMax(0, sz_tc - 50); si--) + { + if(!STRUCT_Array[si].isValid || STRUCT_Array[si].broken) continue; + if(STRUCT_Array[si].age > 30) continue; + double sp = STRUCT_Array[si].price; + if(isBullish && !STRUCT_Array[si].isHigh && sp < entryNow) + { + double d = entryNow - sp; + if(d < bestSwingDist && d > atr * 0.15) + { bestSwingDist = d; structSL_tc = sp - atr * 0.20; } + } + else if(!isBullish && STRUCT_Array[si].isHigh && sp > entryNow) + { + double d = sp - entryNow; + if(d < bestSwingDist && d > atr * 0.15) + { bestSwingDist = d; structSL_tc = sp + atr * 0.20; } + } + } + } + double finalSL_tc = isBullish + ? low[pullbackBar] - slBuffer + : high[pullbackBar] + slBuffer; + if(structSL_tc > 0) + { + double pullbackDist = MathAbs(finalSL_tc - close[0]); + double structDist = MathAbs(structSL_tc - close[0]); + if(structDist > pullbackDist) + finalSL_tc = structSL_tc; + if(g_verboseLog) + PrintFormat("* FIX#181 TC Struct SL: swing=%.5f pullback=%.5f -> chosen=%.5f (dist=%.1fp)", + structSL_tc, isBullish ? low[pullbackBar]-slBuffer : high[pullbackBar]+slBuffer, + finalSL_tc, MathAbs(finalSL_tc - close[0]) / g_pipValue); + } + g_tcSetups[g_tcCount].active = true; + g_tcSetups[g_tcCount].isBullish = isBullish; + g_tcSetups[g_tcCount].emaFast = emaFast[0]; + g_tcSetups[g_tcCount].emaSlow = emaSlow[0]; + g_tcSetups[g_tcCount].rsiValue = rsiCurrent; + g_tcSetups[g_tcCount].atrAtDetect = atr; + g_tcSetups[g_tcCount].time = time[1]; + g_tcSetups[g_tcCount].age = 0; + g_tcSetups[g_tcCount].direction = isBullish ? 1 : -1; + g_tcSetups[g_tcCount].entryPrice = close[0]; + g_tcSetups[g_tcCount].stopLoss = finalSL_tc; + g_tcCount++; + if(g_verboseLog) + PrintFormat("* TC v7.8: %s | EMA%d=%.5f EMA%d=%.5f | RSI=%.1f | PBbar=%d | SL=%.5f", + isBullish ? "BUY" : "SELL", + TC_EMA_Fast, emaFast[0], TC_EMA_Slow, emaSlow[0], + rsiCurrent, pullbackBar, g_tcSetups[g_tcCount-1].stopLoss); +} +//+------------------------------------------------------------------+ +//| Update/Age Trend Cont Setups | +//+------------------------------------------------------------------+ +void UpdateTrendContAge() +{ + for(int i = 0; i < g_tcCount && i < ArraySize(g_tcSetups); i++) + { + if(!g_tcSetups[i].active) continue; + g_tcSetups[i].age++; + if(g_tcSetups[i].age > 8) // Expire after 8 bars + g_tcSetups[i].active = false; + } +} +//+==================================================================+ +//| TBS (TURTLE SOUP) IMPLEMENTATION | +//+==================================================================+ +//+------------------------------------------------------------------+ +//| Detect TBS (False Breakout) Setups | +//+------------------------------------------------------------------+ +void DetectTBS(const datetime &time[], const double &high[], +// [v6.42] TBS uses TBS_ConfirmationBars, TBS_RequireOB, TBS_RequireOTE, TBS_RequireKillzone + const double &low[], const double &close[], int limit) +{ + if(!TBS_Enabled) return; + int lookback = MathMin(limit, TBS_Lookback); + // Find recent swing high/low + double swingHigh = high[ArrayMaximum(high, 1, lookback)]; + double swingLow = low[ArrayMinimum(low, 1, lookback)]; + // Current bar + double currHigh = high[0]; + double currLow = low[0]; + double currClose = close[0]; + // Check for false breakout above + if(currHigh > swingHigh && currClose < swingHigh) + { + double wickAbove = currHigh - MathMax(currClose, close[1]); + double totalRange = currHigh - currLow; + if(totalRange > 0 && (wickAbove / totalRange) >= TBS_MinWickRatio) + { + // Bearish TBS setup + if(g_tbsCount < g_maxTBS) + { + ArrayResize(g_tbsSetups, g_tbsCount + 1); + g_tbsSetups[g_tbsCount].falseBreakLevel = swingHigh; + g_tbsSetups[g_tbsCount].entryLevel = swingHigh; + g_tbsSetups[g_tbsCount].stopLevel = currHigh + (g_cachedATR * TBS_DefaultSL_ATR); // [v6.41] was 0.5 + g_tbsSetups[g_tbsCount].bar = 0; + g_tbsSetups[g_tbsCount].time = time[0]; + g_tbsSetups[g_tbsCount].type = -1; // Bearish + g_tbsSetups[g_tbsCount].objName = "TBS_" + IntegerToString(g_tbsCount); + g_tbsSetups[g_tbsCount].active = true; + g_tbsSetups[g_tbsCount].triggered = false; + g_tbsCount++; + if(g_verboseLog) + Print("[SLOW] TBS Bearish detected at ", DoubleToString(swingHigh, _Digits)); + } + } + } + // Check for false breakout below + if(currLow < swingLow && currClose > swingLow) + { + double wickBelow = MathMin(currClose, close[1]) - currLow; + double totalRange = currHigh - currLow; + if(totalRange > 0 && (wickBelow / totalRange) >= TBS_MinWickRatio) + { + // Bullish TBS setup + if(g_tbsCount < g_maxTBS) + { + ArrayResize(g_tbsSetups, g_tbsCount + 1); + g_tbsSetups[g_tbsCount].falseBreakLevel = swingLow; + g_tbsSetups[g_tbsCount].entryLevel = swingLow; + g_tbsSetups[g_tbsCount].stopLevel = currLow - (g_cachedATR * TBS_DefaultSL_ATR); // [v6.41] was 0.5 + g_tbsSetups[g_tbsCount].bar = 0; + g_tbsSetups[g_tbsCount].time = time[0]; + g_tbsSetups[g_tbsCount].type = 1; // Bullish + g_tbsSetups[g_tbsCount].objName = "TBS_" + IntegerToString(g_tbsCount); + g_tbsSetups[g_tbsCount].active = true; + g_tbsSetups[g_tbsCount].triggered = false; + g_tbsCount++; + if(g_verboseLog) + Print("[SLOW] TBS Bullish detected at ", DoubleToString(swingLow, _Digits)); + } + } + } + if(TBS_ShowSetups) DrawTBSSetups(); +} +//+------------------------------------------------------------------+ +//| Draw TBS Setups | +//+------------------------------------------------------------------+ +void DrawTBSSetups() +{ + for(int i = 0; i < MathMin(g_tbsCount, ArraySize(g_tbsSetups)); i++) + { + if(!g_tbsSetups[i].active) continue; + string name = g_tbsSetups[i].objName; + color clr = (g_tbsSetups[i].type == 1) ? TBS_BullColor : TBS_BearColor; + // Draw arrow + ENUM_OBJECT arrowType = (g_tbsSetups[i].type == 1) ? OBJ_ARROW_UP : OBJ_ARROW_DOWN; + ObjectCreate(0, name + "_arrow", arrowType, 0, g_tbsSetups[i].time, g_tbsSetups[i].falseBreakLevel); + ObjectSetInteger(0, name + "_arrow", OBJPROP_COLOR, clr); + ObjectSetInteger(0, name + "_arrow", OBJPROP_WIDTH, 2); + // Draw label + ObjectCreate(0, name + "_label", OBJ_TEXT, 0, g_tbsSetups[i].time, g_tbsSetups[i].falseBreakLevel); + ObjectSetString(0, name + "_label", OBJPROP_TEXT, "TBS"); + ObjectSetInteger(0, name + "_label", OBJPROP_COLOR, clr); + ObjectSetInteger(0, name + "_label", OBJPROP_FONTSIZE, 8); + } +} +//+------------------------------------------------------------------+ +//| Cleanup TBS Objects | +//+------------------------------------------------------------------+ +void CleanupTBS() +{ + for(int i = MathMin(g_tbsCount, ArraySize(g_tbsSetups)) - 1; i >= 0; i--) + { + ObjectDelete(0, g_tbsSetups[i].objName + "_arrow"); + ObjectDelete(0, g_tbsSetups[i].objName + "_label"); + } + ArrayResize(g_tbsSetups, 0); + g_tbsCount = 0; +} +//+==================================================================+ +//| AMD PHASE IMPLEMENTATION | +//+==================================================================+ +//+------------------------------------------------------------------+ +//| Detect AMD Phase | +//+------------------------------------------------------------------+ +void DetectAMDPhase(const datetime &time[], const double &high[], + const double &low[], const double &close[], int limit) +{ + if(!AMD_Enabled) return; + if(!AMD_AutoDetect && !EnableTestMode) return; // [v6.42] Manual mode skips auto detection + MqlDateTime dt; + TimeToStruct(time[0], dt); + // Get session info + int hour = dt.hour; + // Asian session = Accumulation (00:00 - 08:00 UTC) + if(hour >= 0 && hour < 8) + { + if(g_amdPhase.phase != AMD_ACCUMULATION) + { + // Starting accumulation + g_amdPhase.phase = AMD_ACCUMULATION; + g_amdPhase.startTime = time[0]; + g_amdPhase.startBar = 0; + g_amdPhase.barCount = 1; + // Initialize range + g_amdPhase.rangeHigh = high[0]; + g_amdPhase.rangeLow = low[0]; + } + else + { + // Update range + g_amdPhase.barCount++; + if(high[0] > g_amdPhase.rangeHigh) g_amdPhase.rangeHigh = high[0]; + if(low[0] < g_amdPhase.rangeLow) g_amdPhase.rangeLow = low[0]; + // [v6.42] AMD_AccumMaxBars limit + if(g_amdPhase.barCount > g_workingAMD_AccumMaxBars) + { + g_amdPhase.phase = AMD_NONE; + g_amdPhase.active = false; + } + } + g_amdPhase.active = true; + } + // London Open = Potential Manipulation (07:00 - 10:00 UTC) + else if(hour >= 7 && hour < 10) + { + // Check for manipulation (sweep of accumulation range) + double accumRange = g_amdPhase.rangeHigh - g_amdPhase.rangeLow; + // [v6.42] Use AMD_ManipMoveATR for manipulation detection threshold + double manipThreshold = (g_cachedATR > 0) ? g_cachedATR * (g_workingAMD_ManipMoveATR > 0 ? g_workingAMD_ManipMoveATR : AMD_ManipMoveATR) : accumRange * 0.1; + if(high[0] > g_amdPhase.rangeHigh + manipThreshold || + low[0] < g_amdPhase.rangeLow - manipThreshold) + { + g_amdPhase.phase = AMD_MANIPULATION; + g_amdPhase.active = true; + } + } + // Rest of day = Distribution (10:00 - 20:00 UTC) + else if(hour >= 10 && hour < 20) + { + if(g_amdPhase.phase == AMD_MANIPULATION) + { + // [v6.42] Check AMD_DistMinMove before transitioning + double distMove = MathAbs(close[0] - (g_amdPhase.rangeHigh + g_amdPhase.rangeLow) / 2); + double distMinReq = (g_cachedATR > 0) ? g_cachedATR * (g_workingAMD_DistMinMove > 0 ? g_workingAMD_DistMinMove : AMD_DistMinMove) : 0; + if(distMove < distMinReq) return; // Not enough distribution move yet + g_amdPhase.phase = AMD_DISTRIBUTION; + g_amdPhase.active = true; + } + } + else + { + // Reset for next day + g_amdPhase.phase = AMD_NONE; + g_amdPhase.active = false; + } + if(AMD_ShowPhases) DrawAMDPhase(); + // [v6.42] AMD_TradeInDist / AMD_TradeInManip filter for signal generation + // (Tracked in g_amdPhase for use in EA_CheckSignals) +} +//+------------------------------------------------------------------+ +//| Draw AMD Phase | +//+------------------------------------------------------------------+ +void DrawAMDPhase() +{ + if(!g_amdPhase.active) return; + string phaseName = GetAMDPhaseString(g_amdPhase.phase); + color phaseColor = clrGray; + // [v6.42] Use AMD color inputs + switch(g_amdPhase.phase) + { + case AMD_ACCUMULATION: phaseColor = AMD_AccumColor; break; + case AMD_MANIPULATION: phaseColor = AMD_ManipColor; break; + case AMD_DISTRIBUTION: phaseColor = AMD_DistColor; break; + } + // Original switch for any fallback + switch(g_amdPhase.phase) + { + case AMD_ACCUMULATION: phaseColor = AMD_AccumColor; break; + case AMD_MANIPULATION: phaseColor = AMD_ManipColor; break; + case AMD_DISTRIBUTION: phaseColor = AMD_DistColor; break; + } + // Draw phase label on dashboard or chart corner + string objName = "AMD_Phase_Label"; + ObjectDelete(0, objName); + if(!Regime_ShowOnChart) return; // [v6.42] + ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0); + ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER); + ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, 20); + ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, 200); + ObjectSetString(0, objName, OBJPROP_TEXT, "AMD: " + phaseName); + ObjectSetInteger(0, objName, OBJPROP_COLOR, phaseColor); + ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, 10); +} +//+------------------------------------------------------------------+ +//| Get AMD Phase String | +//+------------------------------------------------------------------+ +string GetAMDPhaseString(ENUM_AMD_PHASE phase) +{ + switch(phase) + { + case AMD_ACCUMULATION: return "ACCUMULATION"; + case AMD_MANIPULATION: return "MANIPULATION"; + case AMD_DISTRIBUTION: return "DISTRIBUTION"; + default: return "NONE"; + } +} +//+==================================================================+ +//| JUDAS SWING IMPLEMENTATION | +//+==================================================================+ +//+------------------------------------------------------------------+ +//| Detect Judas Swing | +//+------------------------------------------------------------------+ +void DetectJudasSwing(const datetime &time[], const double &high[], + const double &low[], const double &close[], int limit) +{ + if(!Judas_Enabled) return; + MqlDateTime dt; + TimeToStruct(time[0], dt); + // Judas swings typically occur in London/NY open + int hour = dt.hour; + if(hour < 7 || hour > 15) return; + // Need at least 3 bars + if(limit < 3) return; + double minMove = Judas_MinMove * g_cachedATR; + // Check for bullish Judas (fake move down, then reversal up) + if(low[1] < low[2] && close[0] > high[1]) + { + double fakeMove = low[2] - low[1]; + if(fakeMove >= minMove) + { + if(g_judasCount < g_maxJudas) + { + ArrayResize(g_judasSwings, g_judasCount + 1); + g_judasSwings[g_judasCount].swingPrice = low[1]; + g_judasSwings[g_judasCount].reversalPrice = close[0]; + g_judasSwings[g_judasCount].time = time[1]; + g_judasSwings[g_judasCount].bar = 1; + g_judasSwings[g_judasCount].type = 1; // Bullish + g_judasSwings[g_judasCount].objName = "Judas_" + IntegerToString(g_judasCount); + g_judasSwings[g_judasCount].active = true; + g_judasSwings[g_judasCount].confirmed = true; + g_judasActive = true; + g_judasCount++; + if(g_verboseLog) + Print("[DANGER] Judas Bullish detected at ", DoubleToString(low[1], _Digits)); + } + } + } + // Check for bearish Judas (fake move up, then reversal down) + if(high[1] > high[2] && close[0] < low[1]) + { + double fakeMove = high[1] - high[2]; + if(fakeMove >= minMove) + { + if(g_judasCount < g_maxJudas) + { + ArrayResize(g_judasSwings, g_judasCount + 1); + g_judasSwings[g_judasCount].swingPrice = high[1]; + g_judasSwings[g_judasCount].reversalPrice = close[0]; + g_judasSwings[g_judasCount].time = time[1]; + g_judasSwings[g_judasCount].bar = 1; + g_judasSwings[g_judasCount].type = -1; // Bearish + g_judasSwings[g_judasCount].objName = "Judas_" + IntegerToString(g_judasCount); + g_judasSwings[g_judasCount].active = true; + g_judasSwings[g_judasCount].confirmed = true; + g_judasActive = true; + g_judasCount++; + if(g_verboseLog) + Print("[DANGER] Judas Bearish detected at ", DoubleToString(high[1], _Digits)); + } + } + } + if(Judas_ShowSwings) DrawJudasSwings(); +} +//+------------------------------------------------------------------+ +//| Draw Judas Swings | +//+------------------------------------------------------------------+ +void DrawJudasSwings() +{ + for(int i = 0; i < g_judasCount; i++) + { + if(!g_judasSwings[i].active) continue; + string name = g_judasSwings[i].objName; + // Draw arrow + ENUM_OBJECT arrowType = (g_judasSwings[i].type == 1) ? OBJ_ARROW_UP : OBJ_ARROW_DOWN; + ObjectCreate(0, name + "_arrow", arrowType, 0, g_judasSwings[i].time, g_judasSwings[i].swingPrice); + ObjectSetInteger(0, name + "_arrow", OBJPROP_COLOR, Judas_Color); + ObjectSetInteger(0, name + "_arrow", OBJPROP_WIDTH, 3); + // Draw label + ObjectCreate(0, name + "_label", OBJ_TEXT, 0, g_judasSwings[i].time, g_judasSwings[i].swingPrice); + ObjectSetString(0, name + "_label", OBJPROP_TEXT, "JUDAS"); + ObjectSetInteger(0, name + "_label", OBJPROP_COLOR, Judas_Color); + ObjectSetInteger(0, name + "_label", OBJPROP_FONTSIZE, 9); + } +} +//+------------------------------------------------------------------+ +//| Cleanup Judas Swings | +//+------------------------------------------------------------------+ +void CleanupJudasSwings() +{ + for(int i = g_judasCount - 1; i >= 0; i--) + { + ObjectDelete(0, g_judasSwings[i].objName + "_arrow"); + ObjectDelete(0, g_judasSwings[i].objName + "_label"); + } + ArrayResize(g_judasSwings, 0); + g_judasCount = 0; + g_judasActive = false; +} +//+==================================================================+ +//| MARKET REGIME DETECTION IMPLEMENTATION | +//+==================================================================+ +//+==================================================================+ +//| SMART ENTRY SYSTEM IMPLEMENTATION | +//+==================================================================+ +//+------------------------------------------------------------------+ +//| Evaluate Smart Entry — FIX#370 WRAPPER | +//| Delegates scoring to ComputeUnifiedScore (cascadeAvailable=true)| +//| Maps UnifiedScoreResult → SmartEntryDecision. | +//| Gate logic (minConf, EV, WinP, CT, Choppy) retained here. | +//+------------------------------------------------------------------+ +// ========================================================================= +// EvaluateSmartEntry — Final validation before opening a trade +// +// Called by EA_CheckSignals AFTER SelectBestCandidate has already filtered: +// - technique enabled, D1 direction, CT/MTF, score, RR, confirmations, KZ, divergence +// +// SmartEntry's job: validate the QUALITY of the specific setup — not repeat +// the pre-filters. It adds 4 dimensions SelectBestCandidate cannot: +// +// 1. Score — ComputeUnifiedScore (14 modules: trend, timing, RR, momentum, +// ML, regime, cascade zone/confluence/sweep/timing/pattern/tech, +// HTF bonus, legacy). Final quality number. +// +// 2. DOL (Draw On Liquidity) — institutional liquidity magnet direction. +// If price is strongly drawn to an opposing BSL/SSL, the trade fights +// the delivery mechanism. Hard block if DOL ratio > 2×. +// +// 3. Technique-specific ADX gate — different strategies need different +// momentum levels: BOS_RETEST needs ADX≥20 (confirmed break), +// TC needs ADX≥22 (trend continuation in trend). +// OTE/FVG/OB work in any regime — no ADX floor. +// +// 4. Prior bar confirmation (H4+) — higher TF entries need the previous +// candle to confirm direction. Single-bar noise entry on H4 = stop hunt. +// Exception: liquidity sweep in prior bar = valid aggressive entry. +// +// 5. CT quality gate — counter-trend entries need extra evidence: +// EV scaled by ADX (stronger trend = need higher EV), +// minimum WinProb, minimum score. CT with ADX>50 also needs a +// recent liquidity sweep (institutional proof of flow reversal). +// +// 6. EV + WinProb final gate — trade must have positive expected value +// and sufficient win probability from pair table calibration. +// +// Connection to exit system: +// - Sets decision.isCounterTrend → EA_CheckSignals → g_ea_signal.isCounterTrend +// → AddMultiTPEntry → ProtectTrade (tighter BE) + EvaluateExit (2 signals) +// - adjustedScore → CalculatePositionSize (Kelly sizing) +// ========================================================================= + +SmartEntryDecision EvaluateSmartEntry(bool isBullish, double entryPrice, + double slPrice, double tp1Price, + double tp2Price, double tp3Price, + const ConfirmationCascade &existingCascade) +{ + SmartEntryDecision decision; + ZeroMemory(decision); + decision.analysisTime = TimeCurrent(); + decision.regimeAtEntry = (int)g_regimeData.regime; + decision.inKillzone = g_isInKillzone; + decision.amdPhase = g_amdData.phase; + + // ── 1. Compute Unified Score (14 modules, full cascade) ────────── + ConfluenceScoreStruct emptyConf; ZeroMemory(emptyConf); + CandlePatternStruct emptyCdl; ZeroMemory(emptyCdl); + UnifiedScoreResult u = ComputeUnifiedScore( + isBullish, entryPrice, slPrice, tp1Price, tp2Price, tp3Price, + emptyConf, emptyCdl, 0, + existingCascade, true); + + decision.rawScore = u.totalScore; + decision.adjustedScore = u.totalScore; + decision.finalConfidence = MathMin(100, u.totalScore); + decision.trendScore = u.trendScore; + decision.zoneScore = u.cascadeZoneScore; + decision.confluenceScore = u.cascadeConfluenceScore; + decision.sweepScore = u.cascadeSweepScore; + decision.timingScore = u.cascadeTimingScore; + decision.patternScore = u.cascadePatternScore; + decision.regimeBonus = u.regimeScore; + decision.winProbability = u.winProbability; + decision.expectedValue = u.expectedValue; + decision.isPositiveEV = u.isPositiveEV; + decision.quality = (u.grade == "A+") ? QUALITY_A_PLUS : + (u.grade == "A" ) ? QUALITY_A : + (u.grade == "B+" || u.grade == "B") ? QUALITY_B : + (u.grade == "C" ) ? QUALITY_C : + (u.grade == "D" ) ? QUALITY_D : QUALITY_REJECT; + decision.positionSizeMultiplier = GetAdaptivePositionMultiplier(u.totalScore); + decision.optimalRR = g_regimeData.optimalRR; + + // ── 2. DOL gate — institutional liquidity magnet ───────────────── + // ICT: price is always being delivered toward unswept liquidity (BSL/SSL). + // Trading against the DOL means fighting the delivery mechanism itself. + // DOL gate: 4 cases + // TWO-SIDE opposing + ratio > threshold → HARD BLOCK + // TWO-SIDE opposing + ratio <= threshold → soft penalty -15 + // ONE-SIDE opposing (no opposite target) → soft penalty -10 (weaker conviction) + // Aligned (one-side or two-side) → bonus +8 + if(g_dolValid && g_dolDirection != 0) + { + // TF-aware hard block threshold + // M5=1.80, M15/H1=2.00, H4=2.50, D1=3.00 + double _dolBlockRatio; + switch(_Period) + { + case PERIOD_M1: + case PERIOD_M5: _dolBlockRatio = 1.80; break; + case PERIOD_M15: + case PERIOD_M30: + case PERIOD_H1: _dolBlockRatio = 2.00; break; + case PERIOD_H4: _dolBlockRatio = 2.50; break; + default: _dolBlockRatio = 3.00; break; // D1+ + } + bool dolOpposes = ( isBullish && g_dolDirection < 0) || + (!isBullish && g_dolDirection > 0); + bool dolOneSide = (g_dolOppositeDistance >= 99998); + + if(dolOpposes) + { + if(!dolOneSide && g_dolDistance > 0) + { + // TWO-SIDE opposing: compute ratio + double ratio = g_dolOppositeDistance / g_dolDistance; + if(ratio > _dolBlockRatio) + { + decision.shouldEnter = false; + decision.rejectReason = StringFormat("DOL BLOCK: %s vs DOL=%s | ratio=%.1fx (thr=%.1f)", + isBullish?"BUY":"SELL", g_dolDirection>0?"UP":"DOWN", ratio, _dolBlockRatio); + g_lastSmartDecision = decision; + if(g_verboseLog) PrintFormat("[SmartEntry] %s", decision.rejectReason); + return decision; + } + decision.adjustedScore -= 15; + if(g_verboseLog) PrintFormat("[SmartEntry] DOL opposing -15: ratio=%.1fx", ratio); + } + else + { + // ONE-SIDE opposing: only one liquidity target, it points away from trade + // ICT: market has delivery target only on the other side → soft penalty + decision.adjustedScore -= 10; + if(g_verboseLog) PrintFormat("[SmartEntry] DOL ONE-SIDE opposing -10: DOL=%s", + g_dolDirection>0?"UP":"DOWN"); + } + } + else + { + // Aligned (one-side or two-side) → delivery supports this direction + decision.adjustedScore += 8; + } + } + + // ── 3. D1 raw candle bias — when CHoCH not yet confirmed ───────── + // Active only when pair table enables D1 CHoCH gate for this TF. + if(g_workingD1CHoCHGate && !g_d1CHoCH_Valid) + { + double d1c1 = iClose(_Symbol, PERIOD_D1, 1); + double d1c2 = iClose(_Symbol, PERIOD_D1, 2); + if(d1c1 > 0 && d1c2 > 0) + { + bool d1Bull = (d1c1 > d1c2); + bool opposes = ( isBullish && !d1Bull) || (!isBullish && d1Bull); + if(opposes) + { + double mtfConf = g_mtfAnalysis.overallConfidence; + if(mtfConf < 80.0) + { + decision.shouldEnter = false; + decision.rejectReason = StringFormat( + "D1 bias gate: %s trade vs D1=%s | MTF conf=%.0f%% < 80%%", + isBullish?"BUY":"SELL", d1Bull?"BULL":"BEAR", mtfConf); + g_lastSmartDecision = decision; + if(g_verboseLog) PrintFormat("[SmartEntry] %s", decision.rejectReason); + return decision; + } + decision.adjustedScore -= 8; // soft penalty when MTF ≥ 80% + } + } + } + else + { + // D1 CHoCH valid and aligned with trade = conviction bonus + bool chochAligns = ( isBullish && g_d1CHoCH_Bull) || + (!isBullish && g_d1CHoCH_Bear); + if(chochAligns) decision.adjustedScore += 5; + } + + // ── 4. Technique-specific ADX gate ─────────────────────────────── + // BOS_RETEST: the break must have momentum behind it (ADX≥20). + // A retest of a break with ADX=15 = no institutional commitment. + // TC (Trend Continuation): must be in a trend (ADX≥22). + // TC in flat market = EMA reversion is noise, not momentum. + // FVG/OB/OTE: structural zones valid in any volatility — no gate. + if(g_cachedADX > 0 && g_ea_signal.isValid) + { + double adxFloor = 0; + string sigType = g_ea_signal.type; + if(StringFind(sigType, "BOS") >= 0) adxFloor = 20.0; + if(StringFind(sigType, "TC") >= 0) adxFloor = 22.0; + if(adxFloor > 0 && g_cachedADX < adxFloor) + { + decision.shouldEnter = false; + decision.rejectReason = StringFormat( + "ADX gate: %s needs ADX≥%.0f, current=%.0f", + sigType, adxFloor, g_cachedADX); + g_lastSmartDecision = decision; + if(g_verboseLog) PrintFormat("[SmartEntry] %s", decision.rejectReason); + return decision; + } + } + + // ── 5. Prior bar confirmation (H4+) ────────────────────────────── + // On H4/D1, the previous bar's close must confirm trade direction. + // ICT: look for a 'displacement' candle or confirming close. + // BUY: prior bar close above midpoint (or lower 33% for D1 — pullback bar). + // SELL: prior bar close below midpoint (or upper 33% for D1). + // Soft penalty only (not block) — strong confluence can override bad prior bar. + // Exception: a liquidity sweep in the prior bar = valid aggressive entry. + if(_Period >= PERIOD_H4) + { + double c1 = iClose(_Symbol, _Period, 1); + double o1 = iOpen (_Symbol, _Period, 1); + double h1 = iHigh (_Symbol, _Period, 1); + double l1 = iLow (_Symbol, _Period, 1); + if(c1 > 0 && h1 > l1) + { + double range = h1 - l1; + // D1 uses 33% threshold (normal pullback bars in trend are ok) + // H4 uses midpoint (shorter TF needs tighter confirmation) + double threshBull = (_Period >= PERIOD_D1) ? l1 + range*0.33 : (h1+l1)*0.5; + double threshSell = (_Period >= PERIOD_D1) ? h1 - range*0.33 : (h1+l1)*0.5; + bool opposes = ( isBullish && c1 < threshBull) || + (!isBullish && c1 > threshSell); + if(opposes) + { + // Check for sweep exception — sweep in prior bar = valid OFC entry + bool sweepException = false; + datetime priorBarTime = iTime(_Symbol, _Period, 1); + for(int ls = ArraySize(LIQ_Array)-1; ls >= 0 && !sweepException; ls--) + { + if(!LIQ_Array[ls].isValid || !LIQ_Array[ls].swept) continue; + bool sweepDir = isBullish ? (!LIQ_Array[ls].isBSL) : (LIQ_Array[ls].isBSL); + if(sweepDir && LIQ_Array[ls].sweepTime >= priorBarTime) + sweepException = true; + } + if(!sweepException) + { + decision.adjustedScore -= 12; + if(g_verboseLog) + PrintFormat("[SmartEntry] Prior bar opposes %s (C=%.5f, thresh=%.5f) -12pts", + isBullish?"BUY":"SELL", c1, isBullish?threshBull:threshSell); + } + } + } + } + + // ── 6. Counter-trend quality gate ──────────────────────────────── + // SelectBestCandidate already ensured MTF supports the CT direction. + // SmartEntry adds: minimum EV + WinProb + score for CT trades. + // The harder the trend (ADX), the more evidence we need to counter it. + // When ADX>50: also require a recent liquidity sweep near entry (ICT OFC proof). + bool isCounterTrend = false; + { + // CT = opposing regime trend direction (same detection as SelectBestCandidate) + bool regTrendBull = (g_regimeData.regime == REGIME_STRONG_TREND_UP || + g_regimeData.regime == REGIME_TREND_UP || + g_regimeData.regime == REGIME_WEAK_TREND_UP); + bool regTrendBear = (g_regimeData.regime == REGIME_STRONG_TREND_DOWN || + g_regimeData.regime == REGIME_TREND_DOWN || + g_regimeData.regime == REGIME_WEAK_TREND_DOWN); + isCounterTrend = (regTrendBull && !isBullish) || (regTrendBear && isBullish); + + // Also CT if structure directly opposes + if(!isCounterTrend) + { + bool structOpposes = ( isBullish && !g_isBullishStructure) || + (!isBullish && g_isBullishStructure); + bool chochConfirms = ( isBullish && g_d1CHoCH_Bull && g_d1CHoCH_Valid) || + (!isBullish && g_d1CHoCH_Bear && g_d1CHoCH_Valid); + if(structOpposes && !chochConfirms) + { + // Structure opposes without CHoCH confirmation = hard block + // ICT: never sell into live bullish structure without confirmed reversal + decision.shouldEnter = false; + decision.rejectReason = StringFormat( + "Structural CT block: %s trade vs structure=%s (no CHoCH confirmation)", + isBullish?"BUY":"SELL", g_isBullishStructure?"BULL":"BEAR"); + g_lastSmartDecision = decision; + if(g_verboseLog) PrintFormat("[SmartEntry] %s", decision.rejectReason); + return decision; + } + if(structOpposes && chochConfirms) isCounterTrend = true; + } + + if(isCounterTrend) + { + decision.isCounterTrend = true; + + // Minimum EV scales with ADX — stronger trend = more evidence needed + // ADX=25: 1× base. ADX=50: 1.33×. ADX=75: 1.83×. ADX=100: 2.33×. + double ctEVBase = (g_autoOptParams.ct_ev_min > 0) ? g_autoOptParams.ct_ev_min : 0.20; + if(g_cachedADX > 25.0) + ctEVBase *= 1.0 + MathMax(0.0, MathMin(1.0, (g_cachedADX-25.0)/75.0)) * 2.5; + + double ctScoreMin = (g_autoOptParams.ct_score_min > 0) ? g_autoOptParams.ct_score_min : g_gates.counterFloor; + double ctWPMin = (g_autoOptParams.ct_wp_min > 0) ? g_autoOptParams.ct_wp_min : 60.0; + // EV-adaptive WP relaxation: high EV compensates for lower WP + if(u.expectedValue >= 0.50) ctWPMin = MathMax(ctWPMin - 10.0, 40.0); + else if(u.expectedValue >= 0.30) ctWPMin = MathMax(ctWPMin - 5.0, 40.0); + + bool ctFails = (u.expectedValue < ctEVBase) || + (u.totalScore < ctScoreMin) || + (u.winProbability < ctWPMin); + + if(ctFails) + { + decision.shouldEnter = false; + decision.rejectReason = StringFormat( + "CT quality gate: EV=%.2fR (min=%.2f) score=%d (min=%.0f) WP=%.0f%% (min=%.0f%%)", + u.expectedValue, ctEVBase, u.totalScore, ctScoreMin, u.winProbability, ctWPMin); + g_lastSmartDecision = decision; + if(g_verboseLog) PrintFormat("[SmartEntry] %s", decision.rejectReason); + return decision; + } + + // CT in strong trend (ADX>50): require recent liquidity sweep near entry + // ICT: OFC (Order Flow Confirmation) — the sweep proves delivery has shifted + if(_Period >= PERIOD_H1 && g_cachedADX > 50.0) + { + bool sweepFound = false; + datetime recentBar = iTime(_Symbol, _Period, 5); + for(int ls = 0; ls < ArraySize(LIQ_Array) && !sweepFound; ls++) + { + if(!LIQ_Array[ls].isValid || !LIQ_Array[ls].swept) continue; + bool sweepDir = isBullish ? (!LIQ_Array[ls].isBSL) : (LIQ_Array[ls].isBSL); + if(sweepDir && LIQ_Array[ls].sweepTime >= recentBar && + MathAbs(LIQ_Array[ls].price - entryPrice) <= 1.5*g_cachedATR) + sweepFound = true; + } + if(!sweepFound) + { + decision.shouldEnter = false; + decision.rejectReason = StringFormat( + "CT sweep gate: ADX=%.0f>50, no OFC sweep near entry", + g_cachedADX); + g_lastSmartDecision = decision; + if(g_verboseLog) PrintFormat("[SmartEntry] %s", decision.rejectReason); + return decision; + } + } + } + } + + // ── 7. Score cap — CT trades with inflated score ────────────────── + // A very high score on a CT trade often means conflicting signals + // are scoring each other up rather than genuine confluence. + // Only applies to CT (structure-opposing) trades. + if(g_autoOptParams.score_cap > 0 && decision.finalConfidence >= g_autoOptParams.score_cap) + { + bool structAligned = ( isBullish && g_isBullishStructure) || + (!isBullish && !g_isBullishStructure); + if(!structAligned) + { + decision.shouldEnter = false; + decision.rejectReason = StringFormat( + "CT score cap: %d >= cap=%d (CT conflict inflation)", + decision.finalConfidence, g_autoOptParams.score_cap); + g_lastSmartDecision = decision; + if(g_verboseLog) PrintFormat("[SmartEntry] %s", decision.rejectReason); + return decision; + } + } + + // ── 8. Final EV + WinProb + Score gate ─────────────────────────── + // Last check: trade must meet minimum expected value and quality. + // All thresholds from pair table via g_gates (calibrated per TF/pair). + double effectiveMinEV = g_gates.computed ? g_gates.minEV + : (AutoOpt_Enabled ? g_autoOptParams.smart_min_ev : SmartEntry_MinExpValue); + double effectiveMinWP = (AutoOpt_Enabled) + ? MathMax(40.0, g_autoOptParams.smart_min_win_prob) + : MathMax(40.0, SmartEntry_MinWinProb); + int effectiveMinScore = g_gates.computed ? (int)g_gates.minScore : (int)EA_MinEntryScore; + int minConf = g_gates.computed ? g_gates.minConfirmations + : (g_gates.computed ? g_gates.minConfirmations : 2); + + // EV≥0.15 allows WP bypass — high reward compensates lower probability + bool evBypassWP = (u.expectedValue >= 0.15); + + bool passes = (decision.adjustedScore >= effectiveMinScore) && + (u.expectedValue >= effectiveMinEV) && + (evBypassWP || u.winProbability >= effectiveMinWP) && + (existingCascade.totalConfirmed >= minConf); + + decision.shouldEnter = passes && SmartEntry_Enabled; + + if(!decision.shouldEnter) + { + if(decision.adjustedScore < effectiveMinScore) + decision.rejectReason = StringFormat("Low score: %d < %d", decision.adjustedScore, effectiveMinScore); + else if(u.expectedValue < effectiveMinEV) + decision.rejectReason = StringFormat("Low EV: %.2fR < %.2fR", u.expectedValue, effectiveMinEV); + else if(!evBypassWP && u.winProbability < effectiveMinWP) + decision.rejectReason = StringFormat("Low WP: %.1f%% < %.1f%%", u.winProbability, effectiveMinWP); + else if(existingCascade.totalConfirmed < minConf) + decision.rejectReason = StringFormat("Confirmations: %d/%d", existingCascade.totalConfirmed, minConf); + else + decision.rejectReason = "SmartEntry disabled"; + } + else + { + decision.entryReason = StringFormat( + "%s%s sc=%d EV=%.2fR WP=%.0f%% conf=%d/%d", + isBullish ? "BUY" : "SELL", + isCounterTrend ? "[CT]" : "", + decision.adjustedScore, u.expectedValue, u.winProbability, + existingCascade.totalConfirmed, minConf); + } + + if(g_verboseLog) + PrintFormat("[SmartEntry] %s | score=%d→%d | EV=%.2fR | WP=%.0f%% | CT=%s | %s", + isBullish?"BUY":"SELL", + decision.rawScore, decision.adjustedScore, + u.expectedValue, u.winProbability, + isCounterTrend?"Y":"N", + decision.shouldEnter ? "APPROVED" : ("REJECTED: "+decision.rejectReason)); + + g_lastSmartDecision = decision; + if(SmartEntry_LogDecisions) LogSmartDecision(decision); + return decision; +} +//+------------------------------------------------------------------+ +//| Calculate Win Probability - * FIXED v7.0 | +//+------------------------------------------------------------------+ +double CalculateWinProbability(int entryScore = 50) +{ + // * v7.4 FIX: WP was COMPLETELY decoupled from Score! + // * v9.22 FIX-G: EvaluateSmartEntry passes rawScore (max ~153), but CalculateEntryScore passes totalScore (max 85) + // Normalize both to 0-85 range for consistent WP calculation + // Scores above 85 are clamped -- extra points beyond 85 reflect exceptional confluence but WP caps at 70% + int normalizedScore = MathMin(entryScore, 85); + // === COMPONENT 1: Score-based WP (60% weight) === + // Map score 0-85 -> WP 30-65% + double scoreWP = 30.0 + (normalizedScore / 85.0) * 35.0; + // Score=85 -> 65%, Score=60 -> 55.2%, Score=40 -> 46.5%, Score=20 -> 38.2% + // === COMPONENT 2: Factor-based adjustments (40% weight) === + double adjustment = 0; + // Historical base rate (if available) + // * v7.5b FIX: Historical WR 16.1% -> clamped to 35% -> penalty -7.5 on EVERY signal! + // This is from multi-TP counting bug (3 partial closes = 3 "trades", 1 win + 2 SL = 33% WR) + // and perf-persistence loading stale data from previous bad runs. + // FIX: Only apply historical penalty if we have ENOUGH recent trades in THIS run, + // and cap the penalty to +/-3 points instead of +/-7.5 (was 0.5 weight). + if(g_totalHistoricalTrades > 20) + { + double histRate = g_historicalWinRate * 100.0; + histRate = MathMax(40, MathMin(60, histRate)); // * Tighter clamp: 35->40 min, 65->60 max + adjustment += (histRate - 50.0) * 0.3; // * Reduced weight: 0.5->0.3 (max +/-3 pts) + } + // Structure alignment + bool structureAligned = false; + if(g_ea_signal.isValid) + { + structureAligned = (g_ea_signal.isBullish && g_isBullishStructure) || + (!g_ea_signal.isBullish && !g_isBullishStructure); + } + if(structureAligned) + adjustment += 3; + else + adjustment -= 3; // * v7.4: -5->-3 (score already penalizes counter-structure) + // * v7.5b FIX: MTF alignment penalty in WinP + // Previously: SELL@MTF=STRONG_BULL had same WinP as SELL@MTF=NEUTRAL -> lost every trade + // A counter-HTF trade during a strong trend is much less likely to win + if(MTF_Enabled && g_ea_signal.isValid) + { + bool mtfAligned = (g_ea_signal.isBullish && (g_mtfAnalysis.overallDirection == MTF_BULLISH || g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH)) || + (!g_ea_signal.isBullish && (g_mtfAnalysis.overallDirection == MTF_BEARISH || g_mtfAnalysis.overallDirection == MTF_STRONG_BEARISH)); + bool mtfStrongOpposed = (g_ea_signal.isBullish && g_mtfAnalysis.overallDirection == MTF_STRONG_BEARISH) || + (!g_ea_signal.isBullish && g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH); + bool mtfOpposed = (g_ea_signal.isBullish && (g_mtfAnalysis.overallDirection == MTF_BEARISH || g_mtfAnalysis.overallDirection == MTF_STRONG_BEARISH)) || + (!g_ea_signal.isBullish && (g_mtfAnalysis.overallDirection == MTF_BULLISH || g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH)); + if(mtfAligned) + adjustment += 4; // HTF supports direction + else if(mtfStrongOpposed) + adjustment -= 8; // Trading against STRONG HTF trend = very dangerous + else if(mtfOpposed) + adjustment -= 5; // Trading against moderate HTF trend + } + // Killzone + if(g_isInKillzone) adjustment += 2; + // Judas + if(g_judasActive) adjustment += 3; + // Regime -- * v7.4 FIX: UNKNOWN treated as neutral (was 0 = no boost for 65% of bars!) + if(g_regimeData.regime == REGIME_TRENDING || + g_regimeData.regime == REGIME_TREND_UP || + g_regimeData.regime == REGIME_TREND_DOWN || + g_regimeData.regime == REGIME_STRONG_TREND_UP || + g_regimeData.regime == REGIME_STRONG_TREND_DOWN) + adjustment += 3; + else if(g_regimeData.regime == REGIME_VOLATILE || g_regimeData.regime == REGIME_CHOPPY) + adjustment -= 3; // * v7.4: -5->-3 (was blocking ALL volatile entries) + else if(g_regimeData.regime == REGIME_RANGING || g_regimeData.regime == REGIME_RANGING_TIGHT || + g_regimeData.regime == REGIME_RANGING_WIDE) + adjustment -= 2; + // UNKNOWN regime: 0 adjustment (neutral, as intended) + // Premium/Discount zone + bool inOptimalZone = false; + if(g_ea_signal.isValid) + { + inOptimalZone = (g_ea_signal.isBullish && g_currentPDZone == "DISCOUNT") || + (!g_ea_signal.isBullish && g_currentPDZone == "PREMIUM"); + } + if(inOptimalZone) adjustment += 2; + // RSI extreme + if(g_cachedRSI > 0 && g_ea_signal.isValid) + { + if(g_ea_signal.isBullish && g_cachedRSI < 30) adjustment += 2; + else if(!g_ea_signal.isBullish && g_cachedRSI > 70) adjustment += 2; + else if(g_ea_signal.isBullish && g_cachedRSI > 70) adjustment -= 3; + else if(!g_ea_signal.isBullish && g_cachedRSI < 30) adjustment -= 3; + } + // Losing streak -- * FIX#18a: CAPPED to prevent death spiral + // OLD: streak -5 = -7.5pts, streak -7 = -10.5pts -> WP<40% -> blocks ALL signals -> streak NEVER breaks + // NEW: Max -4pts (streak -3=-1.5, -4=-3, -5+=-4 cap). Can never single-handedly push WP below threshold. + if(g_currentStreak < -3) + { + double streakPenalty = MathMin(4.0, (MathAbs(g_currentStreak) - 2) * 1.0); // -3->1, -4->2, -5->3, -6+->4 cap + adjustment -= streakPenalty; + } + // === COMBINE: 60% score-based + 40% factor-adjusted === + double finalWP = scoreWP * 0.60 + (50.0 + adjustment) * 0.40; + // * v7.4 ranges: + // Score=90, all positive: 61.5*0.6 + 60*0.4 = 36.9 + 24.0 = 60.9% + // Score=70, neutral: 54.5*0.6 + 50*0.4 = 32.7 + 20.0 = 52.7% + // Score=40, counter-str: 44.0*0.6 + 44*0.4 = 26.4 + 17.6 = 44.0% + // Score=20, worst case: 37.0*0.6 + 38*0.4 = 22.2 + 15.2 = 37.4% + // * v9.10: NN-based TP1 probability adjustment (+/-6 points max) + if(g_nnReadyForUse) + { + double nnWPAdj = (g_nnTP1WinProb - 0.50) * 20.0; // range: -5 to +6 + finalWP += MathMax(-6.0, MathMin(6.0, nnWPAdj)); + } + return MathMin(70, MathMax(30, finalWP)); +} +//+------------------------------------------------------------------+ +//| Calculate Expected Value | +//+------------------------------------------------------------------+ +double CalculateExpectedValue(double winProb, double rr) +{ + // EV = (Win% * RR) - (Loss% * 1) + double lossProb = 1.0 - winProb; + return (winProb * rr) - (lossProb * 1.0); +} +//+------------------------------------------------------------------+ +//| Get Adaptive Position Multiplier | +//+------------------------------------------------------------------+ +double GetAdaptivePositionMultiplier(int explicitConfidence = -1) +{ + if(!SmartEntry_AdaptiveSize || !SmartEntry_AdaptToRegime) return 1.0; // [v6.42] + double multiplier = 1.0; + // * FIX v7.3: Use explicit confidence from caller, not stale g_lastSmartDecision + // Previously used g_lastSmartDecision which contained PREVIOUS trade's data + int confidence = (explicitConfidence >= 0) ? explicitConfidence : g_smartEntry.finalConfidence; + // * v9.26 FIX#92: Score tiers recalibrated for adjustedScore 0-100 range. + // OLD thresholds (50/47/43/38/36) were for legacy max=85 system -> virtually ALL trades hit >=50 + // -> always returned x1.40 -> tiering was dead code (no differentiation). + // NEW: aligned with QUALITY grades defined in EvaluateSmartEntry. + // A+: score>=90 -> max lots (+40%) | Excellent: structure+KZ+HTF+FVG all aligned + // A : score>=72 -> +30% | Strong: most confirmations present + // B : score>=58 -> +15% | Good: standard quality trade + // C : score>=44 -> normal (x1.00) | Acceptable: passes minimum thresholds + // D : score>=36 -> -20% | Marginal: just above floor + // else -> x0.50 | Poor: should rarely execute (below filter) + if(confidence >= 90) + multiplier = 1.40; + else if(confidence >= 72) + multiplier = 1.30; + else if(confidence >= 58) + multiplier = 1.15; + else if(confidence >= 44) + multiplier = 1.00; + else if(confidence >= 36) + multiplier = 0.80; + else + multiplier = 0.50; + // Adjust for regime + if(g_regimeData.regime == REGIME_VOLATILE) + multiplier *= 0.7; + else if(g_regimeData.regime == REGIME_TRENDING) + multiplier *= 1.1; + // * v9.27 FIX#96: Score-aware return cap (was always MathMin(1.5)) + // A+ on win streak: up to 2.5x | A: up to 2.0x | Others: 1.5x (original) + double returnCap = 1.5; + if(PosSize_WinStreak_Uncap && g_currentStreak > 0) + { + if(confidence >= 90) returnCap = 2.5; + else if(confidence >= 72) returnCap = 2.0; + } + else if(confidence >= 90) returnCap = 2.0; + return MathMin(returnCap, MathMax(0.3, multiplier)); +} +//+------------------------------------------------------------------+ +//| Log Smart Decision | +//+------------------------------------------------------------------+ +void LogSmartDecision(SmartEntryDecision &decision) +{ + Print("==========================================================="); + Print("[ML] SMART ENTRY DECISION"); + Print("==========================================================="); + Print("Should Enter: ", decision.shouldEnter ? "YES [OK]" : "NO [X]"); + Print("Score: ", decision.finalConfidence, "/100"); + Print("Win Probability: ", DoubleToString(decision.winProbability, 1), "%"); + Print("Expected Value: ", DoubleToString(decision.expectedValue, 2), "R"); + Print("Position Multiplier: ", DoubleToString(decision.positionSizeMultiplier, 2), "x"); + Print("Regime: ", GetRegimeString((ENUM_MARKET_REGIME)decision.regimeAtEntry)); + // * v7.5c: Show effective thresholds (auto-opt vs input) + if(AutoOpt_Enabled) + Print("Thresholds [AutoOpt]: MinConf=", g_autoOptParams.smart_min_confidence, + " | MinWP=", DoubleToString(g_autoOptParams.smart_min_win_prob, 1), "%", + " | MinEV=", DoubleToString(g_autoOptParams.smart_min_ev, 2), "R"); + else + Print("Thresholds [Input]: MinConf=", SmartEntry_MinConfidence, + " | MinWP=", DoubleToString(SmartEntry_MinWinProb, 1), "%", + " | MinEV=", DoubleToString(SmartEntry_MinExpValue, 2), "R"); + if(decision.shouldEnter) + Print("Reason: ", decision.entryReason); + else + Print("Reject Reason: ", decision.rejectReason); + Print("==========================================================="); +} +//+------------------------------------------------------------------+ +//| Get Entry Grade | +//+------------------------------------------------------------------+ +string GetEntryGrade(int score) +{ + // * v9.22 FIX-F (Block 3/3): Recalibrated for EvaluateSmartEntry real max~153 + // OLD (FIX#17): score>=50=A+ (calibrated for max=85 -- only 33% of real max!) + // NEW: aligned with EvaluateSmartEntry quality grades + if(score >= 90) return "A+"; + if(score >= 72) return "A"; + if(score >= 58) return "B"; + if(score >= 44) return "C"; + if(score >= 32) return "D"; + return "F"; +} +//+------------------------------------------------------------------+ +//| Detect Candle Patterns | +//+------------------------------------------------------------------+ +CandlePatternStruct DetectCandlePatterns(int bar) +{ + CandlePatternStruct pattern; + ZeroMemory(pattern); + double open0 = iOpen(_Symbol, _Period, bar); + double close0 = iClose(_Symbol, _Period, bar); + double high0 = iHigh(_Symbol, _Period, bar); + double low0 = iLow(_Symbol, _Period, bar); + double open1 = iOpen(_Symbol, _Period, bar + 1); + double close1 = iClose(_Symbol, _Period, bar + 1); + double high1 = iHigh(_Symbol, _Period, bar + 1); + double low1 = iLow(_Symbol, _Period, bar + 1); + double body0 = MathAbs(close0 - open0); + double body1 = MathAbs(close1 - open1); + double range0 = MathMax(high0 - low0, _Point); // * v7.5c: prevent zero divide + // Bullish Engulfing + if(close1 < open1 && close0 > open0 && + close0 > open1 && open0 < close1 && + body0 > body1 * 1.1) + { + pattern.isBullishEngulfing = true; + pattern.patternStrength = 2; + pattern.patternName = "Bullish Engulfing"; + } + // Bearish Engulfing + if(close1 > open1 && close0 < open0 && + close0 < open1 && open0 > close1 && + body0 > body1 * 1.1) + { + pattern.isBearishEngulfing = true; + pattern.patternStrength = 2; + pattern.patternName = "Bearish Engulfing"; + } + // Bullish Pin Bar + double lowerWick = MathMin(open0, close0) - low0; + double upperWick = high0 - MathMax(open0, close0); + if(range0 > 0 && lowerWick > body0 * 2.0 && upperWick < body0 * 0.5) + { + pattern.isBullishPinBar = true; + pattern.patternStrength = 3; + pattern.patternName = "Bullish Pin Bar"; + } + // Bearish Pin Bar + if(range0 > 0 && upperWick > body0 * 2.0 && lowerWick < body0 * 0.5) + { + pattern.isBearishPinBar = true; + pattern.patternStrength = 3; + pattern.patternName = "Bearish Pin Bar"; + } + g_lastCandlePattern = pattern; + return pattern; +} +//+------------------------------------------------------------------+ +//| Initialize Extended Candle Patterns | +//+------------------------------------------------------------------+ +void InitializeExtendedCandlePatterns() +{ + ArrayResize(g_extendedCandlePatterns, 0); + g_extendedCandleCount = 0; + ZeroMemory(g_lastExtendedCandlePattern); + if(g_verboseLog) + Print("[OK] Extended Candle Patterns initialized"); +} +//+------------------------------------------------------------------+ +//| Master Candlestick Detection Function | +//+------------------------------------------------------------------+ +CandlePatternStructExtended DetectAllCandlePatterns(int bar) +{ + CandlePatternStructExtended pattern; + ZeroMemory(pattern); + if(!CandlePatterns_Enabled) return pattern; + if(bar + 3 >= Bars(_Symbol, _Period)) return pattern; + // Get candle data + double open0 = iOpen(_Symbol, _Period, bar); + double high0 = iHigh(_Symbol, _Period, bar); + double low0 = iLow(_Symbol, _Period, bar); + double close0 = iClose(_Symbol, _Period, bar); + double open1 = iOpen(_Symbol, _Period, bar + 1); + double high1 = iHigh(_Symbol, _Period, bar + 1); + double low1 = iLow(_Symbol, _Period, bar + 1); + double close1 = iClose(_Symbol, _Period, bar + 1); + double open2 = iOpen(_Symbol, _Period, bar + 2); + double high2 = iHigh(_Symbol, _Period, bar + 2); + double low2 = iLow(_Symbol, _Period, bar + 2); + double close2 = iClose(_Symbol, _Period, bar + 2); + // Calculate candle properties + double body0 = MathAbs(close0 - open0); + double body1 = MathAbs(close1 - open1); + double body2 = MathAbs(close2 - open2); + double range0 = MathMax(high0 - low0, _Point); // * v7.5c: prevent zero divide + double range1 = MathMax(high1 - low1, _Point); // * v7.5c: prevent zero divide + double range2 = MathMax(high2 - low2, _Point); // * v7.5c: prevent zero divide + double upperWick0 = high0 - MathMax(open0, close0); + double lowerWick0 = MathMin(open0, close0) - low0; + double upperWick1 = high1 - MathMax(open1, close1); + double lowerWick1 = MathMin(open1, close1) - low1; + bool isBull0 = close0 > open0; + bool isBear0 = close0 < open0; + bool isBull1 = close1 > open1; + bool isBear1 = close1 < open1; + bool isBull2 = close2 > open2; + bool isBear2 = close2 < open2; + // =============================================================== + // SINGLE CANDLE PATTERNS + // =============================================================== + // DOJI Patterns + if(Doji_Enabled && range0 > 0) + { + double bodyRatio = body0 / range0; + if(bodyRatio <= Doji_MaxBodyRatio) + { + pattern.isDoji = true; + pattern.patternType = CANDLE_DOJI; + pattern.candlesInPattern = 1; + // Dragonfly Doji (long lower shadow, no upper shadow) + if(lowerWick0 > body0 * 2 && upperWick0 < range0 * 0.1) + { + pattern.isDojiDragonfly = true; + pattern.patternType = CANDLE_DOJI_DRAGONFLY; + pattern.isBullish = true; + pattern.isReversal = true; + pattern.patternStrength = 3; + pattern.patternName = "Dragonfly Doji"; + pattern.reliability = 0.65; + } + // Gravestone Doji (long upper shadow, no lower shadow) + else if(upperWick0 > body0 * 2 && lowerWick0 < range0 * 0.1) + { + pattern.isDojiGravestone = true; + pattern.patternType = CANDLE_DOJI_GRAVESTONE; + pattern.isBearish = true; + pattern.isReversal = true; + pattern.patternStrength = 3; + pattern.patternName = "Gravestone Doji"; + pattern.reliability = 0.65; + } + // Long-legged Doji + else if(upperWick0 > body0 * 1.5 && lowerWick0 > body0 * 1.5) + { + pattern.isDojiLongLegged = true; + pattern.patternType = CANDLE_DOJI_LONG_LEGGED; + pattern.patternStrength = 2; + pattern.patternName = "Long-Legged Doji"; + pattern.reliability = 0.55; + } + // Standard Doji + else + { + pattern.patternStrength = 2; + pattern.patternName = "Doji"; + pattern.reliability = 0.50; + } + } + } + // HAMMER / HANGING MAN / INVERTED HAMMER / SHOOTING STAR + if(Hammer_Enabled && range0 > 0 && body0 > 0) + { + double bodyRatio = body0 / range0; + double lowerWickRatio = lowerWick0 / body0; + double upperWickRatio = upperWick0 / body0; + // Hammer (bullish reversal at bottom) + if(lowerWickRatio >= Hammer_MinWickRatio && + upperWick0 / range0 <= Hammer_MaxUpperWick && + bodyRatio > 0.1) + { + pattern.isHammer = true; + pattern.patternType = CANDLE_HAMMER; + pattern.isBullish = true; + pattern.isReversal = true; + pattern.candlesInPattern = 1; + pattern.patternStrength = 3; + pattern.patternName = "Hammer"; + pattern.reliability = 0.60; + } + // Inverted Hammer (bullish reversal at bottom) + if(upperWickRatio >= Hammer_MinWickRatio && + lowerWick0 / range0 <= Hammer_MaxUpperWick && + bodyRatio > 0.1) + { + pattern.isInvertedHammer = true; + pattern.patternType = CANDLE_INVERTED_HAMMER; + pattern.isBullish = true; + pattern.isReversal = true; + pattern.candlesInPattern = 1; + pattern.patternStrength = 2; + pattern.patternName = "Inverted Hammer"; + pattern.reliability = 0.55; + } + // Hanging Man (bearish reversal at top) - same shape as hammer + // Context determines if it's Hammer or Hanging Man + // Shooting Star (bearish reversal at top) - same as inverted hammer + if(upperWickRatio >= Hammer_MinWickRatio && + lowerWick0 / range0 <= Hammer_MaxUpperWick && + bodyRatio > 0.1) + { + pattern.isShootingStar = true; + pattern.patternType = CANDLE_SHOOTING_STAR; + pattern.isBearish = true; + pattern.isReversal = true; + pattern.candlesInPattern = 1; + pattern.patternStrength = 3; + pattern.patternName = "Shooting Star"; + pattern.reliability = 0.60; + } + } + // SPINNING TOP + if(range0 > 0) + { + double bodyRatio = body0 / range0; + if(bodyRatio > 0.1 && bodyRatio < 0.3 && + upperWick0 > body0 * 0.5 && lowerWick0 > body0 * 0.5) + { + pattern.isSpinningTop = true; + pattern.patternType = CANDLE_SPINNING_TOP; + pattern.candlesInPattern = 1; + pattern.patternStrength = 1; + pattern.patternName = "Spinning Top"; + pattern.reliability = 0.45; + } + } + // MARUBOZU + if(Marubozu_Enabled && range0 > 0) + { + double totalWicks = upperWick0 + lowerWick0; + if(totalWicks / range0 <= Marubozu_MaxWickRatio) + { + if(isBull0) + { + pattern.isMarubozuBull = true; + pattern.patternType = CANDLE_MARUBOZU_BULL; + pattern.isBullish = true; + pattern.isContinuation = true; + pattern.candlesInPattern = 1; + pattern.patternStrength = 4; + pattern.patternName = "Bullish Marubozu"; + pattern.reliability = 0.70; + } + else if(isBear0) + { + pattern.isMarubozuBear = true; + pattern.patternType = CANDLE_MARUBOZU_BEAR; + pattern.isBearish = true; + pattern.isContinuation = true; + pattern.candlesInPattern = 1; + pattern.patternStrength = 4; + pattern.patternName = "Bearish Marubozu"; + pattern.reliability = 0.70; + } + } + } + // =============================================================== + // TWO CANDLE PATTERNS + // =============================================================== + // ENGULFING (Already exists but enhance) + if(body0 > body1 * 1.1) + { + // Bullish Engulfing + if(isBear1 && isBull0 && close0 > open1 && open0 < close1) + { + pattern.isBullishEngulfing = true; + pattern.patternType = CANDLE_ENGULFING_BULL; + pattern.isBullish = true; + pattern.isReversal = true; + pattern.candlesInPattern = 2; + pattern.patternStrength = 4; + pattern.patternName = "Bullish Engulfing"; + pattern.reliability = 0.75; + } + // Bearish Engulfing + else if(isBull1 && isBear0 && close0 < open1 && open0 > close1) + { + pattern.isBearishEngulfing = true; + pattern.patternType = CANDLE_ENGULFING_BEAR; + pattern.isBearish = true; + pattern.isReversal = true; + pattern.candlesInPattern = 2; + pattern.patternStrength = 4; + pattern.patternName = "Bearish Engulfing"; + pattern.reliability = 0.75; + } + } + // HARAMI + if(Harami_Enabled && body1 > 0) + { + double bodyRatio = body0 / body1; + if(bodyRatio <= Harami_MaxBodyRatio) + { + // Bullish Harami + if(isBear1 && isBull0 && + high0 < high1 && low0 > low1 && + open0 > close1 && close0 < open1) + { + pattern.isBullishHarami = true; + pattern.patternType = CANDLE_HARAMI_BULL; + pattern.isBullish = true; + pattern.isReversal = true; + pattern.candlesInPattern = 2; + pattern.patternStrength = 3; + pattern.patternName = "Bullish Harami"; + pattern.reliability = 0.60; + // Check for Harami Cross (small body = doji) + if(body0 / range0 <= Doji_MaxBodyRatio) + { + pattern.isBullishHaramiCross = true; + pattern.patternType = CANDLE_HARAMI_CROSS_BULL; + pattern.patternStrength = 4; + pattern.patternName = "Bullish Harami Cross"; + pattern.reliability = 0.70; + } + } + // Bearish Harami + else if(isBull1 && isBear0 && + high0 < high1 && low0 > low1 && + open0 < close1 && close0 > open1) + { + pattern.isBearishHarami = true; + pattern.patternType = CANDLE_HARAMI_BEAR; + pattern.isBearish = true; + pattern.isReversal = true; + pattern.candlesInPattern = 2; + pattern.patternStrength = 3; + pattern.patternName = "Bearish Harami"; + pattern.reliability = 0.60; + // Check for Harami Cross + if(body0 / range0 <= Doji_MaxBodyRatio) + { + pattern.isBearishHaramiCross = true; + pattern.patternType = CANDLE_HARAMI_CROSS_BEAR; + pattern.patternStrength = 4; + pattern.patternName = "Bearish Harami Cross"; + pattern.reliability = 0.70; + } + } + } + } + // PIERCING LINE / DARK CLOUD COVER + if(PiercingDarkCloud_Enabled && body1 > 0) + { + // Piercing Line (bullish) + if(isBear1 && isBull0 && + open0 < low1 && // Opens below previous low + close0 > (open1 + close1) / 2 && // Closes above midpoint + close0 < open1) // But below previous open + { + double penetration = (close0 - close1) / body1; + if(penetration >= Piercing_MinPenetration) + { + pattern.isPiercingLine = true; + pattern.patternType = CANDLE_PIERCING_LINE; + pattern.isBullish = true; + pattern.isReversal = true; + pattern.candlesInPattern = 2; + pattern.patternStrength = 4; + pattern.patternName = "Piercing Line"; + pattern.reliability = 0.70; + } + } + // Dark Cloud Cover (bearish) + if(isBull1 && isBear0 && + open0 > high1 && // Opens above previous high + close0 < (open1 + close1) / 2 && // Closes below midpoint + close0 > open1) // But above previous open + { + double penetration = (close1 - close0) / body1; + if(penetration >= Piercing_MinPenetration) + { + pattern.isDarkCloudCover = true; + pattern.patternType = CANDLE_DARK_CLOUD_COVER; + pattern.isBearish = true; + pattern.isReversal = true; + pattern.candlesInPattern = 2; + pattern.patternStrength = 4; + pattern.patternName = "Dark Cloud Cover"; + pattern.reliability = 0.70; + } + } + } + // TWEEZER TOP / BOTTOM + if(Tweezer_Enabled && g_cachedATR > 0) + { + double tolerance = g_cachedATR * Tweezer_MaxDiff; + // Tweezer Top + if(MathAbs(high0 - high1) <= tolerance && + isBull1 && isBear0) + { + pattern.isTweezerTop = true; + pattern.patternType = CANDLE_TWEEZER_TOP; + pattern.isBearish = true; + pattern.isReversal = true; + pattern.candlesInPattern = 2; + pattern.patternStrength = 3; + pattern.patternName = "Tweezer Top"; + pattern.reliability = 0.60; + } + // Tweezer Bottom + if(MathAbs(low0 - low1) <= tolerance && + isBear1 && isBull0) + { + pattern.isTweezerBottom = true; + pattern.patternType = CANDLE_TWEEZER_BOTTOM; + pattern.isBullish = true; + pattern.isReversal = true; + pattern.candlesInPattern = 2; + pattern.patternStrength = 3; + pattern.patternName = "Tweezer Bottom"; + pattern.reliability = 0.60; + } + } + // =============================================================== + // THREE CANDLE PATTERNS + // =============================================================== + // MORNING STAR / EVENING STAR + if(Star_Enabled) + { + double middleBodyRatio = (range1 > 0) ? body1 / range1 : 1.0; + // Morning Star (bullish) + if(isBear2 && body2 > g_cachedATR * 0.3 && // First: big bearish + middleBodyRatio <= Star_MaxMiddleBody && // Second: small body (star) + isBull0 && body0 > g_cachedATR * 0.3 && // Third: big bullish + close0 > (open2 + close2) / 2) // Closes above midpoint of first + { + pattern.isMorningStar = true; + pattern.patternType = CANDLE_MORNING_STAR; + pattern.isBullish = true; + pattern.isReversal = true; + pattern.candlesInPattern = 3; + pattern.patternStrength = 5; + pattern.patternName = "Morning Star"; + pattern.reliability = 0.80; + // Check for Morning Doji Star + if(body1 / range1 <= Doji_MaxBodyRatio) + { + pattern.isMorningDojiStar = true; + pattern.patternType = CANDLE_MORNING_DOJI_STAR; + pattern.patternName = "Morning Doji Star"; + pattern.reliability = 0.85; + } + } + // Evening Star (bearish) + if(isBull2 && body2 > g_cachedATR * 0.3 && // First: big bullish + middleBodyRatio <= Star_MaxMiddleBody && // Second: small body (star) + isBear0 && body0 > g_cachedATR * 0.3 && // Third: big bearish + close0 < (open2 + close2) / 2) // Closes below midpoint of first + { + pattern.isEveningStar = true; + pattern.patternType = CANDLE_EVENING_STAR; + pattern.isBearish = true; + pattern.isReversal = true; + pattern.candlesInPattern = 3; + pattern.patternStrength = 5; + pattern.patternName = "Evening Star"; + pattern.reliability = 0.80; + // Check for Evening Doji Star + if(body1 / range1 <= Doji_MaxBodyRatio) + { + pattern.isEveningDojiStar = true; + pattern.patternType = CANDLE_EVENING_DOJI_STAR; + pattern.patternName = "Evening Doji Star"; + pattern.reliability = 0.85; + } + } + } + // THREE WHITE SOLDIERS / THREE BLACK CROWS + if(ThreeSoldiersCrows_Enabled && range0 > 0 && range1 > 0 && range2 > 0) + { + double bodyRatio0 = body0 / range0; + double bodyRatio1 = body1 / range1; + double bodyRatio2 = body2 / range2; + double wickRatio0 = (upperWick0 + lowerWick0) / range0; + double wickRatio1 = (upperWick1 + lowerWick1) / range1; + // Three White Soldiers + if(isBull0 && isBull1 && isBull2 && + bodyRatio0 >= ThreeSC_MinBodySize && + bodyRatio1 >= ThreeSC_MinBodySize && + bodyRatio2 >= ThreeSC_MinBodySize && + close0 > close1 && close1 > close2 && // Each closes higher + open0 > open1 && open1 > open2 && // Each opens higher + open0 < close1 && open1 < close2 && // Opens within previous body + wickRatio0 <= ThreeSC_MaxWickSize && + wickRatio1 <= ThreeSC_MaxWickSize) + { + pattern.isThreeWhiteSoldiers = true; + pattern.patternType = CANDLE_THREE_WHITE_SOLDIERS; + pattern.isBullish = true; + pattern.isReversal = true; + pattern.candlesInPattern = 3; + pattern.patternStrength = 5; + pattern.patternName = "Three White Soldiers"; + pattern.reliability = 0.85; + } + // Three Black Crows + if(isBear0 && isBear1 && isBear2 && + bodyRatio0 >= ThreeSC_MinBodySize && + bodyRatio1 >= ThreeSC_MinBodySize && + bodyRatio2 >= ThreeSC_MinBodySize && + close0 < close1 && close1 < close2 && // Each closes lower + open0 < open1 && open1 < open2 && // Each opens lower + open0 > close1 && open1 > close2 && // Opens within previous body + wickRatio0 <= ThreeSC_MaxWickSize && + wickRatio1 <= ThreeSC_MaxWickSize) + { + pattern.isThreeBlackCrows = true; + pattern.patternType = CANDLE_THREE_BLACK_CROWS; + pattern.isBearish = true; + pattern.isReversal = true; + pattern.candlesInPattern = 3; + pattern.patternStrength = 5; + pattern.patternName = "Three Black Crows"; + pattern.reliability = 0.85; + } + } + // THREE INSIDE UP / DOWN + if(ThreeInsideOutside_Enabled) + { + // Three Inside Up (bullish) + if(isBear2 && body2 > body1 && // First: big bearish + high1 < high2 && low1 > low2 && // Second: inside first + isBull0 && close0 > high2) // Third: bullish, closes above first high + { + pattern.isThreeInsideUp = true; + pattern.patternType = CANDLE_THREE_INSIDE_UP; + pattern.isBullish = true; + pattern.isReversal = true; + pattern.candlesInPattern = 3; + pattern.patternStrength = 4; + pattern.patternName = "Three Inside Up"; + pattern.reliability = 0.75; + } + // Three Inside Down (bearish) + if(isBull2 && body2 > body1 && // First: big bullish + high1 < high2 && low1 > low2 && // Second: inside first + isBear0 && close0 < low2) // Third: bearish, closes below first low + { + pattern.isThreeInsideDown = true; + pattern.patternType = CANDLE_THREE_INSIDE_DOWN; + pattern.isBearish = true; + pattern.isReversal = true; + pattern.candlesInPattern = 3; + pattern.patternStrength = 4; + pattern.patternName = "Three Inside Down"; + pattern.reliability = 0.75; + } + // Three Outside Up (bullish) + if(isBear2 && // First: bearish + isBull1 && close1 > open2 && open1 < close2 && // Second: engulfs first + isBull0 && close0 > close1) // Third: continues up + { + pattern.isThreeOutsideUp = true; + pattern.patternType = CANDLE_THREE_OUTSIDE_UP; + pattern.isBullish = true; + pattern.isReversal = true; + pattern.candlesInPattern = 3; + pattern.patternStrength = 4; + pattern.patternName = "Three Outside Up"; + pattern.reliability = 0.75; + } + // Three Outside Down (bearish) + if(isBull2 && // First: bullish + isBear1 && close1 < open2 && open1 > close2 && // Second: engulfs first + isBear0 && close0 < close1) // Third: continues down + { + pattern.isThreeOutsideDown = true; + pattern.patternType = CANDLE_THREE_OUTSIDE_DOWN; + pattern.isBearish = true; + pattern.isReversal = true; + pattern.candlesInPattern = 3; + pattern.patternStrength = 4; + pattern.patternName = "Three Outside Down"; + pattern.reliability = 0.75; + } + } + // ABANDONED BABY + if(AbandonedBaby_Enabled && g_cachedATR > 0) + { + double minGap = g_cachedATR * AbandonedBaby_MinGap; + // Abandoned Baby Bullish + if(isBear2 && body2 > g_cachedATR * 0.3 && // First: big bearish + body1 / range1 <= Doji_MaxBodyRatio && // Second: doji + high1 < low2 - minGap && // Gap down before doji + low1 + minGap < low0 && // Gap up after doji + isBull0 && body0 > g_cachedATR * 0.3) // Third: big bullish + { + pattern.isAbandonedBabyBull = true; + pattern.patternType = CANDLE_ABANDONED_BABY_BULL; + pattern.isBullish = true; + pattern.isReversal = true; + pattern.candlesInPattern = 3; + pattern.patternStrength = 5; + pattern.patternName = "Abandoned Baby Bullish"; + pattern.reliability = 0.90; + } + // Abandoned Baby Bearish + if(isBull2 && body2 > g_cachedATR * 0.3 && // First: big bullish + body1 / range1 <= Doji_MaxBodyRatio && // Second: doji + low1 > high2 + minGap && // Gap up before doji + high1 - minGap > high0 && // Gap down after doji + isBear0 && body0 > g_cachedATR * 0.3) // Third: big bearish + { + pattern.isAbandonedBabyBear = true; + pattern.patternType = CANDLE_ABANDONED_BABY_BEAR; + pattern.isBearish = true; + pattern.isReversal = true; + pattern.candlesInPattern = 3; + pattern.patternStrength = 5; + pattern.patternName = "Abandoned Baby Bearish"; + pattern.reliability = 0.90; + } + } + // Set metadata + pattern.patternTime = iTime(_Symbol, _Period, bar); + pattern.patternBar = bar; + // Store if pattern detected + if(pattern.patternStrength >= CandlePatterns_MinStrength) + { + g_lastExtendedCandlePattern = pattern; + } + return pattern; +} +//+------------------------------------------------------------------+ +//| Draw Candle Pattern Arrow | +//+------------------------------------------------------------------+ +void DrawCandlePatternArrow(const CandlePatternStructExtended &pattern) +{ + if(!CandlePatterns_ShowOnChart) return; + if(pattern.patternStrength < CandlePatterns_MinStrength) return; + string objName = "CANDLE_" + pattern.patternName + "_" + IntegerToString(pattern.patternBar); + double price; + ENUM_OBJECT arrowType; + color arrowColor; + if(pattern.isBullish) + { + price = iLow(_Symbol, _Period, pattern.patternBar) - g_cachedATR * 0.2; + arrowType = OBJ_ARROW_UP; + arrowColor = Candle_BullishColor; + } + else if(pattern.isBearish) + { + price = iHigh(_Symbol, _Period, pattern.patternBar) + g_cachedATR * 0.2; + arrowType = OBJ_ARROW_DOWN; + arrowColor = Candle_BearishColor; + } + else + { + price = iHigh(_Symbol, _Period, pattern.patternBar) + g_cachedATR * 0.2; + arrowType = OBJ_ARROW; + arrowColor = Candle_NeutralColor; + } + if(ObjectFind(0, objName) < 0) + { + ObjectCreate(0, objName, arrowType, 0, pattern.patternTime, price); + } + ObjectSetInteger(0, objName, OBJPROP_COLOR, arrowColor); + ObjectSetInteger(0, objName, OBJPROP_WIDTH, Candle_ArrowSize); + ObjectSetString(0, objName, OBJPROP_TOOLTIP, + StringFormat("%s [Str:%d Rel:%.0f%%]", + pattern.patternName, + pattern.patternStrength, + pattern.reliability * 100)); +} +//+------------------------------------------------------------------+ +//| Cleanup Candle Pattern Objects | +//+------------------------------------------------------------------+ +void CleanupCandlePatternObjects() +{ + ObjectsDeleteAll(0, "CANDLE_"); +} +//+------------------------------------------------------------------+ +//| END OF PART 3A - CANDLESTICK PATTERN FUNCTIONS | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| PATTERN MODULE - PART 3B | +//| CHART PATTERN DETECTION FUNCTIONS | +//+------------------------------------------------------------------+ +//+==================================================================+ +//| CHART PATTERN DETECTION | +//+==================================================================+ +//+------------------------------------------------------------------+ +//| Initialize Chart Patterns | +//+------------------------------------------------------------------+ +void InitializeChartPatterns() +{ + if(!ChartPatterns_Enabled) return; + // [v6.42] CandlePatterns_Alerts controls alert notifications for candle patterns + bool candleAlerts = CandlePatterns_Alerts; + ArrayResize(g_hsPatterns, 0); + ArrayResize(g_mtbPatterns, 0); + ArrayResize(g_trianglePatterns, 0); + ArrayResize(g_fpPatterns, 0); + ArrayResize(g_wedgePatterns, 0); + ArrayResize(g_diamondPatterns, 0); + ArrayResize(g_vPatterns, 0); + g_hsCount = 0; + g_mtbCount = 0; + g_triangleCount = 0; + g_fpCount = 0; + g_wedgeCount = 0; + g_diamondCount = 0; + g_vCount = 0; + ArrayResize(g_swingHighs, 0); + ArrayResize(g_swingLows, 0); + ArrayResize(g_swingHighTimes, 0); + ArrayResize(g_swingLowTimes, 0); + ArrayResize(g_swingHighBars, 0); + ArrayResize(g_swingLowBars, 0); + g_patternsInitialized = true; + if(g_verboseLog) + Print("[OK] Chart Patterns initialized"); +} +//+------------------------------------------------------------------+ +//| Find Swing Points for Pattern Detection | +//+------------------------------------------------------------------+ +void FindSwingPointsForPatterns(const datetime &time[], const double &high[], + const double &low[], int limit, int swingStrength = 5) +{ + ArrayResize(g_swingHighs, 0); + ArrayResize(g_swingLows, 0); + ArrayResize(g_swingHighTimes, 0); + ArrayResize(g_swingLowTimes, 0); + ArrayResize(g_swingHighBars, 0); + ArrayResize(g_swingLowBars, 0); + g_swingHighCount = 0; + g_swingLowCount = 0; + int lookback = MathMin(ChartPatterns_Lookback, limit - swingStrength); + for(int i = swingStrength; i < lookback; i++) + { + // Check for Swing High + bool isSwingHigh = true; + for(int j = 1; j <= swingStrength; j++) + { + if(high[i] <= high[i-j] || high[i] <= high[i+j]) + { + isSwingHigh = false; + break; + } + } + if(isSwingHigh) + { + ArrayResize(g_swingHighs, g_swingHighCount + 1); + ArrayResize(g_swingHighTimes, g_swingHighCount + 1); + ArrayResize(g_swingHighBars, g_swingHighCount + 1); + g_swingHighs[g_swingHighCount] = high[i]; + g_swingHighTimes[g_swingHighCount] = time[i]; + g_swingHighBars[g_swingHighCount] = i; + g_swingHighCount++; + } + // Check for Swing Low + bool isSwingLow = true; + for(int j = 1; j <= swingStrength; j++) + { + if(low[i] >= low[i-j] || low[i] >= low[i+j]) + { + isSwingLow = false; + break; + } + } + if(isSwingLow) + { + ArrayResize(g_swingLows, g_swingLowCount + 1); + ArrayResize(g_swingLowTimes, g_swingLowCount + 1); + ArrayResize(g_swingLowBars, g_swingLowCount + 1); + g_swingLows[g_swingLowCount] = low[i]; + g_swingLowTimes[g_swingLowCount] = time[i]; + g_swingLowBars[g_swingLowCount] = i; + g_swingLowCount++; + } + } +} +//+------------------------------------------------------------------+ +//| Detect Head & Shoulders Pattern | +//+------------------------------------------------------------------+ +void DetectHeadAndShoulders(const datetime &time[], const double &high[], + const double &low[], const double &close[], int limit) +{ + if(!HS_Enabled || g_swingHighCount < 3 || g_swingLowCount < 2) return; + // Look for Head & Shoulders Top + for(int h = 0; h < g_swingHighCount - 2; h++) + { + double leftShoulder = g_swingHighs[h]; + int leftShoulderBar = g_swingHighBars[h]; + for(int hd = h + 1; hd < g_swingHighCount - 1; hd++) + { + double head = g_swingHighs[hd]; + int headBar = g_swingHighBars[hd]; + // Head must be higher than left shoulder + if(head <= leftShoulder) continue; + if(headBar - leftShoulderBar < ChartPatterns_MinBars / 3) continue; + for(int rs = hd + 1; rs < g_swingHighCount; rs++) + { + double rightShoulder = g_swingHighs[rs]; + int rightShoulderBar = g_swingHighBars[rs]; + // Right shoulder must be lower than head + if(rightShoulder >= head) continue; + if(rightShoulderBar - headBar < ChartPatterns_MinBars / 3) continue; + // Check shoulder symmetry + double shoulderDiff = MathAbs(leftShoulder - rightShoulder); + // [v6.42] HS_MaxShoulderDiff check + if(g_cachedATR > 0 && shoulderDiff > g_cachedATR * HS_MaxShoulderDiff) continue; + if(rightShoulderBar - leftShoulderBar > ChartPatterns_MaxBars) continue; + double avgShoulder = (leftShoulder + rightShoulder) / 2; + double symmetry = 1.0 - (shoulderDiff / avgShoulder); + if(symmetry < HS_MinShoulderSymmetry) continue; + // Check minimum head height + double headHeight = head - avgShoulder; + // [v6.42] Use ChartPatterns_MinHeight as minimum overall height + if(g_cachedATR > 0 && headHeight < g_cachedATR * MathMax(HS_MinHeadHeight, ChartPatterns_MinHeight)) continue; + // Find neckline points (lows between shoulders) + double neckLeft = DBL_MAX, neckRight = DBL_MAX; + datetime neckLeftTime = 0, neckRightTime = 0; + for(int nl = 0; nl < g_swingLowCount; nl++) + { + int nlBar = g_swingLowBars[nl]; + if(nlBar > leftShoulderBar && nlBar < headBar) + { + if(g_swingLows[nl] < neckLeft) + { + neckLeft = g_swingLows[nl]; + neckLeftTime = g_swingLowTimes[nl]; + } + } + if(nlBar > headBar && nlBar < rightShoulderBar) + { + if(g_swingLows[nl] < neckRight) + { + neckRight = g_swingLows[nl]; + neckRightTime = g_swingLowTimes[nl]; + } + } + } + if(neckLeft == DBL_MAX || neckRight == DBL_MAX) continue; + // Create pattern + HeadShouldersPattern hs; + ZeroMemory(hs); + hs.id = g_hsCount; + hs.objName = "HS_TOP_" + IntegerToString(g_hsCount); + hs.type = CHART_PATTERN_HEAD_SHOULDERS_TOP; + hs.status = PATTERN_CONFIRMED; + hs.leftShoulder = leftShoulder; + hs.head = head; + hs.rightShoulder = rightShoulder; + hs.necklineLeft = neckLeft; + hs.necklineRight = neckRight; + hs.leftShoulderTime = g_swingHighTimes[h]; + hs.headTime = g_swingHighTimes[hd]; + hs.rightShoulderTime = g_swingHighTimes[rs]; + hs.necklineLeftTime = neckLeftTime; + hs.necklineRightTime = neckRightTime; + // [v6.42] HS_RequireNecklineBreak + // [v6.42] DTB_RequireNeckBreak also applies to double top/bottom + if(HS_RequireNecklineBreak || DTB_RequireNeckBreak) + { + double neckline = (neckLeft + neckRight) / 2.0; + if(close[0] > neckline) continue; // Not broken yet for top pattern + } + // [v6.42] HS_AllowSlopedNeckline + if(!HS_AllowSlopedNeckline) + { + double neckSlope = MathAbs(neckRight - neckLeft); + if(neckSlope > g_cachedATR * 0.5) continue; // Too sloped + } + hs.leftShoulderBar = leftShoulderBar; + hs.headBar = headBar; + hs.rightShoulderBar = rightShoulderBar; + // Calculate neckline slope + int neckBars = (int)((neckRightTime - neckLeftTime) / PeriodSeconds()); + hs.necklineSlope = (neckBars > 0) ? (neckRight - neckLeft) / neckBars : 0; + // Calculate pattern height and trade levels + hs.patternHeight = head - (neckLeft + neckRight) / 2; + // Entry at neckline + double currentNeckline = neckRight + hs.necklineSlope * (rightShoulderBar - g_swingLowBars[g_swingLowCount-1]); + hs.entryPrice = currentNeckline; + hs.stopLoss = rightShoulder + g_cachedATR * 0.3; + hs.takeProfit1 = hs.entryPrice - hs.patternHeight; + hs.takeProfit2 = hs.entryPrice - hs.patternHeight * 1.618; + double sl = hs.stopLoss - hs.entryPrice; + double tp = hs.entryPrice - hs.takeProfit1; + hs.riskReward = (sl > 0) ? tp / sl : 0; + // Check if neckline broken + hs.necklineBroken = (close[0] < currentNeckline); + if(hs.necklineBroken) + { + hs.breakPrice = close[0]; + hs.breakTime = time[0]; + hs.status = PATTERN_TRIGGERED; + } + // Calculate quality + hs.symmetryScore = symmetry; + int qualityScore = 0; + if(symmetry >= 0.9) qualityScore += 30; + else if(symmetry >= 0.8) qualityScore += 20; + else if(symmetry >= 0.7) qualityScore += 10; + if(hs.patternHeight >= g_cachedATR * 2) qualityScore += 30; + else if(hs.patternHeight >= g_cachedATR * 1.5) qualityScore += 20; + if(hs.necklineBroken) qualityScore += 20; + if(MathAbs(hs.necklineSlope) < 0.1) qualityScore += 10; // Flat neckline is better + hs.score = qualityScore; + if(qualityScore >= 70) hs.quality = PATTERN_QUALITY_PREMIUM; + else if(qualityScore >= 50) hs.quality = PATTERN_QUALITY_HIGH; + else if(qualityScore >= 30) hs.quality = PATTERN_QUALITY_MEDIUM; + else hs.quality = PATTERN_QUALITY_LOW; + hs.isValid = true; + hs.active = true; + hs.createdTime = TimeCurrent(); + hs.expiryTime = TimeCurrent() + ChartPatterns_ExpiryBars * PeriodSeconds(); + // * v9.13 FIX#1b v3: STALE PATTERN FILTER + // ROOT CAUSE: InitializeChartPatterns() clears g_hsPatterns[] to 0 every bar, + // then DetectHeadAndShoulders() re-detects the SAME swings and re-adds them + // with fresh expiryTime. Dedup was useless (checked empty array, always 0 matches). + // Result: H&S Top from Jan 12 persisted 29+ days, blocking 1548 BUY entries. + // FIX: If the head swing point is older than ExpiryBars, the pattern is STALE. + // Don't add it -- it already played out or failed. + // * v9.58 FIX#224: TF-aware expiry. H1=24 bars (24h). H4=40 bars (240h=10d). + // H1 default 40 bars = 40h was too long: H&S Top from bar-0 blocked BUYs for 40h. + { + int _tfExpBars = (_Period == PERIOD_H1) ? 24 : ChartPatterns_ExpiryBars; + int expirySeconds = _tfExpBars * PeriodSeconds(); + if(TimeCurrent() - hs.headTime > expirySeconds) + { + // * FIX#125 (Mar 06 2026): Old code printed 1 line per swing COMBINATION + // that is stale -- e.g. 20 swing highs × 10 combos = 200 log lines/bar. + // Fix: Track already-logged headTimes per bar; print max once per headTime. + static datetime s_staleTopLogged[]; + static int s_staleTopCount = 0; + bool alreadyLogged125 = false; + for(int _si = 0; _si < s_staleTopCount; _si++) + if(s_staleTopLogged[_si] == hs.headTime) { alreadyLogged125 = true; break; } + if(!alreadyLogged125 && (EnableDebugMode || AutoOpt_ShowLog)) + { + Print("* v9.14 FIX#34: H&S Top STALE -- headTime=", TimeToString(hs.headTime), + " age=", (int)(TimeCurrent() - hs.headTime) / 3600, "h > expiry=", + (int)expirySeconds / 3600, "h -> SKIPPED [FIX#125: first occurrence only]"); + ArrayResize(s_staleTopLogged, s_staleTopCount + 1); + s_staleTopLogged[s_staleTopCount++] = hs.headTime; + if(s_staleTopCount > 50) s_staleTopCount = 0; // ring buffer reset + } + continue; // Don't add stale pattern + } + } + // Add to array + ArrayResize(g_hsPatterns, g_hsCount + 1); + g_hsPatterns[g_hsCount] = hs; + g_hsCount++; + g_hasHeadShoulders = true; + g_currentHS = hs; + // Draw pattern + DrawHeadAndShoulders(hs); + // Alert + if(ChartPatterns_Alerts && hs.necklineBroken) + { + Alert(_Symbol, " ", EnumToString(_Period), + ": Head & Shoulders Top detected! Score: ", hs.score); + } + return; // Found pattern, exit + } + } + } + // Look for Inverse Head & Shoulders (same logic inverted) + for(int l = 0; l < g_swingLowCount - 2; l++) + { + double leftShoulder = g_swingLows[l]; + int leftShoulderBar = g_swingLowBars[l]; + for(int hd = l + 1; hd < g_swingLowCount - 1; hd++) + { + double head = g_swingLows[hd]; + int headBar = g_swingLowBars[hd]; + if(head >= leftShoulder) continue; + if(headBar - leftShoulderBar < ChartPatterns_MinBars / 3) continue; + for(int rs = hd + 1; rs < g_swingLowCount; rs++) + { + double rightShoulder = g_swingLows[rs]; + int rightShoulderBar = g_swingLowBars[rs]; + if(rightShoulder <= head) continue; + if(rightShoulderBar - headBar < ChartPatterns_MinBars / 3) continue; + if(rightShoulderBar - leftShoulderBar > ChartPatterns_MaxBars) continue; + double shoulderDiff = MathAbs(leftShoulder - rightShoulder); + double avgShoulder = (leftShoulder + rightShoulder) / 2; + double symmetry = 1.0 - (shoulderDiff / avgShoulder); + if(symmetry < HS_MinShoulderSymmetry) continue; + double headHeight = avgShoulder - head; + if(g_cachedATR > 0 && headHeight < g_cachedATR * HS_MinHeadHeight) continue; + // Find neckline points (highs between shoulders) + double neckLeft = 0, neckRight = 0; + datetime neckLeftTime = 0, neckRightTime = 0; + for(int nl = 0; nl < g_swingHighCount; nl++) + { + int nlBar = g_swingHighBars[nl]; + if(nlBar > leftShoulderBar && nlBar < headBar) + { + if(g_swingHighs[nl] > neckLeft) + { + neckLeft = g_swingHighs[nl]; + neckLeftTime = g_swingHighTimes[nl]; + } + } + if(nlBar > headBar && nlBar < rightShoulderBar) + { + if(g_swingHighs[nl] > neckRight) + { + neckRight = g_swingHighs[nl]; + neckRightTime = g_swingHighTimes[nl]; + } + } + } + if(neckLeft == 0 || neckRight == 0) continue; + HeadShouldersPattern hs; + ZeroMemory(hs); + hs.id = g_hsCount; + hs.objName = "HS_BOTTOM_" + IntegerToString(g_hsCount); + hs.type = CHART_PATTERN_HEAD_SHOULDERS_BOTTOM; + hs.status = PATTERN_CONFIRMED; + hs.leftShoulder = leftShoulder; + hs.head = head; + hs.rightShoulder = rightShoulder; + hs.necklineLeft = neckLeft; + hs.necklineRight = neckRight; + hs.leftShoulderTime = g_swingLowTimes[l]; + hs.headTime = g_swingLowTimes[hd]; + hs.rightShoulderTime = g_swingLowTimes[rs]; + hs.necklineLeftTime = neckLeftTime; + hs.necklineRightTime = neckRightTime; + hs.leftShoulderBar = leftShoulderBar; + hs.headBar = headBar; + hs.rightShoulderBar = rightShoulderBar; + int neckBars = (int)((neckRightTime - neckLeftTime) / PeriodSeconds()); + hs.necklineSlope = (neckBars > 0) ? (neckRight - neckLeft) / neckBars : 0; + hs.patternHeight = (neckLeft + neckRight) / 2 - head; + double currentNeckline = neckRight + hs.necklineSlope * (rightShoulderBar - g_swingHighBars[g_swingHighCount-1]); + hs.entryPrice = currentNeckline; + hs.stopLoss = rightShoulder - g_cachedATR * 0.3; + hs.takeProfit1 = hs.entryPrice + hs.patternHeight; + hs.takeProfit2 = hs.entryPrice + hs.patternHeight * 1.618; + double sl = hs.entryPrice - hs.stopLoss; + double tp = hs.takeProfit1 - hs.entryPrice; + hs.riskReward = (sl > 0) ? tp / sl : 0; + hs.necklineBroken = (close[0] > currentNeckline); + if(hs.necklineBroken) + { + hs.breakPrice = close[0]; + hs.breakTime = time[0]; + hs.status = PATTERN_TRIGGERED; + } + hs.symmetryScore = symmetry; + int qualityScore = 0; + if(symmetry >= 0.9) qualityScore += 30; + else if(symmetry >= 0.8) qualityScore += 20; + else if(symmetry >= 0.7) qualityScore += 10; + if(hs.patternHeight >= g_cachedATR * 2) qualityScore += 30; + else if(hs.patternHeight >= g_cachedATR * 1.5) qualityScore += 20; + if(hs.necklineBroken) qualityScore += 20; + if(MathAbs(hs.necklineSlope) < 0.1) qualityScore += 10; + hs.score = qualityScore; + if(qualityScore >= 70) hs.quality = PATTERN_QUALITY_PREMIUM; + else if(qualityScore >= 50) hs.quality = PATTERN_QUALITY_HIGH; + else if(qualityScore >= 30) hs.quality = PATTERN_QUALITY_MEDIUM; + else hs.quality = PATTERN_QUALITY_LOW; + hs.isValid = true; + hs.active = true; + hs.createdTime = TimeCurrent(); + // * v9.58 FIX#224: TF-aware expiryTime (H1=24 bars, others=ChartPatterns_ExpiryBars) + { + int _tfExpBars224b = (_Period == PERIOD_H1) ? 24 : ChartPatterns_ExpiryBars; + hs.expiryTime = TimeCurrent() + _tfExpBars224b * PeriodSeconds(); + } + // * v9.13 FIX#1b v3: STALE PATTERN FILTER for Inverse H&S (same logic as Top) + { + // * v9.58 FIX#224: TF-aware expiry (H1=24 bars = 24h, was 40h) + int _tfExpBars = (_Period == PERIOD_H1) ? 24 : ChartPatterns_ExpiryBars; + int expirySeconds = _tfExpBars * PeriodSeconds(); + if(TimeCurrent() - hs.headTime > expirySeconds) + { + // * FIX#125b (Mar 06 2026): Same dedup fix as H&S Top + static datetime s_staleInvLogged[]; + static int s_staleInvCount = 0; + bool alreadyLogged125b = false; + for(int _si = 0; _si < s_staleInvCount; _si++) + if(s_staleInvLogged[_si] == hs.headTime) { alreadyLogged125b = true; break; } + if(!alreadyLogged125b && (EnableDebugMode || AutoOpt_ShowLog)) + { + Print("* v9.14 FIX#34: Inv H&S STALE -- headTime=", TimeToString(hs.headTime), + " age=", (int)(TimeCurrent() - hs.headTime) / 3600, "h -> SKIPPED [FIX#125b: first occurrence only]"); + ArrayResize(s_staleInvLogged, s_staleInvCount + 1); + s_staleInvLogged[s_staleInvCount++] = hs.headTime; + if(s_staleInvCount > 50) s_staleInvCount = 0; // ring buffer reset + } + continue; + } + } + ArrayResize(g_hsPatterns, g_hsCount + 1); + g_hsPatterns[g_hsCount] = hs; + g_hsCount++; + g_hasHeadShoulders = true; + g_currentHS = hs; + DrawHeadAndShoulders(hs); + if(ChartPatterns_Alerts && hs.necklineBroken) + { + Alert(_Symbol, " ", EnumToString(_Period), + ": Inverse Head & Shoulders detected! Score: ", hs.score); + } + return; + } + } + } +} +//+------------------------------------------------------------------+ +//| Draw Head & Shoulders Pattern | +//+------------------------------------------------------------------+ +void DrawHeadAndShoulders(const HeadShouldersPattern &hs) +{ + if(!ChartPatterns_ShowOnChart) return; + color patternColor = (hs.type == CHART_PATTERN_HEAD_SHOULDERS_TOP) ? HS_BearColor : HS_BullColor; + // Draw Left Shoulder + string lsName = hs.objName + "_LS"; + ObjectCreate(0, lsName, OBJ_ARROW_DOWN, 0, hs.leftShoulderTime, hs.leftShoulder); + ObjectSetInteger(0, lsName, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, lsName, OBJPROP_WIDTH, 2); + ObjectSetString(0, lsName, OBJPROP_TOOLTIP, "Left Shoulder"); + // Draw Head + string hdName = hs.objName + "_HD"; + ObjectCreate(0, hdName, OBJ_ARROW_DOWN, 0, hs.headTime, hs.head); + ObjectSetInteger(0, hdName, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, hdName, OBJPROP_WIDTH, 3); + ObjectSetString(0, hdName, OBJPROP_TOOLTIP, "Head"); + // Draw Right Shoulder + string rsName = hs.objName + "_RS"; + ObjectCreate(0, rsName, OBJ_ARROW_DOWN, 0, hs.rightShoulderTime, hs.rightShoulder); + ObjectSetInteger(0, rsName, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, rsName, OBJPROP_WIDTH, 2); + ObjectSetString(0, rsName, OBJPROP_TOOLTIP, "Right Shoulder"); + // Draw Neckline + string neckName = hs.objName + "_NECK"; + ObjectCreate(0, neckName, OBJ_TREND, 0, hs.necklineLeftTime, hs.necklineLeft, + hs.necklineRightTime, hs.necklineRight); + ObjectSetInteger(0, neckName, OBJPROP_COLOR, clrYellow); + ObjectSetInteger(0, neckName, OBJPROP_STYLE, STYLE_DASH); + ObjectSetInteger(0, neckName, OBJPROP_WIDTH, 2); + ObjectSetInteger(0, neckName, OBJPROP_RAY_RIGHT, true); + ObjectSetString(0, neckName, OBJPROP_TOOLTIP, "Neckline"); + // Draw Label + string labelName = hs.objName + "_LABEL"; + string labelText = (hs.type == CHART_PATTERN_HEAD_SHOULDERS_TOP) ? + StringFormat("H&S Top [%d]", hs.score) : + StringFormat("Inv H&S [%d]", hs.score); + ObjectCreate(0, labelName, OBJ_TEXT, 0, hs.headTime, + hs.head + ((hs.type == CHART_PATTERN_HEAD_SHOULDERS_TOP) ? g_cachedATR * 0.3 : -g_cachedATR * 0.3)); + ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); + ObjectSetInteger(0, labelName, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 10); +} +//+------------------------------------------------------------------+ +//| Detect Double/Triple Top-Bottom | +//+------------------------------------------------------------------+ +void DetectDoubleTripleTopBottom(const datetime &time[], const double &high[], + const double &low[], const double &close[], int limit) +{ + if(!DTB_Enabled) return; + if(g_cachedATR <= 0) return; + double tolerance = g_cachedATR * DTB_PeakTolerance; + // Detect Double Top + for(int i = 0; i < g_swingHighCount - 1; i++) + { + double peak1 = g_swingHighs[i]; + int peak1Bar = g_swingHighBars[i]; + for(int j = i + 1; j < g_swingHighCount; j++) + { + double peak2 = g_swingHighs[j]; + int peak2Bar = g_swingHighBars[j]; + int distance = peak2Bar - peak1Bar; + if(distance < DTB_MinPeakDistance || distance > DTB_MaxPeakDistance) continue; + // Peaks should be at similar level + if(MathAbs(peak1 - peak2) > tolerance) continue; + // Find neckline (low between peaks) + double neckline = DBL_MAX; + datetime neckTime = 0; + for(int k = 0; k < g_swingLowCount; k++) + { + if(g_swingLowBars[k] > peak1Bar && g_swingLowBars[k] < peak2Bar) + { + if(g_swingLows[k] < neckline) + { + neckline = g_swingLows[k]; + neckTime = g_swingLowTimes[k]; + } + } + } + if(neckline == DBL_MAX) continue; + MultipleTopBottomPattern mtb; + ZeroMemory(mtb); + mtb.id = g_mtbCount; + mtb.objName = "DTB_TOP_" + IntegerToString(g_mtbCount); + mtb.type = CHART_PATTERN_DOUBLE_TOP; + mtb.status = PATTERN_CONFIRMED; + mtb.peak1 = peak1; + mtb.peak2 = peak2; + mtb.peak1Time = g_swingHighTimes[i]; + mtb.peak2Time = g_swingHighTimes[j]; + mtb.peak1Bar = peak1Bar; + mtb.peak2Bar = peak2Bar; + mtb.neckline = neckline; + mtb.necklineTime = neckTime; + double avgPeak = (peak1 + peak2) / 2; + mtb.patternHeight = avgPeak - neckline; + mtb.entryPrice = neckline; + mtb.stopLoss = avgPeak + g_cachedATR * 0.3; + mtb.takeProfit1 = neckline - mtb.patternHeight; + mtb.takeProfit2 = neckline - mtb.patternHeight * 1.618; + mtb.necklineBroken = (close[0] < neckline); + if(mtb.necklineBroken) + { + mtb.breakTime = time[0]; + mtb.breakPrice = close[0]; + mtb.status = PATTERN_TRIGGERED; + } + mtb.peakTolerance = (g_cachedATR > 0) ? MathAbs(peak1 - peak2) / g_cachedATR : 0; + int qualityScore = 0; + if(mtb.peakTolerance < 0.1) qualityScore += 30; + else if(mtb.peakTolerance < 0.2) qualityScore += 20; + if(mtb.patternHeight >= g_cachedATR * 1.5) qualityScore += 30; + if(mtb.necklineBroken) qualityScore += 20; + mtb.score = qualityScore; + if(qualityScore >= 60) mtb.quality = PATTERN_QUALITY_PREMIUM; + else if(qualityScore >= 40) mtb.quality = PATTERN_QUALITY_HIGH; + else if(qualityScore >= 25) mtb.quality = PATTERN_QUALITY_MEDIUM; + else mtb.quality = PATTERN_QUALITY_LOW; + mtb.isValid = true; + mtb.active = true; + mtb.createdTime = TimeCurrent(); + ArrayResize(g_mtbPatterns, g_mtbCount + 1); + g_mtbPatterns[g_mtbCount] = mtb; + g_mtbCount++; + g_hasMultipleTopBottom = true; + g_currentMTB = mtb; + DrawDoubleTriple(mtb); + if(ChartPatterns_Alerts && mtb.necklineBroken) + { + Alert(_Symbol, " ", EnumToString(_Period), + ": Double Top detected! Score: ", mtb.score); + } + } + } + // Detect Double Bottom (similar logic) + for(int i = 0; i < g_swingLowCount - 1; i++) + { + double peak1 = g_swingLows[i]; + int peak1Bar = g_swingLowBars[i]; + for(int j = i + 1; j < g_swingLowCount; j++) + { + double peak2 = g_swingLows[j]; + int peak2Bar = g_swingLowBars[j]; + int distance = peak2Bar - peak1Bar; + if(distance < DTB_MinPeakDistance || distance > DTB_MaxPeakDistance) continue; + if(MathAbs(peak1 - peak2) > tolerance) continue; + double neckline = 0; + datetime neckTime = 0; + for(int k = 0; k < g_swingHighCount; k++) + { + if(g_swingHighBars[k] > peak1Bar && g_swingHighBars[k] < peak2Bar) + { + if(g_swingHighs[k] > neckline) + { + neckline = g_swingHighs[k]; + neckTime = g_swingHighTimes[k]; + } + } + } + if(neckline == 0) continue; + MultipleTopBottomPattern mtb; + ZeroMemory(mtb); + mtb.id = g_mtbCount; + mtb.objName = "DTB_BOT_" + IntegerToString(g_mtbCount); + mtb.type = CHART_PATTERN_DOUBLE_BOTTOM; + mtb.status = PATTERN_CONFIRMED; + mtb.peak1 = peak1; + mtb.peak2 = peak2; + mtb.peak1Time = g_swingLowTimes[i]; + mtb.peak2Time = g_swingLowTimes[j]; + mtb.peak1Bar = peak1Bar; + mtb.peak2Bar = peak2Bar; + mtb.neckline = neckline; + mtb.necklineTime = neckTime; + double avgPeak = (peak1 + peak2) / 2; + mtb.patternHeight = neckline - avgPeak; + mtb.entryPrice = neckline; + mtb.stopLoss = avgPeak - g_cachedATR * 0.3; + mtb.takeProfit1 = neckline + mtb.patternHeight; + mtb.takeProfit2 = neckline + mtb.patternHeight * 1.618; + mtb.necklineBroken = (close[0] > neckline); + if(mtb.necklineBroken) + { + mtb.breakTime = time[0]; + mtb.breakPrice = close[0]; + mtb.status = PATTERN_TRIGGERED; + } + mtb.peakTolerance = (g_cachedATR > 0) ? MathAbs(peak1 - peak2) / g_cachedATR : 0; + int qualityScore = 0; + if(mtb.peakTolerance < 0.1) qualityScore += 30; + else if(mtb.peakTolerance < 0.2) qualityScore += 20; + if(mtb.patternHeight >= g_cachedATR * 1.5) qualityScore += 30; + if(mtb.necklineBroken) qualityScore += 20; + mtb.score = qualityScore; + if(qualityScore >= 60) mtb.quality = PATTERN_QUALITY_PREMIUM; + else if(qualityScore >= 40) mtb.quality = PATTERN_QUALITY_HIGH; + else if(qualityScore >= 25) mtb.quality = PATTERN_QUALITY_MEDIUM; + else mtb.quality = PATTERN_QUALITY_LOW; + mtb.isValid = true; + mtb.active = true; + mtb.createdTime = TimeCurrent(); + ArrayResize(g_mtbPatterns, g_mtbCount + 1); + g_mtbPatterns[g_mtbCount] = mtb; + g_mtbCount++; + g_hasMultipleTopBottom = true; + g_currentMTB = mtb; + DrawDoubleTriple(mtb); + if(ChartPatterns_Alerts && mtb.necklineBroken) + { + Alert(_Symbol, " ", EnumToString(_Period), + ": Double Bottom detected! Score: ", mtb.score); + } + } + } +} +//+------------------------------------------------------------------+ +//| Draw Double/Triple Pattern | +//+------------------------------------------------------------------+ +void DrawDoubleTriple(const MultipleTopBottomPattern &mtb) +{ + if(!ChartPatterns_ShowOnChart) return; + bool isTop = (mtb.type == CHART_PATTERN_DOUBLE_TOP || mtb.type == CHART_PATTERN_TRIPLE_TOP); + color patternColor = isTop ? DTB_TopColor : DTB_BottomColor; + // Draw peaks + string peak1Name = mtb.objName + "_P1"; + ENUM_OBJECT arrowType = isTop ? OBJ_ARROW_DOWN : OBJ_ARROW_UP; + ObjectCreate(0, peak1Name, arrowType, 0, mtb.peak1Time, mtb.peak1); + ObjectSetInteger(0, peak1Name, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, peak1Name, OBJPROP_WIDTH, 2); + string peak2Name = mtb.objName + "_P2"; + ObjectCreate(0, peak2Name, arrowType, 0, mtb.peak2Time, mtb.peak2); + ObjectSetInteger(0, peak2Name, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, peak2Name, OBJPROP_WIDTH, 2); + // Draw neckline + string neckName = mtb.objName + "_NECK"; + ObjectCreate(0, neckName, OBJ_HLINE, 0, 0, mtb.neckline); + ObjectSetInteger(0, neckName, OBJPROP_COLOR, clrYellow); + ObjectSetInteger(0, neckName, OBJPROP_STYLE, STYLE_DASH); + // Draw label + string labelName = mtb.objName + "_LABEL"; + string labelText = ""; + switch(mtb.type) + { + case CHART_PATTERN_DOUBLE_TOP: labelText = "Double Top"; break; + case CHART_PATTERN_DOUBLE_BOTTOM: labelText = "Double Bottom"; break; + case CHART_PATTERN_TRIPLE_TOP: labelText = "Triple Top"; break; + case CHART_PATTERN_TRIPLE_BOTTOM: labelText = "Triple Bottom"; break; + } + labelText = StringFormat("%s [%d]", labelText, mtb.score); + double labelPrice = isTop ? mtb.peak1 + g_cachedATR * 0.3 : mtb.peak1 - g_cachedATR * 0.3; + ObjectCreate(0, labelName, OBJ_TEXT, 0, mtb.peak1Time, labelPrice); + ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); + ObjectSetInteger(0, labelName, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 10); +} +//+------------------------------------------------------------------+ +//| Detect Triangle Patterns | +//+------------------------------------------------------------------+ +void DetectTriangles(const datetime &time[], const double &high[], + const double &low[], const double &close[], int limit) +{ + if(!Triangle_Enabled) return; + if(g_swingHighCount < Triangle_MinTouches || g_swingLowCount < Triangle_MinTouches) return; + // Calculate trendline slopes using linear regression + double upperSlope = 0, upperIntercept = 0; + double lowerSlope = 0, lowerIntercept = 0; + // Fit upper trendline to swing highs + if(g_swingHighCount >= 2) + { + double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0; + int n = MathMin(g_swingHighCount, 5); + for(int i = 0; i < n; i++) + { + double x = (double)g_swingHighBars[i]; + double y = g_swingHighs[i]; + sumX += x; + sumY += y; + sumXY += x * y; + sumX2 += x * x; + } + double denom = n * sumX2 - sumX * sumX; + if(MathAbs(denom) > 0.0001) + { + upperSlope = (n * sumXY - sumX * sumY) / denom; + upperIntercept = (sumY - upperSlope * sumX) / n; + } + } + // Fit lower trendline to swing lows + if(g_swingLowCount >= 2) + { + double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0; + int n = MathMin(g_swingLowCount, 5); + for(int i = 0; i < n; i++) + { + double x = (double)g_swingLowBars[i]; + double y = g_swingLows[i]; + sumX += x; + sumY += y; + sumXY += x * y; + sumX2 += x * x; + } + double denom = n * sumX2 - sumX * sumX; + if(MathAbs(denom) > 0.0001) + { + lowerSlope = (n * sumXY - sumX * sumY) / denom; + lowerIntercept = (sumY - lowerSlope * sumX) / n; + } + } + // Determine triangle type + ENUM_CHART_PATTERN_TYPE triangleType = CHART_PATTERN_NONE; + double slopeThreshold = 0.0001; + // Ascending Triangle: flat top, rising bottom + if(MathAbs(upperSlope) < slopeThreshold && lowerSlope > slopeThreshold) + { + triangleType = CHART_PATTERN_ASCENDING_TRIANGLE; + } + // Descending Triangle: falling top, flat bottom + else if(upperSlope < -slopeThreshold && MathAbs(lowerSlope) < slopeThreshold) + { + triangleType = CHART_PATTERN_DESCENDING_TRIANGLE; + } + // Symmetrical Triangle: converging lines + else if(upperSlope < -slopeThreshold && lowerSlope > slopeThreshold) + { + triangleType = CHART_PATTERN_SYMMETRICAL_TRIANGLE; + } + if(triangleType == CHART_PATTERN_NONE) return; + // Calculate apex (intersection point) + double apexBar = 0; + double apexPrice = 0; + if(MathAbs(upperSlope - lowerSlope) > 0.00001) + { + apexBar = (lowerIntercept - upperIntercept) / (upperSlope - lowerSlope); + apexPrice = upperSlope * apexBar + upperIntercept; + } + // Apex should be in the future but not too far + if(apexBar < 0 || apexBar > Triangle_MaxApexDistance) return; + // [v6.42] Triangle_MinConvergence - require minimum convergence rate + double convergenceRate = (g_cachedATR > 0 && apexBar > 0) ? + MathAbs(upperSlope - lowerSlope) / g_cachedATR : 0; + if(Triangle_MinConvergence > 0 && convergenceRate < Triangle_MinConvergence) return; + // Create pattern + TrianglePattern tri; + ZeroMemory(tri); + tri.id = g_triangleCount; + tri.objName = "TRI_" + IntegerToString(g_triangleCount); + tri.type = triangleType; + tri.status = PATTERN_CONFIRMED; + tri.upperSlope = upperSlope; + tri.lowerSlope = lowerSlope; + tri.upperStart = g_swingHighs[g_swingHighCount-1]; + tri.upperEnd = upperSlope * 0 + upperIntercept; + tri.lowerStart = g_swingLows[g_swingLowCount-1]; + tri.lowerEnd = lowerSlope * 0 + lowerIntercept; + tri.upperStartTime = g_swingHighTimes[g_swingHighCount-1]; + tri.lowerStartTime = g_swingLowTimes[g_swingLowCount-1]; + tri.upperEndTime = time[0]; + tri.lowerEndTime = time[0]; + tri.apexPrice = apexPrice; + tri.apexTime = time[0] + (int)apexBar * PeriodSeconds(); + tri.touchesUpper = g_swingHighCount; + tri.touchesLower = g_swingLowCount; + tri.patternHeight = tri.upperStart - tri.lowerStart; + tri.patternBars = g_swingHighBars[g_swingHighCount-1] - g_swingLowBars[g_swingLowCount-1]; + // Trade levels based on triangle type + double currentUpper = upperSlope * 0 + upperIntercept; + double currentLower = lowerSlope * 0 + lowerIntercept; + if(triangleType == CHART_PATTERN_ASCENDING_TRIANGLE) + { + tri.entryPrice = currentUpper; // Break above flat resistance + tri.stopLoss = currentLower - g_cachedATR * 0.3; + tri.takeProfit1 = tri.entryPrice + tri.patternHeight; + tri.takeProfit2 = tri.entryPrice + tri.patternHeight * 1.618; + tri.brokenUp = (close[0] > currentUpper); + } + else if(triangleType == CHART_PATTERN_DESCENDING_TRIANGLE) + { + tri.entryPrice = currentLower; // Break below flat support + tri.stopLoss = currentUpper + g_cachedATR * 0.3; + tri.takeProfit1 = tri.entryPrice - tri.patternHeight; + tri.takeProfit2 = tri.entryPrice - tri.patternHeight * 1.618; + tri.brokenDown = (close[0] < currentLower); + } + else // Symmetrical + { + tri.brokenUp = (close[0] > currentUpper); + tri.brokenDown = (close[0] < currentLower); + if(tri.brokenUp) + { + tri.entryPrice = currentUpper; + tri.stopLoss = currentLower - g_cachedATR * 0.3; + tri.takeProfit1 = tri.entryPrice + tri.patternHeight; + } + else if(tri.brokenDown) + { + tri.entryPrice = currentLower; + tri.stopLoss = currentUpper + g_cachedATR * 0.3; + tri.takeProfit1 = tri.entryPrice - tri.patternHeight; + } + } + if(tri.brokenUp || tri.brokenDown) + { + tri.breakTime = time[0]; + tri.breakPrice = close[0]; + tri.status = PATTERN_TRIGGERED; + } + // Quality scoring + int qualityScore = 0; + if(tri.touchesUpper >= 3) qualityScore += 20; + if(tri.touchesLower >= 3) qualityScore += 20; + if(tri.patternBars >= 30) qualityScore += 20; + if(tri.brokenUp || tri.brokenDown) qualityScore += 20; + tri.score = qualityScore; + if(qualityScore >= 70) tri.quality = PATTERN_QUALITY_PREMIUM; + else if(qualityScore >= 50) tri.quality = PATTERN_QUALITY_HIGH; + else if(qualityScore >= 30) tri.quality = PATTERN_QUALITY_MEDIUM; + else tri.quality = PATTERN_QUALITY_LOW; + tri.isValid = true; + tri.active = true; + tri.createdTime = TimeCurrent(); + ArrayResize(g_trianglePatterns, g_triangleCount + 1); + g_trianglePatterns[g_triangleCount] = tri; + g_triangleCount++; + g_hasTriangle = true; + g_currentTriangle = tri; + DrawTriangle(tri); + if(ChartPatterns_Alerts && (tri.brokenUp || tri.brokenDown)) + { + string triName = ""; + switch(triangleType) + { + case CHART_PATTERN_ASCENDING_TRIANGLE: triName = "Ascending Triangle"; break; + case CHART_PATTERN_DESCENDING_TRIANGLE: triName = "Descending Triangle"; break; + case CHART_PATTERN_SYMMETRICAL_TRIANGLE: triName = "Symmetrical Triangle"; break; + } + Alert(_Symbol, " ", EnumToString(_Period), ": ", triName, " breakout! Score: ", tri.score); + } +} +//+------------------------------------------------------------------+ +//| Draw Triangle Pattern | +//+------------------------------------------------------------------+ +void DrawTriangle(const TrianglePattern &tri) +{ + if(!ChartPatterns_ShowOnChart) return; + color patternColor; + switch(tri.type) + { + case CHART_PATTERN_ASCENDING_TRIANGLE: patternColor = Triangle_AscendingColor; break; + case CHART_PATTERN_DESCENDING_TRIANGLE: patternColor = Triangle_DescendingColor; break; + default: patternColor = Triangle_SymmetricalColor; break; + } + // Draw upper trendline + string upperName = tri.objName + "_UPPER"; + ObjectCreate(0, upperName, OBJ_TREND, 0, tri.upperStartTime, tri.upperStart, tri.apexTime, tri.apexPrice); + ObjectSetInteger(0, upperName, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, upperName, OBJPROP_WIDTH, 2); + // Draw lower trendline + string lowerName = tri.objName + "_LOWER"; + ObjectCreate(0, lowerName, OBJ_TREND, 0, tri.lowerStartTime, tri.lowerStart, tri.apexTime, tri.apexPrice); + ObjectSetInteger(0, lowerName, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, lowerName, OBJPROP_WIDTH, 2); + // Draw label + string labelName = tri.objName + "_LABEL"; + string labelText = ""; + switch(tri.type) + { + case CHART_PATTERN_ASCENDING_TRIANGLE: labelText = "Asc Triangle"; break; + case CHART_PATTERN_DESCENDING_TRIANGLE: labelText = "Desc Triangle"; break; + case CHART_PATTERN_SYMMETRICAL_TRIANGLE: labelText = "Sym Triangle"; break; + } + labelText = StringFormat("%s [%d]", labelText, tri.score); + ObjectCreate(0, labelName, OBJ_TEXT, 0, tri.upperStartTime, tri.upperStart + g_cachedATR * 0.3); + ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); + ObjectSetInteger(0, labelName, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 10); +} +//+==================================================================+ +//| FLAGS & PENNANTS DETECTION | +//+==================================================================+ +//+------------------------------------------------------------------+ +//| Detect Flag and Pennant Patterns | +//+------------------------------------------------------------------+ +void DetectFlagsAndPennants(const datetime &time[], const double &high[], + const double &low[], const double &close[], int limit) +{ + if(!FlagPennant_Enabled) return; + if(g_cachedATR <= 0) return; + int lookback = MathMin(ChartPatterns_Lookback, limit); + // Look for a strong pole (impulsive move) + for(int poleEnd = 1; poleEnd < lookback / 2; poleEnd++) + { + // Find pole start - look for significant move + int poleStart = -1; + double poleHigh = high[poleEnd]; + double poleLow = low[poleEnd]; + for(int i = poleEnd + 1; i < poleEnd + 30 && i < lookback; i++) + { + if(high[i] > poleHigh) poleHigh = high[i]; + if(low[i] < poleLow) poleLow = low[i]; + double poleHeight = MathAbs(close[poleEnd] - close[i]); + if(poleHeight >= g_cachedATR * FlagPennant_MinPoleHeight) + { + poleStart = i; + break; + } + } + if(poleStart < 0) continue; + // Determine pole direction + bool isBullPole = close[poleEnd] > close[poleStart]; + double poleHeight = MathAbs(close[poleEnd] - close[poleStart]); + int poleBars = poleStart - poleEnd; + // Now look for consolidation (flag/pennant body) + double flagHigh = high[0]; + double flagLow = low[0]; + for(int i = 0; i < poleEnd && i < FlagPennant_MaxFlagBars; i++) + { + if(high[i] > flagHigh) flagHigh = high[i]; + if(low[i] < flagLow) flagLow = low[i]; + } + double flagHeight = flagHigh - flagLow; + double retracement = flagHeight / poleHeight; + // Check if retracement is within acceptable range + if(retracement < FlagPennant_MinRetracement || retracement > FlagPennant_MaxRetracement) + continue; + // Determine if it's a flag or pennant + // Pennant: converging trendlines + // Flag: parallel trendlines (slight counter-trend slope) + ENUM_CHART_PATTERN_TYPE patternType; + // Calculate slopes of flag boundaries + double upperSlope = (flagHigh - high[poleEnd]) / poleEnd; + double lowerSlope = (flagLow - low[poleEnd]) / poleEnd; + bool isConverging = (isBullPole && upperSlope < 0 && lowerSlope > 0) || + (!isBullPole && upperSlope < 0 && lowerSlope > 0); + if(isBullPole) + { + patternType = isConverging ? CHART_PATTERN_BULL_PENNANT : CHART_PATTERN_BULL_FLAG; + } + else + { + patternType = isConverging ? CHART_PATTERN_BEAR_PENNANT : CHART_PATTERN_BEAR_FLAG; + } + // Create pattern + FlagPennantPattern fp; + ZeroMemory(fp); + fp.id = g_fpCount; + fp.objName = "FP_" + IntegerToString(g_fpCount); + fp.type = patternType; + fp.status = PATTERN_CONFIRMED; + fp.poleStart = close[poleStart]; + fp.poleEnd = close[poleEnd]; + fp.poleHeight = poleHeight; + fp.poleStartTime = time[poleStart]; + fp.poleEndTime = time[poleEnd]; + fp.poleBars = poleBars; + fp.flagHigh = flagHigh; + fp.flagLow = flagLow; + fp.flagUpperSlope = upperSlope; + fp.flagLowerSlope = lowerSlope; + fp.flagStartTime = time[poleEnd]; + fp.flagEndTime = time[0]; + fp.flagBars = poleEnd; + // Trade levels + if(isBullPole) + { + fp.entryPrice = flagHigh; + fp.stopLoss = flagLow - g_cachedATR * 0.3; + fp.takeProfit1 = fp.entryPrice + poleHeight; // 100% of pole + fp.takeProfit2 = fp.entryPrice + poleHeight * 1.618; // 161.8% of pole + fp.breakoutConfirmed = (close[0] > flagHigh); + } + else + { + fp.entryPrice = flagLow; + fp.stopLoss = flagHigh + g_cachedATR * 0.3; + fp.takeProfit1 = fp.entryPrice - poleHeight; + fp.takeProfit2 = fp.entryPrice - poleHeight * 1.618; + fp.breakoutConfirmed = (close[0] < flagLow); + } + if(fp.breakoutConfirmed) + { + fp.breakTime = time[0]; + fp.breakPrice = close[0]; + fp.status = PATTERN_TRIGGERED; + } + // Quality scoring + int qualityScore = 0; + if(poleHeight >= g_cachedATR * 2) qualityScore += 25; + if(poleBars <= 10) qualityScore += 15; // Sharp pole is better + if(retracement <= 0.38) qualityScore += 20; // Shallow retracement + if(fp.flagBars <= 15) qualityScore += 15; // Tight consolidation + if(fp.breakoutConfirmed) qualityScore += 25; + fp.score = qualityScore; + if(qualityScore >= 70) fp.quality = PATTERN_QUALITY_PREMIUM; + else if(qualityScore >= 50) fp.quality = PATTERN_QUALITY_HIGH; + else if(qualityScore >= 30) fp.quality = PATTERN_QUALITY_MEDIUM; + else fp.quality = PATTERN_QUALITY_LOW; + fp.isValid = true; + fp.active = true; + fp.createdTime = TimeCurrent(); + ArrayResize(g_fpPatterns, g_fpCount + 1); + g_fpPatterns[g_fpCount] = fp; + g_fpCount++; + g_hasFlagPennant = true; + g_currentFP = fp; + DrawFlagPennant(fp); + if(ChartPatterns_Alerts && fp.breakoutConfirmed) + { + string fpName = ""; + switch(patternType) + { + case CHART_PATTERN_BULL_FLAG: fpName = "Bull Flag"; break; + case CHART_PATTERN_BEAR_FLAG: fpName = "Bear Flag"; break; + case CHART_PATTERN_BULL_PENNANT: fpName = "Bull Pennant"; break; + case CHART_PATTERN_BEAR_PENNANT: fpName = "Bear Pennant"; break; + } + Alert(_Symbol, " ", EnumToString(_Period), ": ", fpName, " breakout! Score: ", fp.score); + } + return; // Found a pattern, exit + } +} +//+------------------------------------------------------------------+ +//| Draw Flag/Pennant Pattern | +//+------------------------------------------------------------------+ +void DrawFlagPennant(const FlagPennantPattern &fp) +{ + if(!ChartPatterns_ShowOnChart) return; + bool isBull = (fp.type == CHART_PATTERN_BULL_FLAG || fp.type == CHART_PATTERN_BULL_PENNANT); + color patternColor = isBull ? FlagPennant_BullColor : FlagPennant_BearColor; + // Draw pole + string poleName = fp.objName + "_POLE"; + ObjectCreate(0, poleName, OBJ_TREND, 0, fp.poleStartTime, fp.poleStart, fp.poleEndTime, fp.poleEnd); + ObjectSetInteger(0, poleName, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, poleName, OBJPROP_WIDTH, 3); + // Draw flag body + string flagName = fp.objName + "_FLAG"; + ObjectCreate(0, flagName, OBJ_RECTANGLE, 0, fp.flagStartTime, fp.flagHigh, fp.flagEndTime, fp.flagLow); + ObjectSetInteger(0, flagName, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, flagName, OBJPROP_FILL, false); + // Draw label + string labelName = fp.objName + "_LABEL"; + string labelText = ""; + switch(fp.type) + { + case CHART_PATTERN_BULL_FLAG: labelText = "Bull Flag"; break; + case CHART_PATTERN_BEAR_FLAG: labelText = "Bear Flag"; break; + case CHART_PATTERN_BULL_PENNANT: labelText = "Bull Pennant"; break; + case CHART_PATTERN_BEAR_PENNANT: labelText = "Bear Pennant"; break; + } + labelText = StringFormat("%s [%d]", labelText, fp.score); + ObjectCreate(0, labelName, OBJ_TEXT, 0, fp.poleStartTime, + isBull ? fp.poleStart - g_cachedATR * 0.3 : fp.poleStart + g_cachedATR * 0.3); + ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); + ObjectSetInteger(0, labelName, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 10); +} +//+==================================================================+ +//| WEDGE DETECTION | +//+==================================================================+ +//+------------------------------------------------------------------+ +//| Detect Wedge Patterns | +//+------------------------------------------------------------------+ +void DetectWedges(const datetime &time[], const double &high[], + const double &low[], const double &close[], int limit) +{ + if(!Wedge_Enabled) return; + if(g_swingHighCount < Wedge_MinTouches || g_swingLowCount < Wedge_MinTouches) return; + // Calculate trendline slopes (similar to triangles) + double upperSlope = 0, upperIntercept = 0; + double lowerSlope = 0, lowerIntercept = 0; + // Fit trendlines using linear regression + if(g_swingHighCount >= 2) + { + double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0; + int n = MathMin(g_swingHighCount, 5); + for(int i = 0; i < n; i++) + { + double x = (double)g_swingHighBars[i]; + double y = g_swingHighs[i]; + sumX += x; + sumY += y; + sumXY += x * y; + sumX2 += x * x; + } + double denom = n * sumX2 - sumX * sumX; + if(MathAbs(denom) > 0.0001) + { + upperSlope = (n * sumXY - sumX * sumY) / denom; + upperIntercept = (sumY - upperSlope * sumX) / n; + } + } + if(g_swingLowCount >= 2) + { + double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0; + int n = MathMin(g_swingLowCount, 5); + for(int i = 0; i < n; i++) + { + double x = (double)g_swingLowBars[i]; + double y = g_swingLows[i]; + sumX += x; + sumY += y; + sumXY += x * y; + sumX2 += x * x; + } + double denom = n * sumX2 - sumX * sumX; + if(MathAbs(denom) > 0.0001) + { + lowerSlope = (n * sumXY - sumX * sumY) / denom; + lowerIntercept = (sumY - lowerSlope * sumX) / n; + } + } + // Wedge: both lines slope in same direction but converge + ENUM_CHART_PATTERN_TYPE wedgeType = CHART_PATTERN_NONE; + // Rising Wedge: both slope up, but lower slope steeper (bearish) + if(upperSlope > 0 && lowerSlope > 0 && lowerSlope > upperSlope) + { + wedgeType = CHART_PATTERN_RISING_WEDGE; + } + // Falling Wedge: both slope down, but upper slope steeper (bullish) + else if(upperSlope < 0 && lowerSlope < 0 && upperSlope < lowerSlope) + { + wedgeType = CHART_PATTERN_FALLING_WEDGE; + } + if(wedgeType == CHART_PATTERN_NONE) return; + // Check angle difference + double upperAngle = MathArctan(upperSlope) * 180 / M_PI; + double lowerAngle = MathArctan(lowerSlope) * 180 / M_PI; + double angleDiff = MathAbs(upperAngle - lowerAngle); + if(angleDiff > Wedge_MaxAngleDiff) return; + // Create pattern + WedgePattern wedge; + ZeroMemory(wedge); + wedge.id = g_wedgeCount; + wedge.objName = "WEDGE_" + IntegerToString(g_wedgeCount); + wedge.type = wedgeType; + wedge.status = PATTERN_CONFIRMED; + wedge.upperSlope = upperSlope; + wedge.lowerSlope = lowerSlope; + wedge.upperStart = g_swingHighs[g_swingHighCount-1]; + wedge.upperEnd = upperSlope * 0 + upperIntercept; + wedge.lowerStart = g_swingLows[g_swingLowCount-1]; + wedge.lowerEnd = lowerSlope * 0 + lowerIntercept; + wedge.upperStartTime = g_swingHighTimes[g_swingHighCount-1]; + wedge.lowerStartTime = g_swingLowTimes[g_swingLowCount-1]; + wedge.upperEndTime = time[0]; + wedge.lowerEndTime = time[0]; + wedge.upperTouches = g_swingHighCount; + wedge.lowerTouches = g_swingLowCount; + wedge.patternHeight = wedge.upperStart - wedge.lowerStart; + wedge.patternBars = MathMax(g_swingHighBars[g_swingHighCount-1], g_swingLowBars[g_swingLowCount-1]); + wedge.convergenceRate = MathAbs(upperSlope - lowerSlope); + // Trade levels + double currentUpper = upperSlope * 0 + upperIntercept; + double currentLower = lowerSlope * 0 + lowerIntercept; + if(wedgeType == CHART_PATTERN_RISING_WEDGE) + { + // Expect bearish breakout + wedge.entryPrice = currentLower; + wedge.stopLoss = currentUpper + g_cachedATR * 0.3; + wedge.takeProfit1 = wedge.entryPrice - wedge.patternHeight; + wedge.takeProfit2 = wedge.entryPrice - wedge.patternHeight * 1.618; + wedge.brokenDown = (close[0] < currentLower); + } + else // Falling Wedge + { + // Expect bullish breakout + wedge.entryPrice = currentUpper; + wedge.stopLoss = currentLower - g_cachedATR * 0.3; + wedge.takeProfit1 = wedge.entryPrice + wedge.patternHeight; + wedge.takeProfit2 = wedge.entryPrice + wedge.patternHeight * 1.618; + wedge.brokenUp = (close[0] > currentUpper); + } + if(wedge.brokenUp || wedge.brokenDown) + { + wedge.breakTime = time[0]; + wedge.breakPrice = close[0]; + wedge.status = PATTERN_TRIGGERED; + } + // Quality scoring + int qualityScore = 0; + if(wedge.upperTouches >= 3) qualityScore += 20; + if(wedge.lowerTouches >= 3) qualityScore += 20; + if(wedge.patternBars >= Wedge_MinBars) qualityScore += 20; + if(wedge.brokenUp || wedge.brokenDown) qualityScore += 20; + wedge.score = qualityScore; + if(qualityScore >= 70) wedge.quality = PATTERN_QUALITY_PREMIUM; + else if(qualityScore >= 50) wedge.quality = PATTERN_QUALITY_HIGH; + else if(qualityScore >= 30) wedge.quality = PATTERN_QUALITY_MEDIUM; + else wedge.quality = PATTERN_QUALITY_LOW; + wedge.isValid = true; + wedge.active = true; + wedge.createdTime = TimeCurrent(); + ArrayResize(g_wedgePatterns, g_wedgeCount + 1); + g_wedgePatterns[g_wedgeCount] = wedge; + g_wedgeCount++; + g_hasWedge = true; + g_currentWedge = wedge; + DrawWedge(wedge); + if(ChartPatterns_Alerts && (wedge.brokenUp || wedge.brokenDown)) + { + string wedgeName = (wedgeType == CHART_PATTERN_RISING_WEDGE) ? "Rising Wedge" : "Falling Wedge"; + Alert(_Symbol, " ", EnumToString(_Period), ": ", wedgeName, " breakout! Score: ", wedge.score); + } +} +//+------------------------------------------------------------------+ +//| Draw Wedge Pattern | +//+------------------------------------------------------------------+ +void DrawWedge(const WedgePattern &wedge) +{ + if(!ChartPatterns_ShowOnChart) return; + color patternColor = (wedge.type == CHART_PATTERN_RISING_WEDGE) ? Wedge_RisingColor : Wedge_FallingColor; + // Draw upper trendline + string upperName = wedge.objName + "_UPPER"; + ObjectCreate(0, upperName, OBJ_TREND, 0, wedge.upperStartTime, wedge.upperStart, wedge.upperEndTime, wedge.upperEnd); + ObjectSetInteger(0, upperName, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, upperName, OBJPROP_WIDTH, 2); + ObjectSetInteger(0, upperName, OBJPROP_RAY_RIGHT, true); + // Draw lower trendline + string lowerName = wedge.objName + "_LOWER"; + ObjectCreate(0, lowerName, OBJ_TREND, 0, wedge.lowerStartTime, wedge.lowerStart, wedge.lowerEndTime, wedge.lowerEnd); + ObjectSetInteger(0, lowerName, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, lowerName, OBJPROP_WIDTH, 2); + ObjectSetInteger(0, lowerName, OBJPROP_RAY_RIGHT, true); + // Draw label + string labelName = wedge.objName + "_LABEL"; + string labelText = (wedge.type == CHART_PATTERN_RISING_WEDGE) ? "Rising Wedge" : "Falling Wedge"; + labelText = StringFormat("%s [%d]", labelText, wedge.score); + ObjectCreate(0, labelName, OBJ_TEXT, 0, wedge.upperStartTime, wedge.upperStart + g_cachedATR * 0.3); + ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); + ObjectSetInteger(0, labelName, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 10); +} +//+------------------------------------------------------------------+ +//| Detect Diamond Patterns (Expansion -> Contraction) | +//+------------------------------------------------------------------+ +void DetectDiamonds(const datetime &time[], const double &high[], + const double &low[], const double &close[], int limit) +{ + if(!Diamond_Enabled) return; + if(g_swingHighCount < 4 || g_swingLowCount < 4) return; + int lookback = MathMin(Diamond_MinBars * 2, limit); + // Diamond pattern: 4 swing points forming expansion then contraction + // Shape: /\ (diamond top) or \/ (diamond bottom) + // \/ /\ + // Look for diamond top (bearish reversal) + for(int apex = Diamond_MinBars / 2; apex < lookback - Diamond_MinBars / 2; apex++) + { + // Find 4 key points around apex + // Point 1-2: Expansion phase (widening) + // Point 3-4: Contraction phase (narrowing) + // Search for expansion swing points (before apex) + int expandSwing1 = -1, expandSwing2 = -1; + double expandHigh1 = 0, expandLow1 = 0; + double expandHigh2 = 0, expandLow2 = 0; + // Find first expansion (oldest) + for(int i = apex + Diamond_MinBars / 4; i < apex + Diamond_MinBars; i++) + { + if(i >= g_swingHighCount || i >= g_swingLowCount) break; + if(g_swingHighBars[i] >= apex + Diamond_MinBars / 4 && + g_swingLowBars[i] >= apex + Diamond_MinBars / 4) + { + expandHigh1 = g_swingHighs[i]; + expandLow1 = g_swingLows[i]; + expandSwing1 = i; + break; + } + } + if(expandSwing1 < 0) continue; + // Find second expansion (more recent, wider than first) + for(int i = expandSwing1 - 1; i >= 0; i--) + { + if(i >= g_swingHighCount || i >= g_swingLowCount) continue; + int highBar = g_swingHighBars[i]; + int lowBar = g_swingLowBars[i]; + if(highBar >= apex && highBar < apex + Diamond_MinBars / 4 && + lowBar >= apex && lowBar < apex + Diamond_MinBars / 4) + { + expandHigh2 = g_swingHighs[i]; + expandLow2 = g_swingLows[i]; + expandSwing2 = i; + // Check expansion: range2 > range1 + double range1 = expandHigh1 - expandLow1; + double range2 = expandHigh2 - expandLow2; + if(range2 > range1 * Diamond_ExpansionRatio) + { + break; + } + } + } + if(expandSwing2 < 0) continue; + // Now search for contraction phase (after apex) + int contractSwing1 = -1, contractSwing2 = -1; + double contractHigh1 = 0, contractLow1 = 0; + double contractHigh2 = 0, contractLow2 = 0; + // Find first contraction (right after apex) + for(int i = 0; i < g_swingHighCount && i < g_swingLowCount; i++) + { + int highBar = g_swingHighBars[i]; + int lowBar = g_swingLowBars[i]; + if(highBar < apex && highBar >= apex - Diamond_MinBars / 4 && + lowBar < apex && lowBar >= apex - Diamond_MinBars / 4) + { + contractHigh1 = g_swingHighs[i]; + contractLow1 = g_swingLows[i]; + contractSwing1 = i; + break; + } + } + if(contractSwing1 < 0) continue; + // Find second contraction (more recent, narrower than first) + for(int i = contractSwing1 + 1; i < g_swingHighCount && i < g_swingLowCount; i++) + { + int highBar = g_swingHighBars[i]; + int lowBar = g_swingLowBars[i]; + if(highBar <= apex - Diamond_MinBars / 4 && + lowBar <= apex - Diamond_MinBars / 4) + { + contractHigh2 = g_swingHighs[i]; + contractLow2 = g_swingLows[i]; + contractSwing2 = i; + // Check contraction: range2 < range1 + double range1 = contractHigh1 - contractLow1; + double range2 = contractHigh2 - contractLow2; + if(range2 < range1 / Diamond_ExpansionRatio) + { + break; + } + } + } + if(contractSwing2 < 0) continue; + // Validate diamond shape + double patternHigh = MathMax(expandHigh2, contractHigh1); + double patternLow = MathMin(expandLow2, contractLow1); + double patternRange = patternHigh - patternLow; + if(patternRange < g_cachedATR * 2.0) continue; // Too small + // Determine if diamond top or bottom + bool isDiamondTop = (patternHigh > high[apex + Diamond_MinBars]); + bool isDiamondBottom = (patternLow < low[apex + Diamond_MinBars]); + if(!isDiamondTop && !isDiamondBottom) continue; + // Create pattern + DiamondPattern diamond; + ZeroMemory(diamond); + diamond.id = g_diamondCount; + diamond.objName = "DIAMOND_" + IntegerToString(g_diamondCount); + diamond.type = isDiamondTop ? CHART_PATTERN_DIAMOND_TOP : CHART_PATTERN_DIAMOND_BOTTOM; + diamond.status = PATTERN_CONFIRMED; + diamond.expandHigh1 = expandHigh1; + diamond.expandLow1 = expandLow1; + diamond.expandHigh2 = expandHigh2; + diamond.expandLow2 = expandLow2; + diamond.contractHigh1 = contractHigh1; + diamond.contractLow1 = contractLow1; + diamond.contractHigh2 = contractHigh2; + diamond.contractLow2 = contractLow2; + diamond.patternHigh = patternHigh; + diamond.patternLow = patternLow; + diamond.patternMid = (patternHigh + patternLow) / 2; + diamond.startTime = time[apex + Diamond_MinBars]; + diamond.endTime = time[apex - Diamond_MinBars / 2]; + diamond.patternBars = Diamond_MinBars + Diamond_MinBars / 2; + // Calculate trade levels + if(isDiamondTop) + { + // Bearish breakout expected + diamond.entryPrice = patternLow - g_cachedATR * 0.1; // Break below + diamond.stopLoss = patternHigh + g_cachedATR * 0.3; + diamond.takeProfit1 = patternLow - patternRange * 0.618; + diamond.takeProfit2 = patternLow - patternRange * 1.0; + } + else + { + // Bullish breakout expected + diamond.entryPrice = patternHigh + g_cachedATR * 0.1; // Break above + diamond.stopLoss = patternLow - g_cachedATR * 0.3; + diamond.takeProfit1 = patternHigh + patternRange * 0.618; + diamond.takeProfit2 = patternHigh + patternRange * 1.0; + } + double riskPoints = MathAbs(diamond.entryPrice - diamond.stopLoss); + double rewardPoints = MathAbs(diamond.takeProfit1 - diamond.entryPrice); + diamond.riskReward = (riskPoints > 0) ? rewardPoints / riskPoints : 0; + // Quality scoring + int qualityScore = 0; + // Symmetry of expansion/contraction + double expansionRatio = (expandHigh2 - expandLow2) / (expandHigh1 - expandLow1); + double contractionRatio = (contractHigh1 - contractLow1) / (contractHigh2 - contractLow2); + if(expansionRatio >= 1.8 && contractionRatio >= 1.8) qualityScore += 40; + else if(expansionRatio >= 1.5 && contractionRatio >= 1.5) qualityScore += 25; + else qualityScore += 10; + // Pattern size + if(patternRange >= g_cachedATR * 4.0) qualityScore += 30; + else if(patternRange >= g_cachedATR * 3.0) qualityScore += 20; + else if(patternRange >= g_cachedATR * 2.0) qualityScore += 10; + // Risk:Reward + if(diamond.riskReward >= 3.0) qualityScore += 30; + else if(diamond.riskReward >= 2.0) qualityScore += 20; + else if(diamond.riskReward >= 1.5) qualityScore += 10; + diamond.score = qualityScore; + if(qualityScore >= 70) diamond.quality = PATTERN_QUALITY_PREMIUM; + else if(qualityScore >= 50) diamond.quality = PATTERN_QUALITY_HIGH; + else if(qualityScore >= 30) diamond.quality = PATTERN_QUALITY_MEDIUM; + else diamond.quality = PATTERN_QUALITY_LOW; + diamond.isValid = true; + diamond.active = true; + diamond.createdTime = TimeCurrent(); + diamond.brokenUp = false; + diamond.brokenDown = false; + // Store pattern + if(g_diamondCount < ArraySize(g_diamondPatterns)) + { + g_diamondPatterns[g_diamondCount] = diamond; + g_diamondCount++; + // Draw pattern + DrawDiamondPattern(diamond); + if(g_verboseLog) + { + PrintFormat("[OK] Diamond %s Detected | Score: %d | R:R: %.2f | Range: %.1f ATR", + isDiamondTop ? "TOP" : "BOTTOM", + qualityScore, + diamond.riskReward, + patternRange / g_cachedATR); + } + } + break; // Found valid diamond, stop searching at this apex + } +} +//+------------------------------------------------------------------+ +//| Draw Diamond Pattern on Chart | +//+------------------------------------------------------------------+ +void DrawDiamondPattern(const DiamondPattern &diamond) +{ + if(!ChartPatterns_ShowOnChart) return; + color patternColor = (diamond.type == CHART_PATTERN_DIAMOND_TOP) ? + Diamond_TopColor : Diamond_BottomColor; + // Draw diamond outline (4 lines forming diamond shape) + string objName; + // Left expansion line (bottom-left to top-left) + objName = diamond.objName + "_L1"; + ObjectCreate(0, objName, OBJ_TREND, 0, + diamond.startTime, diamond.expandLow1, + diamond.startTime + (diamond.endTime - diamond.startTime) / 3, diamond.expandHigh2); + ObjectSetInteger(0, objName, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, objName, OBJPROP_WIDTH, 2); + ObjectSetInteger(0, objName, OBJPROP_STYLE, STYLE_SOLID); + ObjectSetInteger(0, objName, OBJPROP_RAY_RIGHT, false); + // Top line (top-left to top-right) + objName = diamond.objName + "_T"; + ObjectCreate(0, objName, OBJ_TREND, 0, + diamond.startTime + (diamond.endTime - diamond.startTime) / 3, diamond.expandHigh2, + diamond.startTime + 2 * (diamond.endTime - diamond.startTime) / 3, diamond.contractHigh1); + ObjectSetInteger(0, objName, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, objName, OBJPROP_WIDTH, 2); + ObjectSetInteger(0, objName, OBJPROP_STYLE, STYLE_SOLID); + ObjectSetInteger(0, objName, OBJPROP_RAY_RIGHT, false); + // Right contraction line (top-right to bottom-right) + objName = diamond.objName + "_R"; + ObjectCreate(0, objName, OBJ_TREND, 0, + diamond.startTime + 2 * (diamond.endTime - diamond.startTime) / 3, diamond.contractHigh1, + diamond.endTime, diamond.contractLow2); + ObjectSetInteger(0, objName, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, objName, OBJPROP_WIDTH, 2); + ObjectSetInteger(0, objName, OBJPROP_STYLE, STYLE_SOLID); + ObjectSetInteger(0, objName, OBJPROP_RAY_RIGHT, false); + // Bottom line (bottom-right to bottom-left) + objName = diamond.objName + "_B"; + ObjectCreate(0, objName, OBJ_TREND, 0, + diamond.endTime, diamond.contractLow2, + diamond.startTime, diamond.expandLow1); + ObjectSetInteger(0, objName, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, objName, OBJPROP_WIDTH, 2); + ObjectSetInteger(0, objName, OBJPROP_STYLE, STYLE_SOLID); + ObjectSetInteger(0, objName, OBJPROP_RAY_RIGHT, false); + // Label with pattern info + string labelName = diamond.objName + "_Label"; + string labelText = StringFormat("%s [%d] R:R:%.1f", + (diamond.type == CHART_PATTERN_DIAMOND_TOP) ? "[*] TOP" : "[*] BOTTOM", + diamond.score, + diamond.riskReward); + ObjectCreate(0, labelName, OBJ_TEXT, 0, + diamond.startTime + (diamond.endTime - diamond.startTime) / 2, + diamond.patternHigh + g_cachedATR * 0.3); + ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); + ObjectSetInteger(0, labelName, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 10); +} +//+==================================================================+ +//| V-PATTERN DETECTION | +//+==================================================================+ +//+------------------------------------------------------------------+ +//| Detect V-Patterns (Sharp Reversals) | +//+------------------------------------------------------------------+ +void DetectVPatterns(const datetime &time[], const double &high[], + const double &low[], const double &close[], int limit) +{ + if(!VPattern_Enabled) return; + if(g_cachedATR <= 0) return; + int lookback = MathMin(VPattern_MaxBars, limit); + // Look for V-Bottom + for(int apex = 1; apex < lookback - 1; apex++) + { + // Check if this is a sharp low (apex of V-bottom) + if(low[apex] >= low[apex-1] || low[apex] >= low[apex+1]) continue; + // Find the start of the drop + int dropStart = -1; + double dropHigh = high[apex]; + for(int i = apex + 1; i < apex + VPattern_MaxBars / 2 && i < lookback; i++) + { + if(high[i] > dropHigh) + { + dropHigh = high[i]; + dropStart = i; + } + } + if(dropStart < 0) continue; + // Calculate drop distance + double dropDistance = dropHigh - low[apex]; + if(dropDistance < g_cachedATR * VPattern_MinHeight) continue; + // Check for rise (recovery) + double riseHigh = high[0]; + for(int i = 0; i < apex; i++) + { + if(high[i] > riseHigh) riseHigh = high[i]; + } + double riseDistance = riseHigh - low[apex]; + // Calculate symmetry + double symmetry = MathMin(dropDistance, riseDistance) / MathMax(dropDistance, riseDistance); + if(symmetry < VPattern_MinSymmetry) continue; + // Create pattern + VPattern vp; + ZeroMemory(vp); + vp.id = g_vCount; + vp.objName = "VBOT_" + IntegerToString(g_vCount); + vp.type = CHART_PATTERN_V_BOTTOM; + vp.status = PATTERN_CONFIRMED; + vp.startPrice = dropHigh; + vp.apexPrice = low[apex]; + vp.endPrice = riseHigh; + vp.startTime = time[dropStart]; + vp.apexTime = time[apex]; + vp.endTime = time[0]; + vp.startBar = dropStart; + vp.apexBar = apex; + vp.endBar = 0; + vp.dropDistance = dropDistance; + vp.riseDistance = riseDistance; + vp.symmetryRatio = symmetry; + vp.dropBars = dropStart - apex; + vp.riseBars = apex; + // Trade levels + vp.entryPrice = close[0]; + vp.stopLoss = vp.apexPrice - g_cachedATR * 0.3; + vp.takeProfit1 = vp.startPrice; // Back to original high + vp.takeProfit2 = vp.startPrice + dropDistance * 0.618; // Extension + // Quality scoring + int qualityScore = 0; + if(symmetry >= 0.9) qualityScore += 30; + else if(symmetry >= 0.8) qualityScore += 20; + else if(symmetry >= 0.7) qualityScore += 10; + if(dropDistance >= g_cachedATR * 3) qualityScore += 30; + else if(dropDistance >= g_cachedATR * 2) qualityScore += 20; + if(vp.dropBars <= 10 && vp.riseBars <= 10) qualityScore += 20; // Sharp V + vp.score = qualityScore; + if(qualityScore >= 70) vp.quality = PATTERN_QUALITY_PREMIUM; + else if(qualityScore >= 50) vp.quality = PATTERN_QUALITY_HIGH; + else if(qualityScore >= 30) vp.quality = PATTERN_QUALITY_MEDIUM; + else vp.quality = PATTERN_QUALITY_LOW; + vp.isValid = true; + vp.active = true; + vp.createdTime = TimeCurrent(); + ArrayResize(g_vPatterns, g_vCount + 1); + g_vPatterns[g_vCount] = vp; + g_vCount++; + g_hasVPattern = true; + g_currentVPattern = vp; + DrawVPattern(vp); + if(ChartPatterns_Alerts) + { + Alert(_Symbol, " ", EnumToString(_Period), ": V-Bottom detected! Score: ", vp.score); + } + return; // Found pattern + } + // Look for V-Top (same logic inverted) + for(int apex = 1; apex < lookback - 1; apex++) + { + if(high[apex] <= high[apex-1] || high[apex] <= high[apex+1]) continue; + int riseStart = -1; + double riseLow = low[apex]; + for(int i = apex + 1; i < apex + VPattern_MaxBars / 2 && i < lookback; i++) + { + if(low[i] < riseLow) + { + riseLow = low[i]; + riseStart = i; + } + } + if(riseStart < 0) continue; + double riseDistance = high[apex] - riseLow; + if(riseDistance < g_cachedATR * VPattern_MinHeight) continue; + double dropLow = low[0]; + for(int i = 0; i < apex; i++) + { + if(low[i] < dropLow) dropLow = low[i]; + } + double dropDistance = high[apex] - dropLow; + double symmetry = MathMin(riseDistance, dropDistance) / MathMax(riseDistance, dropDistance); + if(symmetry < VPattern_MinSymmetry) continue; + VPattern vp; + ZeroMemory(vp); + vp.id = g_vCount; + vp.objName = "VTOP_" + IntegerToString(g_vCount); + vp.type = CHART_PATTERN_V_TOP; + vp.status = PATTERN_CONFIRMED; + vp.startPrice = riseLow; + vp.apexPrice = high[apex]; + vp.endPrice = dropLow; + vp.startTime = time[riseStart]; + vp.apexTime = time[apex]; + vp.endTime = time[0]; + vp.dropDistance = dropDistance; + vp.riseDistance = riseDistance; + vp.symmetryRatio = symmetry; + vp.entryPrice = close[0]; + vp.stopLoss = vp.apexPrice + g_cachedATR * 0.3; + vp.takeProfit1 = vp.startPrice; + vp.takeProfit2 = vp.startPrice - riseDistance * 0.618; + int qualityScore = 0; + if(symmetry >= 0.9) qualityScore += 30; + else if(symmetry >= 0.8) qualityScore += 20; + if(riseDistance >= g_cachedATR * 3) qualityScore += 30; + else if(riseDistance >= g_cachedATR * 2) qualityScore += 20; + vp.score = qualityScore; + if(qualityScore >= 70) vp.quality = PATTERN_QUALITY_PREMIUM; + else if(qualityScore >= 50) vp.quality = PATTERN_QUALITY_HIGH; + else if(qualityScore >= 30) vp.quality = PATTERN_QUALITY_MEDIUM; + else vp.quality = PATTERN_QUALITY_LOW; + vp.isValid = true; + vp.active = true; + vp.createdTime = TimeCurrent(); + ArrayResize(g_vPatterns, g_vCount + 1); + g_vPatterns[g_vCount] = vp; + g_vCount++; + g_hasVPattern = true; + g_currentVPattern = vp; + DrawVPattern(vp); + if(ChartPatterns_Alerts) + { + Alert(_Symbol, " ", EnumToString(_Period), ": V-Top detected! Score: ", vp.score); + } + return; + } +} +//+------------------------------------------------------------------+ +//| Draw V-Pattern | +//+------------------------------------------------------------------+ +void DrawVPattern(const VPattern &vp) +{ + if(!ChartPatterns_ShowOnChart) return; + color patternColor = (vp.type == CHART_PATTERN_V_BOTTOM) ? VPattern_BottomColor : VPattern_TopColor; + // Draw left leg (drop/rise to apex) + string leg1Name = vp.objName + "_LEG1"; + ObjectCreate(0, leg1Name, OBJ_TREND, 0, vp.startTime, vp.startPrice, vp.apexTime, vp.apexPrice); + ObjectSetInteger(0, leg1Name, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, leg1Name, OBJPROP_WIDTH, 2); + // Draw right leg (apex to recovery) + string leg2Name = vp.objName + "_LEG2"; + ObjectCreate(0, leg2Name, OBJ_TREND, 0, vp.apexTime, vp.apexPrice, vp.endTime, vp.endPrice); + ObjectSetInteger(0, leg2Name, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, leg2Name, OBJPROP_WIDTH, 2); + // Draw apex marker + string apexName = vp.objName + "_APEX"; + ENUM_OBJECT arrowType = (vp.type == CHART_PATTERN_V_BOTTOM) ? OBJ_ARROW_UP : OBJ_ARROW_DOWN; + ObjectCreate(0, apexName, arrowType, 0, vp.apexTime, vp.apexPrice); + ObjectSetInteger(0, apexName, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, apexName, OBJPROP_WIDTH, 3); + // Draw label + string labelName = vp.objName + "_LABEL"; + string labelText = (vp.type == CHART_PATTERN_V_BOTTOM) ? + StringFormat("V-Bottom [%d]", vp.score) : + StringFormat("V-Top [%d]", vp.score); + double labelY = (vp.type == CHART_PATTERN_V_BOTTOM) ? + vp.apexPrice - g_cachedATR * 0.5 : + vp.apexPrice + g_cachedATR * 0.5; + ObjectCreate(0, labelName, OBJ_TEXT, 0, vp.apexTime, labelY); + ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); + ObjectSetInteger(0, labelName, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 10); +} +//+------------------------------------------------------------------+ +//| Cleanup Chart Pattern Objects | +//+------------------------------------------------------------------+ +void CleanupChartPatternObjects() +{ + ObjectsDeleteAll(0, "HS_"); + ObjectsDeleteAll(0, "DTB_"); + ObjectsDeleteAll(0, "TRI_"); + ObjectsDeleteAll(0, "FP_"); + ObjectsDeleteAll(0, "WEDGE_"); + ObjectsDeleteAll(0, "DIAMOND_"); + ObjectsDeleteAll(0, "VBOT_"); + ObjectsDeleteAll(0, "VTOP_"); +} +//+==================================================================+ +//| MASTER PATTERN DETECTION | +//+==================================================================+ +//+------------------------------------------------------------------+ +//| Initialize All Pattern Systems | +//+------------------------------------------------------------------+ +void InitializeAllPatterns() +{ + InitializeChartPatterns(); + InitializeExtendedCandlePatterns(); + ZeroMemory(g_allPatterns); + g_patternsInitialized = true; + Print("[OK] All Pattern Systems Initialized"); + Print(" - Chart Patterns: Head & Shoulders, Double/Triple Top-Bottom,"); + Print(" Triangles, Flags, Pennants, Wedges, Diamonds, V-Patterns"); + Print(" - Candlestick Patterns: 30+ patterns including:"); + Print(" Doji, Hammer, Engulfing, Harami, Morning/Evening Star,"); + Print(" Three White Soldiers, Three Black Crows, and more"); +} +//+------------------------------------------------------------------+ +//| Master Pattern Detection Function | +//| Call this from OnCalculate() after ICT concepts detection | +//+------------------------------------------------------------------+ +void DetectAllPatterns(const datetime &time[], const double &open[], + const double &high[], const double &low[], + const double &close[], int limit) +{ + if(!g_patternsInitialized) InitializeAllPatterns(); + // Update ATR if needed + if(g_cachedATR <= 0) UpdateCachedATR(14); + // =============================================================== + // CANDLESTICK PATTERN DETECTION + // =============================================================== + if(CandlePatterns_Enabled) + { + CandlePatternStructExtended candlePattern = DetectAllCandlePatterns(0); + if(candlePattern.patternStrength >= CandlePatterns_MinStrength) + { + DrawCandlePatternArrow(candlePattern); + // Store for reference + ArrayResize(g_extendedCandlePatterns, g_extendedCandleCount + 1); + g_extendedCandlePatterns[g_extendedCandleCount] = candlePattern; + g_extendedCandleCount++; + // Limit array size + if(g_extendedCandleCount > 100) + { + for(int i = 0; i < 50; i++) + g_extendedCandlePatterns[i] = g_extendedCandlePatterns[i + 50]; + g_extendedCandleCount = 50; + } + } + } + // =============================================================== + // CHART PATTERN DETECTION + // =============================================================== + if(ChartPatterns_Enabled) + { + // First, find swing points + FindSwingPointsForPatterns(time, high, low, limit, 5); + // Detect each pattern type + if(HS_Enabled) + DetectHeadAndShoulders(time, high, low, close, limit); + if(DTB_Enabled) + DetectDoubleTripleTopBottom(time, high, low, close, limit); + if(Triangle_Enabled) + DetectTriangles(time, high, low, close, limit); + if(FlagPennant_Enabled) + DetectFlagsAndPennants(time, high, low, close, limit); + if(Wedge_Enabled) + DetectWedges(time, high, low, close, limit); + if(Diamond_Enabled) // <- ΝΕΟ! + DetectDiamonds(time, high, low, close, limit); // <- ΝΕΟ! + if(VPattern_Enabled) + DetectVPatterns(time, high, low, close, limit); + } + // Update timestamp + g_patternsLastUpdate = TimeCurrent(); + // * v9.13 FIX#1a: EXPIRE CHART PATTERNS + // BUG: expiryTime was SET in DetectHeadAndShoulders() but NEVER CHECKED. + // Result: H&S Top (Score=90) detected Jan 12 persisted until Feb 13 (33 days!), + // blocking ALL 1,640 BUY entries. TBS/Judas/Divergence all have expiry checks, + // but chart patterns did not. + // NOTE: Only HeadShouldersPattern has expiryTime field. Other pattern structs + // use createdTime + ChartPatterns_ExpiryBars * PeriodSeconds() as expiry. + { + datetime _now = TimeCurrent(); + int expirySeconds = ChartPatterns_ExpiryBars * PeriodSeconds(); + // Expire H&S patterns (has expiryTime field) + for(int i = 0; i < g_hsCount; i++) + { + if(g_hsPatterns[i].active && g_hsPatterns[i].expiryTime > 0 && _now > g_hsPatterns[i].expiryTime) + { + g_hsPatterns[i].active = false; + g_hsPatterns[i].isValid = false; + g_hsPatterns[i].status = PATTERN_EXPIRED; + Print("* v9.13 FIX#1a: H&S pattern ", i, " EXPIRED at ", TimeToString(_now), + " | Type: ", (g_hsPatterns[i].type == CHART_PATTERN_HEAD_SHOULDERS_TOP) ? "H&S Top" : "Inv H&S", + " | Score: ", g_hsPatterns[i].score, + " | Age: ", (_now - g_hsPatterns[i].createdTime) / 3600, "h"); + } + } + // Expire Double/Triple Top/Bottom patterns (uses createdTime + expirySeconds) + for(int i = 0; i < g_mtbCount; i++) + { + if(g_mtbPatterns[i].active && g_mtbPatterns[i].createdTime > 0 && + _now > g_mtbPatterns[i].createdTime + expirySeconds) + { + g_mtbPatterns[i].active = false; + g_mtbPatterns[i].isValid = false; + if(g_verboseLog) + Print("* v9.13 FIX#1a: MTB pattern ", i, " EXPIRED | Age: ", + (_now - g_mtbPatterns[i].createdTime) / 3600, "h"); + } + } + // Expire Triangle patterns (uses createdTime + expirySeconds) + for(int i = 0; i < g_triangleCount; i++) + { + if(g_trianglePatterns[i].active && g_trianglePatterns[i].createdTime > 0 && + _now > g_trianglePatterns[i].createdTime + expirySeconds) + { + g_trianglePatterns[i].active = false; + g_trianglePatterns[i].isValid = false; + if(g_verboseLog) + Print("* v9.13 FIX#1a: Triangle pattern ", i, " EXPIRED | Age: ", + (_now - g_trianglePatterns[i].createdTime) / 3600, "h"); + } + } + // Expire Flag/Pennant patterns (uses createdTime + expirySeconds) + for(int i = 0; i < g_fpCount; i++) + { + if(g_fpPatterns[i].active && g_fpPatterns[i].createdTime > 0 && + _now > g_fpPatterns[i].createdTime + expirySeconds) + { + g_fpPatterns[i].active = false; + g_fpPatterns[i].isValid = false; + if(g_verboseLog) + Print("* v9.13 FIX#1a: Flag/Pennant pattern ", i, " EXPIRED | Age: ", + (_now - g_fpPatterns[i].createdTime) / 3600, "h"); + } + } + // Expire Wedge patterns (uses createdTime + expirySeconds) + for(int i = 0; i < g_wedgeCount; i++) + { + if(g_wedgePatterns[i].active && g_wedgePatterns[i].createdTime > 0 && + _now > g_wedgePatterns[i].createdTime + expirySeconds) + { + g_wedgePatterns[i].active = false; + g_wedgePatterns[i].isValid = false; + if(g_verboseLog) + Print("* v9.13 FIX#1a: Wedge pattern ", i, " EXPIRED | Age: ", + (_now - g_wedgePatterns[i].createdTime) / 3600, "h"); + } + } + // Expire Diamond patterns (uses createdTime + expirySeconds) + for(int i = 0; i < g_diamondCount; i++) + { + if(g_diamondPatterns[i].active && g_diamondPatterns[i].createdTime > 0 && + _now > g_diamondPatterns[i].createdTime + expirySeconds) + { + g_diamondPatterns[i].active = false; + g_diamondPatterns[i].isValid = false; + if(g_verboseLog) + Print("* v9.13 FIX#1a: Diamond pattern ", i, " EXPIRED | Age: ", + (_now - g_diamondPatterns[i].createdTime) / 3600, "h"); + } + } + // Expire V patterns (uses createdTime + expirySeconds) + for(int i = 0; i < g_vCount; i++) + { + if(g_vPatterns[i].active && g_vPatterns[i].createdTime > 0 && + _now > g_vPatterns[i].createdTime + expirySeconds) + { + g_vPatterns[i].active = false; + g_vPatterns[i].isValid = false; + if(g_verboseLog) + Print("* v9.13 FIX#1a: V pattern ", i, " EXPIRED | Age: ", + (_now - g_vPatterns[i].createdTime) / 3600, "h"); + } + } + } + // Update master container + UpdateMasterPatternContainer(); +} +//+------------------------------------------------------------------+ +//| Update Master Pattern Container | +//+------------------------------------------------------------------+ +void UpdateMasterPatternContainer() +{ + g_allPatterns.hsCount = g_hsCount; + g_allPatterns.mtbCount = g_mtbCount; + g_allPatterns.triangleCount = g_triangleCount; + g_allPatterns.fpCount = g_fpCount; + g_allPatterns.wedgeCount = g_wedgeCount; + g_allPatterns.diamondCount = g_diamondCount; + g_allPatterns.vCount = g_vCount; + g_allPatterns.candleCount = g_extendedCandleCount; + g_allPatterns.totalPatterns = g_hsCount + g_mtbCount + g_triangleCount + + g_fpCount + g_wedgeCount + g_diamondCount + + g_vCount + g_extendedCandleCount; + g_allPatterns.lastUpdate = TimeCurrent(); + // Find strongest pattern + g_allPatterns.hasActivePattern = false; + g_allPatterns.strongestScore = 0; + g_allPatterns.strongestPattern = "None"; + g_allPatterns.strongestPatternTime = 0; // * FIX#108 + // Check Head & Shoulders + for(int i = 0; i < g_hsCount; i++) + { + if(g_hsPatterns[i].active && g_hsPatterns[i].score > g_allPatterns.strongestScore) + { + g_allPatterns.strongestScore = g_hsPatterns[i].score; + g_allPatterns.strongestPattern = (g_hsPatterns[i].type == CHART_PATTERN_HEAD_SHOULDERS_TOP) ? + "H&S Top" : "Inv H&S"; + g_allPatterns.hasActivePattern = true; + g_allPatterns.strongestPatternTime = g_hsPatterns[i].headTime; // * FIX#108 + } + } + // Check Double/Triple + for(int i = 0; i < g_mtbCount; i++) + { + if(g_mtbPatterns[i].active && g_mtbPatterns[i].score > g_allPatterns.strongestScore) + { + g_allPatterns.strongestScore = g_mtbPatterns[i].score; + switch(g_mtbPatterns[i].type) + { + case CHART_PATTERN_DOUBLE_TOP: g_allPatterns.strongestPattern = "Double Top"; break; + case CHART_PATTERN_DOUBLE_BOTTOM: g_allPatterns.strongestPattern = "Double Bottom"; break; + case CHART_PATTERN_TRIPLE_TOP: g_allPatterns.strongestPattern = "Triple Top"; break; + case CHART_PATTERN_TRIPLE_BOTTOM: g_allPatterns.strongestPattern = "Triple Bottom"; break; + } + g_allPatterns.hasActivePattern = true; + // * v9.39 FIX#169: MTB patterns now set strongestPatternTime (was 0, defeating FIX#108 stale check). + // Double Bottom detected Feb02 persisted 8+ days, FIX#108 never fired (strongestPatternTime=0 + // -> patternIsStale always false -> FIX#130 reset when briefly not strongest -> blocked all SELL). + // Use peak2Time (last confirmed peak) as pattern anchor time for stale detection. + datetime _mtbAnchor = g_mtbPatterns[i].peak2Time; + if(_mtbAnchor <= 0) _mtbAnchor = g_mtbPatterns[i].peak1Time; + if(_mtbAnchor <= 0) _mtbAnchor = g_mtbPatterns[i].createdTime; + g_allPatterns.strongestPatternTime = _mtbAnchor; + } + } + // Check Triangles + for(int i = 0; i < g_triangleCount; i++) + { + if(g_trianglePatterns[i].active && g_trianglePatterns[i].score > g_allPatterns.strongestScore) + { + g_allPatterns.strongestScore = g_trianglePatterns[i].score; + switch(g_trianglePatterns[i].type) + { + case CHART_PATTERN_ASCENDING_TRIANGLE: g_allPatterns.strongestPattern = "Asc Triangle"; break; + case CHART_PATTERN_DESCENDING_TRIANGLE: g_allPatterns.strongestPattern = "Desc Triangle"; break; + case CHART_PATTERN_SYMMETRICAL_TRIANGLE: g_allPatterns.strongestPattern = "Sym Triangle"; break; + } + g_allPatterns.hasActivePattern = true; + // * v9.39 FIX#169: set strongestPatternTime for all pattern types + datetime _triAnchor = g_trianglePatterns[i].upperEndTime; + if(_triAnchor <= 0) _triAnchor = g_trianglePatterns[i].createdTime; + g_allPatterns.strongestPatternTime = _triAnchor; + } + } + // Check Flags/Pennants + for(int i = 0; i < g_fpCount; i++) + { + if(g_fpPatterns[i].active && g_fpPatterns[i].score > g_allPatterns.strongestScore) + { + g_allPatterns.strongestScore = g_fpPatterns[i].score; + switch(g_fpPatterns[i].type) + { + case CHART_PATTERN_BULL_FLAG: g_allPatterns.strongestPattern = "Bull Flag"; break; + case CHART_PATTERN_BEAR_FLAG: g_allPatterns.strongestPattern = "Bear Flag"; break; + case CHART_PATTERN_BULL_PENNANT: g_allPatterns.strongestPattern = "Bull Pennant"; break; + case CHART_PATTERN_BEAR_PENNANT: g_allPatterns.strongestPattern = "Bear Pennant"; break; + } + g_allPatterns.hasActivePattern = true; + // * v9.39 FIX#169 + datetime _fpAnchor = g_fpPatterns[i].flagEndTime; + if(_fpAnchor <= 0) _fpAnchor = g_fpPatterns[i].createdTime; + g_allPatterns.strongestPatternTime = _fpAnchor; + } + } + // Check Wedges + for(int i = 0; i < g_wedgeCount; i++) + { + if(g_wedgePatterns[i].active && g_wedgePatterns[i].score > g_allPatterns.strongestScore) + { + g_allPatterns.strongestScore = g_wedgePatterns[i].score; + g_allPatterns.strongestPattern = (g_wedgePatterns[i].type == CHART_PATTERN_RISING_WEDGE) ? + "Rising Wedge" : "Falling Wedge"; + g_allPatterns.hasActivePattern = true; + // * v9.39 FIX#169 + datetime _wAnchor = g_wedgePatterns[i].upperEndTime; + if(_wAnchor <= 0) _wAnchor = g_wedgePatterns[i].createdTime; + g_allPatterns.strongestPatternTime = _wAnchor; + } + } + // Check Diamonds // <- ΝΕΟ! + for(int i = 0; i < g_diamondCount; i++) // <- ΝΕΟ! + { // <- ΝΕΟ! + if(g_diamondPatterns[i].active && g_diamondPatterns[i].score > g_allPatterns.strongestScore) + { // <- ΝΕΟ! + g_allPatterns.strongestScore = g_diamondPatterns[i].score; + g_allPatterns.strongestPattern = (g_diamondPatterns[i].type == CHART_PATTERN_DIAMOND_TOP) ? + "Diamond Top" : "Diamond Bottom"; + g_allPatterns.hasActivePattern = true; // <- ΝΕΟ! + // * v9.39 FIX#169 + datetime _dAnchor = g_diamondPatterns[i].endTime; + if(_dAnchor <= 0) _dAnchor = g_diamondPatterns[i].createdTime; + g_allPatterns.strongestPatternTime = _dAnchor; + } // <- ΝΕΟ! + } // <- ΝΕΟ! + // Check V-Patterns + for(int i = 0; i < g_vCount; i++) + { + if(g_vPatterns[i].active && g_vPatterns[i].score > g_allPatterns.strongestScore) + { + g_allPatterns.strongestScore = g_vPatterns[i].score; + g_allPatterns.strongestPattern = (g_vPatterns[i].type == CHART_PATTERN_V_BOTTOM) ? + "V-Bottom" : "V-Top"; + g_allPatterns.hasActivePattern = true; + // * v9.39 FIX#169 + datetime _vAnchor = g_vPatterns[i].apexTime; + if(_vAnchor <= 0) _vAnchor = g_vPatterns[i].createdTime; + g_allPatterns.strongestPatternTime = _vAnchor; + } + } +} +//+------------------------------------------------------------------+ +//| Cleanup All Pattern Systems | +//+------------------------------------------------------------------+ +void CleanupAllPatterns() +{ + CleanupChartPatternObjects(); + CleanupCandlePatternObjects(); + ArrayResize(g_hsPatterns, 0); + ArrayResize(g_mtbPatterns, 0); + ArrayResize(g_trianglePatterns, 0); + ArrayResize(g_fpPatterns, 0); + ArrayResize(g_wedgePatterns, 0); + ArrayResize(g_diamondPatterns, 0); + ArrayResize(g_vPatterns, 0); + ArrayResize(g_extendedCandlePatterns, 0); + g_hsCount = 0; + g_mtbCount = 0; + g_triangleCount = 0; + g_fpCount = 0; + g_wedgeCount = 0; + g_diamondCount = 0; + g_vCount = 0; + g_extendedCandleCount = 0; + Print("All Pattern Systems cleaned up"); +} +//+------------------------------------------------------------------+ +//| Get Pattern Summary String for Dashboard | +//+------------------------------------------------------------------+ +string GetPatternSummaryString() +{ + string summary = ""; + if(g_allPatterns.hasActivePattern) + { + summary = StringFormat("Pattern: %s [%d]", + g_allPatterns.strongestPattern, + g_allPatterns.strongestScore); + } + else + { + summary = "No Active Patterns"; + } + return summary; +} +//+------------------------------------------------------------------+ +//| Get Candlestick Pattern Summary | +//+------------------------------------------------------------------+ +string GetCandlePatternSummary() +{ + if(g_lastExtendedCandlePattern.patternStrength >= CandlePatterns_MinStrength) + { + return StringFormat("Candle: %s [Str:%d]", + g_lastExtendedCandlePattern.patternName, + g_lastExtendedCandlePattern.patternStrength); + } + return "No Candle Pattern"; +} +//+------------------------------------------------------------------+ +//| Check if Pattern Suggests Entry | +//+------------------------------------------------------------------+ +bool PatternSuggestsBullishEntry() +{ + // Check candlestick pattern + if(g_lastExtendedCandlePattern.isBullish && + g_lastExtendedCandlePattern.patternStrength >= 3) + return true; + // Check chart patterns + for(int i = 0; i < g_hsCount; i++) + { + if(g_hsPatterns[i].type == CHART_PATTERN_HEAD_SHOULDERS_BOTTOM && + g_hsPatterns[i].necklineBroken && g_hsPatterns[i].active) + return true; + } + for(int i = 0; i < g_mtbCount; i++) + { + if(g_mtbPatterns[i].type == CHART_PATTERN_DOUBLE_BOTTOM && + g_mtbPatterns[i].necklineBroken && g_mtbPatterns[i].active) + return true; + } + for(int i = 0; i < g_triangleCount; i++) + { + if(g_trianglePatterns[i].type == CHART_PATTERN_ASCENDING_TRIANGLE && + g_trianglePatterns[i].brokenUp && g_trianglePatterns[i].active) + return true; + } + for(int i = 0; i < g_fpCount; i++) + { + if((g_fpPatterns[i].type == CHART_PATTERN_BULL_FLAG || + g_fpPatterns[i].type == CHART_PATTERN_BULL_PENNANT) && + g_fpPatterns[i].breakoutConfirmed && g_fpPatterns[i].active) + return true; + } + for(int i = 0; i < g_wedgeCount; i++) + { + if(g_wedgePatterns[i].type == CHART_PATTERN_FALLING_WEDGE && + g_wedgePatterns[i].brokenUp && g_wedgePatterns[i].active) + return true; + } + for(int i = 0; i < g_vCount; i++) + { + if(g_vPatterns[i].type == CHART_PATTERN_V_BOTTOM && g_vPatterns[i].active) + return true; + } + return false; +} +//+------------------------------------------------------------------+ +//| Check if Pattern Suggests Bearish Entry | +//+------------------------------------------------------------------+ +bool PatternSuggestsBearishEntry() +{ + // Check candlestick pattern + if(g_lastExtendedCandlePattern.isBearish && + g_lastExtendedCandlePattern.patternStrength >= 3) + return true; + // Check chart patterns + for(int i = 0; i < g_hsCount; i++) + { + if(g_hsPatterns[i].type == CHART_PATTERN_HEAD_SHOULDERS_TOP && + g_hsPatterns[i].necklineBroken && g_hsPatterns[i].active) + return true; + } + for(int i = 0; i < g_mtbCount; i++) + { + if(g_mtbPatterns[i].type == CHART_PATTERN_DOUBLE_TOP && + g_mtbPatterns[i].necklineBroken && g_mtbPatterns[i].active) + return true; + } + for(int i = 0; i < g_triangleCount; i++) + { + if(g_trianglePatterns[i].type == CHART_PATTERN_DESCENDING_TRIANGLE && + g_trianglePatterns[i].brokenDown && g_trianglePatterns[i].active) + return true; + } + for(int i = 0; i < g_fpCount; i++) + { + if((g_fpPatterns[i].type == CHART_PATTERN_BEAR_FLAG || + g_fpPatterns[i].type == CHART_PATTERN_BEAR_PENNANT) && + g_fpPatterns[i].breakoutConfirmed && g_fpPatterns[i].active) + return true; + } + for(int i = 0; i < g_wedgeCount; i++) + { + if(g_wedgePatterns[i].type == CHART_PATTERN_RISING_WEDGE && + g_wedgePatterns[i].brokenDown && g_wedgePatterns[i].active) + return true; + } + for(int i = 0; i < g_vCount; i++) + { + if(g_vPatterns[i].type == CHART_PATTERN_V_TOP && g_vPatterns[i].active) + return true; + } + // Check Diamonds + for(int i = 0; i < g_diamondCount; i++) + { + if(g_diamondPatterns[i].type == CHART_PATTERN_DIAMOND_TOP && + g_diamondPatterns[i].brokenDown && g_diamondPatterns[i].active) + return true; + } + return false; +} +//+------------------------------------------------------------------+ +//| Get Pattern-Based Entry Score Bonus | +//+------------------------------------------------------------------+ +// * v9.11 FIX#22: Direction-aware pattern scoring +// Old: +20 for ANY active pattern regardless of direction -> BUY gets bonus from H&S Top (bearish)! +// New: +bonus only if pattern agrees with direction, PENALTY if opposing +int GetPatternScoreBonus(bool isBullish) +{ + int bonus = 0; + // Candlestick pattern bonus -- ONLY if direction matches + if(isBullish && g_lastExtendedCandlePattern.isBullish) + { + if(g_lastExtendedCandlePattern.patternStrength >= 4) bonus += 15; + else if(g_lastExtendedCandlePattern.patternStrength >= 3) bonus += 10; + else if(g_lastExtendedCandlePattern.patternStrength >= 2) bonus += 5; + } + else if(!isBullish && g_lastExtendedCandlePattern.isBearish) + { + if(g_lastExtendedCandlePattern.patternStrength >= 4) bonus += 15; + else if(g_lastExtendedCandlePattern.patternStrength >= 3) bonus += 10; + else if(g_lastExtendedCandlePattern.patternStrength >= 2) bonus += 5; + } + // Chart pattern -- direction-aware bonus/penalty + if(g_allPatterns.hasActivePattern) + { + bool bullPattern = PatternSuggestsBullishEntry(); + bool bearPattern = PatternSuggestsBearishEntry(); + // ALIGNED: pattern supports trade direction -> bonus + if(isBullish && bullPattern) + bonus += (g_allPatterns.strongestScore >= 70) ? 20 : 10; + else if(!isBullish && bearPattern) + bonus += (g_allPatterns.strongestScore >= 70) ? 20 : 10; + // OPPOSING: pattern fights trade direction -> PENALTY + if(isBullish && bearPattern) + bonus -= (g_allPatterns.strongestScore >= 70) ? 20 : 10; + else if(!isBullish && bullPattern) + bonus -= (g_allPatterns.strongestScore >= 70) ? 20 : 10; + } + return bonus; +} +//+------------------------------------------------------------------+ +//| Initialize Time Analysis | +//+------------------------------------------------------------------+ +void InitializeTimeAnalysis() +{ + if(!Time_AnalysisEnabled) return; + // Initialize hourly performance array + for(int h = 0; h < 24; h++) + { + g_hourlyPerf[h].hour = h; + g_hourlyPerf[h].totalTrades = 0; + g_hourlyPerf[h].wins = 0; + g_hourlyPerf[h].losses = 0; + g_hourlyPerf[h].winRate = 50.0; + g_hourlyPerf[h].avgRR = 0; + g_hourlyPerf[h].profitFactor = 1.0; + g_hourlyPerf[h].quality = HOUR_AVERAGE; + } + // Set ICT session defaults + // London Open (7-10 UTC) + g_hourlyPerf[7].quality = HOUR_GOOD; + g_hourlyPerf[8].quality = HOUR_EXCELLENT; + g_hourlyPerf[9].quality = HOUR_EXCELLENT; + g_hourlyPerf[10].quality = HOUR_GOOD; + // NY Open (12-15 UTC) + g_hourlyPerf[12].quality = HOUR_GOOD; + g_hourlyPerf[13].quality = HOUR_EXCELLENT; + g_hourlyPerf[14].quality = HOUR_EXCELLENT; + g_hourlyPerf[15].quality = HOUR_GOOD; + // Asian Session - poor for majors + for(int h = 0; h <= 5; h++) + g_hourlyPerf[h].quality = HOUR_POOR; + // Dead zone + g_hourlyPerf[21].quality = HOUR_AVOID; + g_hourlyPerf[22].quality = HOUR_AVOID; + g_hourlyPerf[23].quality = HOUR_AVOID; + g_timeAnalysisReady = true; + if(g_verboseLog) + Print("[OK] Time Analysis initialized"); +} +//+------------------------------------------------------------------+ +//| Get Current Hour Quality | +//+------------------------------------------------------------------+ +ENUM_HOUR_QUALITY GetCurrentHourQuality() +{ + if(!Time_AnalysisEnabled) return HOUR_AVERAGE; + MqlDateTime dt; + TimeToStruct(TimeCurrent(), dt); + return g_hourlyPerf[dt.hour].quality; +} +//+------------------------------------------------------------------+ +//| Get Hour Size Multiplier | +//+------------------------------------------------------------------+ +double GetHourSizeMultiplier() +{ + if(!Time_AnalysisEnabled) return 1.0; + ENUM_HOUR_QUALITY quality = GetCurrentHourQuality(); + switch(quality) + { + case HOUR_EXCELLENT: return Time_BestHourBonus; + case HOUR_GOOD: return Time_BestHourBonus * 0.8; + case HOUR_AVERAGE: return 1.0; + case HOUR_POOR: return Time_WorstHourPenalty; + case HOUR_AVOID: return Time_WorstHourPenalty * 0.5; + default: return 1.0; + } +} +//+==================================================================+ +//| NEWS FILTER SECTION | +//+==================================================================+ +//+------------------------------------------------------------------+ +//| Initialize News Filter | +//+------------------------------------------------------------------+ +void InitializeNewsFilter() +{ + if(!News_FilterEnabled) return; + g_newsFilter.enabled = News_FilterEnabled; + g_newsFilter.avoidHighImpact = News_AvoidHighImpact; // [UNIFIED v6.42] EA_FilterHighImpact is now alias + g_newsFilter.avoidMediumImpact = News_AvoidMediumImpact; // [UNIFIED v6.42] EA_FilterMediumImpact is now alias + // [v6.41] Use EA_* news minutes if EA news filter enabled + g_newsFilter.minutesBeforeHigh = News_MinsBeforeHigh; // [UNIFIED v6.42] single source of truth + g_newsFilter.minutesAfterHigh = News_MinsAfterHigh; // [UNIFIED v6.42] single source of truth + g_newsFilter.minutesBeforeMed = News_MinsBeforeMedium; + g_newsFilter.minutesAfterMed = News_MinsAfterMedium; + g_newsFilter.eventCount = 0; + // [v6.42] Apply ManualUTCOffset if AutoDetect disabled + if(!AutoDetectBrokerOffset && ManualUTCOffset != 0) + { + g_newsFilter.utcOffset = ManualUTCOffset; + } + // [v6.42] News currency filters + g_newsFilter.checkCHF = News_CheckCHF; + g_newsFilter.checkAUD = News_CheckAUD; + g_newsFilter.checkCAD = News_CheckCAD; + g_newsFilter.checkNZD = News_CheckNZD; + g_newsFilter.lastUpdate = 0; + g_newsFilter.isTradingBlocked = false; + g_newsFilter.blockReason = ""; + ArrayResize(g_newsEvents, 0); + g_newsEventCount = 0; + g_newsLastUpdate = 0; + g_newsTradingBlocked = false; + g_newsBlockReason = ""; + if(g_verboseLog) + Print("[OK] News Filter initialized"); + // [v6.42] BacktestShowEquityCurve for tester mode + if(MQLInfoInteger(MQL_TESTER) && BacktestShowEquityCurve) + Print("[CHART] Equity curve display enabled for backtest"); +} +//+------------------------------------------------------------------+ +//| Update News Filter | +//+------------------------------------------------------------------+ +void UpdateNewsFilter() +{ + if(!News_FilterEnabled) return; + // * v9.16 FIX#48: News display with color-coded impact (was stubbed with only HighColor) + if(News_ShowOnChart) + { + for(int ne = 0; ne < g_newsEventCount && ne < 5; ne++) + { + string newsObjName = "ICT_News_" + IntegerToString(ne); + color newsColor = News_HighColor; + // * v9.16 FIX#48: Wire News_LowColor + News_MediumColor (were dead inputs) + if(g_newsEvents[ne].impact == NEWS_LOW) newsColor = News_LowColor; + else if(g_newsEvents[ne].impact == NEWS_MEDIUM) newsColor = News_MediumColor; + else newsColor = News_HighColor; + // Draw vertical line at news time + if(g_newsEvents[ne].eventTime > 0) + { + ObjectCreate(0, newsObjName, OBJ_VLINE, 0, g_newsEvents[ne].eventTime, 0); + ObjectSetInteger(0, newsObjName, OBJPROP_COLOR, newsColor); + ObjectSetInteger(0, newsObjName, OBJPROP_STYLE, STYLE_DOT); + ObjectSetInteger(0, newsObjName, OBJPROP_WIDTH, 1); + ObjectSetInteger(0, newsObjName, OBJPROP_BACK, true); + ObjectSetString(0, newsObjName, OBJPROP_TOOLTIP, + StringFormat("%s [%s] %s", g_newsEvents[ne].title, g_newsEvents[ne].currency, + (g_newsEvents[ne].impact == NEWS_HIGH) ? "HIGH" : (g_newsEvents[ne].impact == NEWS_MEDIUM) ? "MED" : "LOW")); + } + } + } + datetime now = TimeCurrent(); + // Update every 30 minutes + if(now - g_newsLastUpdate < 1800) return; + g_newsLastUpdate = now; + // Check if trading should be blocked + g_newsTradingBlocked = IsNewsBlocking(); + // [v6.42] News_CloseBeforeNews - close open positions when news approaching + if(News_CloseBeforeNews && g_newsTradingBlocked) // [UNIFIED] EA_CloseBeforeNews is now alias // [v6.42] + { + for(int p = PositionsTotal() - 1; p >= 0; p--) + { + if(g_ea_position.SelectByIndex(p) && g_ea_position.Symbol() == _Symbol && g_ea_position.Magic() == EA_MagicNumber) + { + g_ea_trade.PositionClose(g_ea_position.Ticket()); + Print("[v6.42] News close: closed ticket #", g_ea_position.Ticket()); + } + } + } +} +//+------------------------------------------------------------------+ +//| Check if News is Blocking Trading | +//+------------------------------------------------------------------+ +bool IsNewsBlocking() +{ + if(!News_FilterEnabled) return false; + // Simple implementation - can be enhanced with calendar integration + // For now, return false (no blocking) + return g_newsTradingBlocked; +} +//+------------------------------------------------------------------+ +//| Get News Block Reason | +//+------------------------------------------------------------------+ +string GetNewsBlockReason() +{ + return g_newsBlockReason; +} +//+==================================================================+ +//| MARKET REGIME SECTION | +//+==================================================================+ +//+------------------------------------------------------------------+ +//| Initialize Market Regime | +//+------------------------------------------------------------------+ +void InitializeMarketRegime() +{ + // * v6.43 FIX: REMOVED SmartEntry_Enabled check! + // Regime detection is used by SL/TP, trailing, breakeven, structure close, etc. + // It must ALWAYS initialize even if SmartEntry is disabled. + // OLD: if(!SmartEntry_Enabled) return; <- This kept regime stuck at UNKNOWN! + ZeroMemory(g_regimeData); + g_regimeData.regime = REGIME_UNKNOWN; + g_regimeData.prevRegime = REGIME_UNKNOWN; + g_regimeData.confidence = 0; + g_regimeData.trendStrength = 0; + g_regimeData.rangeWidth = 0; + g_regimeData.volatilityRatio = 1.0; + g_regimeData.isExpandingVol = false; + g_regimeData.isContractingVol = false; + g_regimeData.barsInRegime = 0; + g_regimeData.lastChange = 0; + g_regimeData.optimalRR = 2.0; + g_regimeData.confidenceMultiplier = 1.0; + g_regimeData.requiredConfidence = 50; + g_regimeLastUpdate = 0; + g_regimeInitialized = true; + if(g_verboseLog) + Print("[OK] Market Regime Detection initialized"); +} +//+------------------------------------------------------------------+ +//| Detect Market Regime | +//+------------------------------------------------------------------+ +void DetectMarketRegime(const double &high[], const double &low[], + const double &close[], int rates_total) +{ + // * v6.38 FIX ISSUE#5: REMOVED conditional return! + // Regime detection is used by SL/TP, trailing, BE, structure close, and more + // Must always run even if SmartEntry is disabled + if(!g_regimeInitialized) InitializeMarketRegime(); + int lookback = MathMin(g_workingRegime_Lookback, rates_total - 1); + if(lookback < 5) return; + // Calculate trend strength (Directional Efficiency) + double netMove = MathAbs(close[0] - close[lookback]); + double totalMove = 0; + for(int i = 0; i < lookback; i++) + { + totalMove += MathAbs(close[i] - close[i+1]); + } + double efficiency = (totalMove > 0) ? (netMove / totalMove) * 100 : 0; + g_regimeData.trendStrength = efficiency; + // Calculate range width + double highestHigh = high[ArrayMaximum(high, 0, lookback)]; + double lowestLow = low[ArrayMinimum(low, 0, lookback)]; + double avgPrice = (highestHigh + lowestLow) / 2; + g_regimeData.rangeWidth = ((highestHigh - lowestLow) / avgPrice) * 100; + // Calculate volatility ratio + if(g_cachedATR > 0) + { + g_regimeData.volatilityRatio = g_cachedATR / (avgPrice * 0.001); + } + // Determine regime + // * FIX v6.43: CRITICAL - Previous code had GAPS between conditions! + // If efficiency=42, rangeWidth=2.5, volatility<2.2, prevRegime!=RANGING + // -> ALL conditions fail -> regime stuck at UNKNOWN FOREVER + // -> Cascading failures: no reaction relaxation, no regime-adapted R:R/SL/TP + ENUM_MARKET_REGIME newRegime = REGIME_UNKNOWN; + if(efficiency >= (g_workingRegime_TrendThresh > 0 ? g_workingRegime_TrendThresh : Regime_TrendThreshold)) // * FIX#82 TF-aware + { + newRegime = REGIME_TRENDING; + } + else if(efficiency < 40 && g_regimeData.rangeWidth < 2.0) + { + newRegime = REGIME_RANGING; + } + else if(g_regimeData.volatilityRatio >= Regime_VolatileThresh) + { + newRegime = REGIME_VOLATILE; + } + else if(g_regimeData.prevRegime == REGIME_RANGING && efficiency > 35) + { + newRegime = REGIME_BREAKOUT; + } + else + { + // * FIX v6.43: FALLBACK - Never leave regime as UNKNOWN + // Classify by closest match using relaxed thresholds + if(efficiency >= (g_workingRegime_TrendThresh > 0 ? g_workingRegime_TrendThresh : Regime_TrendThreshold) * 0.75) // * FIX#82 + newRegime = REGIME_TRENDING; // Moderate directional movement + else if(g_regimeData.rangeWidth < 3.5) // Wider range tolerance for Gold etc + newRegime = REGIME_RANGING; + else + newRegime = REGIME_VOLATILE; // High range + low efficiency = volatile + if(g_verboseLog) + Print("* v6.43 Regime FALLBACK: eff=", DoubleToString(efficiency, 1), + " range=", DoubleToString(g_regimeData.rangeWidth, 2), + " vol=", DoubleToString(g_regimeData.volatilityRatio, 2), + " -> ", GetRegimeString(newRegime)); + } + // Update regime with confirmation + // * v6.43 FIX: Track pending regime separately! + // OLD BUG: Counter only checked "is different from current" -> + // Bar1: TRENDING (count=1), Bar2: RANGING (count=2) -> falsely confirms RANGING! + // NEW: Only confirm if the SAME new regime persists for ConfirmBars + static ENUM_MARKET_REGIME pendingRegime = REGIME_UNKNOWN; + if(newRegime != g_regimeData.regime) + { + if(newRegime == pendingRegime) + { + g_regimeData.barsInRegime++; // Same pending regime continues + } + else + { + pendingRegime = newRegime; // New candidate regime + g_regimeData.barsInRegime = 1; // Start counting from 1 + } + if(g_regimeData.barsInRegime >= g_workingRegime_ConfirmBars) + { + g_regimeData.prevRegime = g_regimeData.regime; + g_regimeData.regime = newRegime; + g_regimeData.barsInRegime = 0; + pendingRegime = REGIME_UNKNOWN; + g_regimeData.lastChange = TimeCurrent(); + g_regimeData.requiredConfidence = GetRegimeMinConfidence(newRegime); + g_regimeData.optimalRR = GetRegimeOptimalRR(newRegime); + if(g_verboseLog) + Print("[CHART] Market Regime: ", GetRegimeString(newRegime)); + } + } + else + { + g_regimeData.barsInRegime = 0; + pendingRegime = REGIME_UNKNOWN; + } + g_regimeLastUpdate = TimeCurrent(); +} +//+------------------------------------------------------------------+ +//| Get Regime Min Confidence | +//+------------------------------------------------------------------+ +int GetRegimeMinConfidence(ENUM_MARKET_REGIME regime) +{ + switch(regime) + { + case REGIME_TRENDING: return Regime_TrendingMinConf; + case REGIME_RANGING: return Regime_RangingMinConf; + case REGIME_VOLATILE: return Regime_VolatileMinConf; + case REGIME_BREAKOUT: return Regime_BreakoutMinConf; + default: return 65; + } +} +//+------------------------------------------------------------------+ +//| Get Regime Optimal RR | +//+------------------------------------------------------------------+ +double GetRegimeOptimalRR(ENUM_MARKET_REGIME regime) +{ + switch(regime) + { + case REGIME_TRENDING: return 3.0; + case REGIME_RANGING: return 1.5; + case REGIME_VOLATILE: return 2.5; + case REGIME_BREAKOUT: return 3.5; + default: return 2.0; + } +} +//+==================================================================+ +//| DIVERGENCE SECTION | +//+==================================================================+ +//+==================================================================+ +//| DIVERGENCE HELPER FUNCTIONS | +//+==================================================================+ +int FindSwingHighs(const double &high[], int lookback, int minBars) +{ + int count = 0; + ArrayInitialize(g_swingHighs, 0); + ArrayInitialize(g_swingHighBars, -1); + for(int i = minBars; i < lookback - minBars && count < 50; i++) + { + bool isSwingHigh = true; + for(int j = 1; j <= minBars; j++) + { + if(high[i] <= high[i-j] || high[i] <= high[i+j]) + { + isSwingHigh = false; + break; + } + } + if(isSwingHigh) + { + g_swingHighs[count] = high[i]; + g_swingHighBars[count] = i; + count++; + } + } + return count; +} +int FindSwingLows(const double &low[], int lookback, int minBars) +{ + int count = 0; + ArrayInitialize(g_swingLows, 0); + ArrayInitialize(g_swingLowBars, -1); + for(int i = minBars; i < lookback - minBars && count < 50; i++) + { + bool isSwingLow = true; + for(int j = 1; j <= minBars; j++) + { + if(low[i] >= low[i-j] || low[i] >= low[i+j]) + { + isSwingLow = false; + break; + } + } + if(isSwingLow) + { + g_swingLows[count] = low[i]; + g_swingLowBars[count] = i; + count++; + } + } + return count; +} +bool DivergenceExists(datetime t1, datetime t2, ENUM_DIVERGENCE_TYPE type) +{ + // * v7.9 FIX: Was using bar1/bar2 indices which shift +1 every new bar. + // This caused the same divergence to be re-created every bar because + // DivergenceExists(5,10,...) != DivergenceExists(6,11,...) after 1 new bar. + // Result: 50-100 "CONFIRMED" prints for the same divergence in the log. + // Fix: compare datetimes (time[bar1], time[bar2]) -- stable across new bars. + for(int i = 0; i < g_divergenceCount; i++) + { + if(!g_divergences[i].active) continue; + if(g_divergences[i].type != type) continue; + if(g_divergences[i].time1 == t1 && g_divergences[i].time2 == t2) + return true; + } + return false; +} +string GetDivergenceTypeString(ENUM_DIVERGENCE_TYPE type) +{ + switch(type) + { + case DIV_REGULAR_BULLISH: return "BULL DIV"; + case DIV_REGULAR_BEARISH: return "BEAR DIV"; + case DIV_HIDDEN_BULLISH: return "HIDDEN BULL"; + case DIV_HIDDEN_BEARISH: return "HIDDEN BEAR"; + default: return "NONE"; + } +} +double CalculateDivergenceScore(const DivergenceStruct &div) +{ + double score = 0; + // Base score by type + if(div.type == DIV_REGULAR_BULLISH || div.type == DIV_REGULAR_BEARISH) + score += 50; + else + score += 35; + // Strength bonus + if(div.strength == DIV_STRONG) score += 30; + else if(div.strength == DIV_MODERATE) score += 20; + else score += 10; + // RSI extreme bonus + bool isBullish = (div.type == DIV_REGULAR_BULLISH || div.type == DIV_HIDDEN_BULLISH); + if(isBullish && div.rsi1 < 30) score += 15; + if(!isBullish && div.rsi1 > 70) score += 15; + // Indicator divergence amount + double indDiff = MathAbs(div.rsi1 - div.rsi2); + if(indDiff > 20) score += 15; + else if(indDiff > 10) score += 10; + else if(indDiff > 5) score += 5; + return MathMin(score, 100); +} +//+------------------------------------------------------------------+ +//| Detect Divergences - ENHANCED | +//+------------------------------------------------------------------+ +void DetectDivergences(const datetime &time[], const double &high[], + const double &low[], const double &close[], int limit) +{ + if(!Divergence_Enabled) return; + if(g_rsiDivergenceHandle == INVALID_HANDLE) return; + // Copy RSI values + double rsi[]; + ArraySetAsSeries(rsi, true); + int lookback = MathMin(g_workingDivergence_Lookback, limit); + if(lookback < 10) return; + int copied = CopyBuffer(g_rsiDivergenceHandle, 0, 0, lookback + 10, rsi); + if(copied < lookback) return; + int minBars = Divergence_MinBars; + // Find swing pivots + int highCount = FindSwingHighs(high, lookback, minBars); + int lowCount = FindSwingLows(low, lookback, minBars); + // Reset global flags + g_bullishDivergence = false; + g_bearishDivergence = false; + //=== DETECT BULLISH DIVERGENCES (from swing lows) === + if(lowCount >= 2) + { + for(int i = 0; i < lowCount - 1; i++) + { + int bar1 = g_swingLowBars[i]; + int bar2 = g_swingLowBars[i+1]; + if(bar1 < 0 || bar2 < 0 || bar2 - bar1 < minBars) continue; + double price1 = g_swingLows[i]; + double price2 = g_swingLows[i+1]; + double rsi1 = rsi[bar1]; + double rsi2 = rsi[bar2]; + // Regular Bullish: Price LL, RSI HL + if(price1 < price2 && rsi1 > rsi2) + { + if(!DivergenceExists(time[bar1], time[bar2], DIV_REGULAR_BULLISH)) + { + CreateDivergence(DIV_REGULAR_BULLISH, time, high, low, + bar1, bar2, price1, price2, rsi1, rsi2); + g_bullishDivergence = true; + } + } + // Hidden Bullish: Price HL, RSI LL + if(Divergence_DetectHidden && price1 > price2 && rsi1 < rsi2) + { + if(!DivergenceExists(time[bar1], time[bar2], DIV_HIDDEN_BULLISH)) + { + CreateDivergence(DIV_HIDDEN_BULLISH, time, high, low, + bar1, bar2, price1, price2, rsi1, rsi2); + g_bullishDivergence = true; + } + } + } + } + //=== DETECT BEARISH DIVERGENCES (from swing highs) === + if(highCount >= 2) + { + for(int i = 0; i < highCount - 1; i++) + { + int bar1 = g_swingHighBars[i]; + int bar2 = g_swingHighBars[i+1]; + if(bar1 < 0 || bar2 < 0 || bar2 - bar1 < minBars) continue; + double price1 = g_swingHighs[i]; + double price2 = g_swingHighs[i+1]; + double rsi1 = rsi[bar1]; + double rsi2 = rsi[bar2]; + // Regular Bearish: Price HH, RSI LH + if(price1 > price2 && rsi1 < rsi2) + { + if(!DivergenceExists(time[bar1], time[bar2], DIV_REGULAR_BEARISH)) + { + CreateDivergence(DIV_REGULAR_BEARISH, time, high, low, + bar1, bar2, price1, price2, rsi1, rsi2); + g_bearishDivergence = true; + } + } + // Hidden Bearish: Price LH, RSI HH + if(Divergence_DetectHidden && price1 < price2 && rsi1 > rsi2) + { + if(!DivergenceExists(time[bar1], time[bar2], DIV_HIDDEN_BEARISH)) + { + CreateDivergence(DIV_HIDDEN_BEARISH, time, high, low, + bar1, bar2, price1, price2, rsi1, rsi2); + g_bearishDivergence = true; + } + } + } + } + ArrayFree(rsi); +} +//+------------------------------------------------------------------+ +//| Create New Divergence | +//+------------------------------------------------------------------+ +void CreateDivergence(ENUM_DIVERGENCE_TYPE type, const datetime &time[], + const double &high[], const double &low[], + int bar1, int bar2, double price1, double price2, + double rsi1, double rsi2) +{ + // Find slot + int idx = -1; + for(int i = 0; i < g_maxDivergences; i++) + { + if(!g_divergences[i].active) + { + idx = i; + break; + } + } + if(idx == -1) + { + // Remove oldest + idx = 0; + DeleteDivergenceObjects(g_divergences[0]); + } + g_divIdCounter++; + // Fill structure + ZeroMemory(g_divergences[idx]); + g_divergences[idx].id = g_divIdCounter; + g_divergences[idx].type = type; + g_divergences[idx].indicator = DIV_IND_RSI; + g_divergences[idx].price1 = price1; + g_divergences[idx].price2 = price2; + g_divergences[idx].bar1 = bar1; + g_divergences[idx].bar2 = bar2; + g_divergences[idx].time1 = time[bar1]; + g_divergences[idx].time2 = time[bar2]; + g_divergences[idx].rsi1 = rsi1; + g_divergences[idx].rsi2 = rsi2; + // Calculate strength + double priceDiff = MathAbs(price1 - price2) / g_pipValue; + double indDiff = MathAbs(rsi1 - rsi2); + g_divergences[idx].priceMove = priceDiff; + g_divergences[idx].indMove = indDiff; + if(indDiff > 15 && priceDiff > 30) + g_divergences[idx].strength = DIV_STRONG; + else if(indDiff > 8 && priceDiff > 15) + g_divergences[idx].strength = DIV_MODERATE; + else + g_divergences[idx].strength = DIV_WEAK; + // Calculate score + g_divergences[idx].score = CalculateDivergenceScore(g_divergences[idx]); + // Status + g_divergences[idx].active = true; + g_divergences[idx].confirmed = false; + g_divergences[idx].broken = false; + g_divergences[idx].triggered = false; + g_divergences[idx].createdTime = TimeCurrent(); + // * v10.01 FIX#248: TF-aware divergence expiry. + // OLD: expiryTime = TimeCurrent() + PeriodSeconds(_Period) * 20 for ALL TFs. + // H4: 240min × 20 = 4800min = 3.3 DAYS → divergence stays active across entire week. + // Result: [SYNC] HIDDEN BULL Score:75 RSI:63.4→78.7 printed identically for 4+ days + // (same swing bars t1/t2 → DivergenceExists check prevents recreation → no new log). + // The divergence IS active for 3+ days — on H4 that means it was stale by day 2. + // FIX: TF-proportional expiry (short TFs need more bars to mature, HTF divergences resolve faster): + // M5/M15: 20 candles (original — quick signals, need time to play out) + // H1: 10 candles (10h — intraday, resolves within same day) + // H4: 5 candles (20h — one trading day, H4 div resolves in 1-2 days max) + // D1: 3 candles ( 3 days — multi-day divergences lose relevance faster) + { + int _divExpCandles; + if(_Period >= PERIOD_D1) _divExpCandles = 3; + else if(_Period >= PERIOD_H4) _divExpCandles = 5; // * FIX#248: was 20 (3.3 days) + else if(_Period >= PERIOD_H1) _divExpCandles = 10; + else _divExpCandles = 20; // M5/M15: keep original + g_divergences[idx].expiryTime = TimeCurrent() + PeriodSeconds(_Period) * _divExpCandles; + } + // Calculate entry levels + bool isBullish = (type == DIV_REGULAR_BULLISH || type == DIV_HIDDEN_BULLISH); + if(isBullish) + { + g_divergences[idx].entryPrice = price1 + g_cachedATR * 0.2; + g_divergences[idx].stopLoss = price1 - g_cachedATR * Divergence_ExpiryATR; + g_divergences[idx].takeProfit = g_divergences[idx].entryPrice + + (g_divergences[idx].entryPrice - g_divergences[idx].stopLoss) * 2.5; + } + else + { + g_divergences[idx].entryPrice = price1 - g_cachedATR * 0.2; + g_divergences[idx].stopLoss = price1 + g_cachedATR * Divergence_ExpiryATR; + g_divergences[idx].takeProfit = g_divergences[idx].entryPrice - + (g_divergences[idx].stopLoss - g_divergences[idx].entryPrice) * 2.5; + } + // Update count + if(idx >= g_divergenceCount) g_divergenceCount = idx + 1; + // Draw on chart + if(Divergence_ShowOnChart) + { + DrawDivergenceEnhanced(g_divergences[idx], time, high, low); + } + // Log + string typeStr = GetDivergenceTypeString(type); + string strengthStr = (g_divergences[idx].strength == DIV_STRONG) ? "STRONG" : + (g_divergences[idx].strength == DIV_MODERATE) ? "MODERATE" : "WEAK"; + Print("[SYNC] ", typeStr, " [", strengthStr, "] Score: ", + DoubleToString(g_divergences[idx].score, 0), + " RSI: ", DoubleToString(rsi1, 1), "->", DoubleToString(rsi2, 1)); +} +//+------------------------------------------------------------------+ +//| Cleanup Divergences | +//+------------------------------------------------------------------+ +void CleanupDivergences() +{ + ObjectsDeleteAll(0, "DIV_"); + ArrayFree(g_divergences); + ArrayFree(g_swingHighs); + ArrayFree(g_swingLows); + ArrayFree(g_swingHighBars); + ArrayFree(g_swingLowBars); + g_divergenceCount = 0; + g_divIdCounter = 0; + g_bullishDivergence = false; + g_bearishDivergence = false; +} +//+==================================================================+ +//| TRENDLINE SECTION | +//+==================================================================+ +//+------------------------------------------------------------------+ +//| Get Trendline Strength String | +//+------------------------------------------------------------------+ +string GetTrendlineStrengthString(ENUM_TRENDLINE_STRENGTH strength) +{ + switch(strength) + { + case TL_VERY_STRONG: return "****"; + case TL_STRONG: return "***"; + case TL_MODERATE: return "**"; + case TL_WEAK: return "*"; + default: return ""; + } +} +//+------------------------------------------------------------------+ +//| Calculate Trendline Strength | +//+------------------------------------------------------------------+ +ENUM_TRENDLINE_STRENGTH CalculateTrendlineStrength(const TrendlineStruct &tl) +{ + int strengthPoints = 0; + // Touches bonus + if(tl.touches >= 5) strengthPoints += 3; + else if(tl.touches >= 4) strengthPoints += 2; + else if(tl.touches >= 3) strengthPoints += 1; + // Age bonus + int age = tl.endBar - tl.startBar; + if(age > 50) strengthPoints += 2; + else if(age > 30) strengthPoints += 1; + // Angle bonus + double absAngle = MathAbs(tl.angle); + if(absAngle >= 20 && absAngle <= 45) strengthPoints += 2; + else if(absAngle >= 10 && absAngle <= 60) strengthPoints += 1; + if(strengthPoints >= 6) return TL_VERY_STRONG; + else if(strengthPoints >= 4) return TL_STRONG; + else if(strengthPoints >= 2) return TL_MODERATE; + else return TL_WEAK; +} +//+------------------------------------------------------------------+ +//| Calculate Trendline Score | +//+------------------------------------------------------------------+ +double CalculateTrendlineScore(const TrendlineStruct &tl) +{ + double score = 0; + // Touches (max 30) + score += MathMin(tl.touches * 6, 30); + // Strength (max 25) + if(tl.strength == TL_VERY_STRONG) score += 25; + else if(tl.strength == TL_STRONG) score += 20; + else if(tl.strength == TL_MODERATE) score += 12; + else score += 5; + // Age (max 20) + int age = tl.endBar - tl.startBar; + if(age > 100) score += 20; + else if(age > 50) score += 15; + else if(age > 30) score += 10; + else if(age > 15) score += 5; + // Angle quality (max 15) + double absAngle = MathAbs(tl.angle); + if(absAngle >= 25 && absAngle <= 40) score += 15; + else if(absAngle >= 15 && absAngle <= 50) score += 10; + else if(absAngle >= 10 && absAngle <= 60) score += 5; + // Proximity (max 10) + double dist = MathAbs(SymbolInfoDouble(_Symbol, SYMBOL_BID) - tl.currentPrice); + double distATR = (g_cachedATR > 0) ? dist / g_cachedATR : 0; + if(distATR < 0.5) score += 10; + else if(distATR < 1.0) score += 7; + else if(distATR < 2.0) score += 4; + return MathMin(score, 100); +} +//+------------------------------------------------------------------+ +//| Find Weakest Trendline | +//+------------------------------------------------------------------+ +int FindWeakestTrendline() +{ + int weakestIdx = 0; + double lowestScore = 999; + for(int i = 0; i < g_trendlineCount; i++) + { + if(!g_trendlines[i].active) return i; + if(g_trendlines[i].score < lowestScore) + { + lowestScore = g_trendlines[i].score; + weakestIdx = i; + } + } + return weakestIdx; +} +//+------------------------------------------------------------------+ +//| Check if Similar Trendline Exists | +//+------------------------------------------------------------------+ +bool TrendlineExistsSimilar(double price, double slope, ENUM_TRENDLINE_TYPE type) +{ + double priceTol = g_cachedATR * 0.5; + double slopeTol = g_cachedATR * 0.001; + for(int i = 0; i < g_trendlineCount; i++) + { + if(!g_trendlines[i].active || g_trendlines[i].type != type) continue; + if(MathAbs(g_trendlines[i].startPrice - price) < priceTol && + MathAbs(g_trendlines[i].slope - slope) < slopeTol) + return true; + } + return false; +} +//+------------------------------------------------------------------+ +//| Count Trendline Touches | +//+------------------------------------------------------------------+ +int CountTrendlineTouches(const double &price[], int bar1, int bar2, + double startPrice, double slope, double tolerance, bool isSupport) +{ + int touches = 2; + for(int i = bar1 + 1; i < bar2; i++) + { + double tlPrice = startPrice + slope * (i - bar1); + double diff = isSupport ? (price[i] - tlPrice) : (tlPrice - price[i]); + if(MathAbs(diff) <= tolerance) + touches++; + } + return touches; +} +//+------------------------------------------------------------------+ +//| Delete Trendline Objects | +//+------------------------------------------------------------------+ +void DeleteTrendlineObjectsEnhanced(TrendlineStruct &tl) +{ + // Delete main trendline + ObjectDelete(0, tl.objName); + // Delete all related objects + ObjectDelete(0, tl.objName + "_Label"); + ObjectDelete(0, tl.objName + "_Break"); + ObjectDelete(0, tl.objName + "_Retest"); + ObjectDelete(0, tl.objName + "_Touch"); + ObjectDelete(0, tl.objName + "_Strength"); + tl.objName = ""; +} +//+------------------------------------------------------------------+ +//| Detect Trendlines - ENHANCED | +//+------------------------------------------------------------------+ +void DetectTrendlines(const datetime &time[], const double &high[], + const double &low[], int limit) +{ + if(!Trendline_Enabled) return; + int lookback = MathMin(g_workingTrendline_Lookback, limit); + if(lookback < 20) return; + double tolerance = Trendline_Tolerance * g_cachedATR; + int minTouches = Trendline_MinTouches; + //=== FIND SWING LOWS FOR SUPPORT TRENDLINES === + int lowBars[]; + double lowPrices[]; + ArrayResize(lowBars, 0); + ArrayResize(lowPrices, 0); + for(int i = 3; i < lookback - 3; i++) + { + if(low[i] < low[i-1] && low[i] < low[i-2] && low[i] < low[i-3] && + low[i] < low[i+1] && low[i] < low[i+2] && low[i] < low[i+3]) + { + int size = ArraySize(lowBars); + ArrayResize(lowBars, size + 1); + ArrayResize(lowPrices, size + 1); + lowBars[size] = i; + lowPrices[size] = low[i]; + } + } + //=== CREATE SUPPORT TRENDLINES === + int lowCount = ArraySize(lowBars); + if(lowCount >= 2) + { + for(int i = 0; i < lowCount - 1 && g_trendlineCount < g_maxTrendlines; i++) + { + for(int j = i + 1; j < lowCount; j++) + { + int bar1 = lowBars[j]; // Earlier bar (larger index) + int bar2 = lowBars[i]; // Later bar (smaller index) + double price1 = lowPrices[j]; + double price2 = lowPrices[i]; + if(bar1 - bar2 < 5) continue; + double slope = (price2 - price1) / (bar1 - bar2); + // Only upward sloping for support + if(slope <= 0) continue; + int touches = CountTrendlineTouches(low, bar2, bar1, price2, slope, tolerance, true); + if(touches >= minTouches && !TrendlineExistsSimilar(price1, slope, TL_SUPPORT)) + { + CreateTrendlineEnhanced(TL_SUPPORT, time, high, low, + bar1, bar2, price1, price2, slope, touches); + break; // One trendline per starting point + } + } + } + } + //=== FIND SWING HIGHS FOR RESISTANCE TRENDLINES === + int highBars[]; + double highPrices[]; + ArrayResize(highBars, 0); + ArrayResize(highPrices, 0); + for(int i = 3; i < lookback - 3; i++) + { + if(high[i] > high[i-1] && high[i] > high[i-2] && high[i] > high[i-3] && + high[i] > high[i+1] && high[i] > high[i+2] && high[i] > high[i+3]) + { + int size = ArraySize(highBars); + ArrayResize(highBars, size + 1); + ArrayResize(highPrices, size + 1); + highBars[size] = i; + highPrices[size] = high[i]; + } + } + //=== CREATE RESISTANCE TRENDLINES === + int highCount = ArraySize(highBars); + if(highCount >= 2) + { + for(int i = 0; i < highCount - 1 && g_trendlineCount < g_maxTrendlines; i++) + { + for(int j = i + 1; j < highCount; j++) + { + int bar1 = highBars[j]; + int bar2 = highBars[i]; + double price1 = highPrices[j]; + double price2 = highPrices[i]; + if(bar1 - bar2 < 5) continue; + double slope = (price2 - price1) / (bar1 - bar2); + // Only downward sloping for resistance + if(slope >= 0) continue; + int touches = CountTrendlineTouches(high, bar2, bar1, price2, slope, tolerance, false); + if(touches >= minTouches && !TrendlineExistsSimilar(price1, slope, TL_RESISTANCE)) + { + CreateTrendlineEnhanced(TL_RESISTANCE, time, high, low, + bar1, bar2, price1, price2, slope, touches); + break; + } + } + } + } + // Draw + if(Trendline_ShowOnChart) DrawTrendlinesOnChart(time); + // Cleanup + ArrayFree(lowBars); + ArrayFree(lowPrices); + ArrayFree(highBars); + ArrayFree(highPrices); +} +//+------------------------------------------------------------------+ +//| Create Trendline - ENHANCED | +//+------------------------------------------------------------------+ +void CreateTrendlineEnhanced(ENUM_TRENDLINE_TYPE type, const datetime &time[], + const double &high[], const double &low[], + int bar1, int bar2, double price1, double price2, + double slope, int touches) +{ + // Find slot + int idx = -1; + for(int i = 0; i < g_maxTrendlines; i++) + { + if(!g_trendlines[i].active) + { + idx = i; + break; + } + } + if(idx == -1) + { + idx = FindWeakestTrendline(); + DeleteTrendlineObjectsEnhanced(g_trendlines[idx]); + } + g_tlIdCounter++; + // Fill structure + ZeroMemory(g_trendlines[idx]); + g_trendlines[idx].id = g_tlIdCounter; + g_trendlines[idx].type = type; + g_trendlines[idx].status = TL_STATUS_ACTIVE; + g_trendlines[idx].startPrice = price1; + g_trendlines[idx].endPrice = price2; + g_trendlines[idx].startBar = bar1; + g_trendlines[idx].endBar = bar2; + g_trendlines[idx].startTime = time[bar1]; + g_trendlines[idx].endTime = time[bar2]; + g_trendlines[idx].slope = slope; + // * FIX#126 (Mar 06 2026): Old: atan(slope/pipValue) -> gave ~89° for indices (US500~6000, + // pipValue=0.01 -> slope/pipValue >> 1 -> atan saturation). Meaningless for display/filters. + // New: Normalize by ATR so that a line moving 1 ATR over 14 bars = 45°. + // This makes angle symbol-agnostic and visually meaningful. + { + double atr_ref126 = (g_cachedATR > 0) ? g_cachedATR : (g_pipValue * 100.0); + double slope_norm126 = slope * 14.0 / atr_ref126; // 14 = ATR reference period + g_trendlines[idx].angle = MathArctan(slope_norm126) * 180.0 / M_PI; + } + // Current price projection + g_trendlines[idx].currentPrice = price1 + slope * bar1; + // Zone + double zoneTol = g_cachedATR * Trendline_Tolerance; + g_trendlines[idx].zoneTop = g_trendlines[idx].currentPrice + zoneTol; + g_trendlines[idx].zoneBottom = g_trendlines[idx].currentPrice - zoneTol; + g_trendlines[idx].touches = touches; + g_trendlines[idx].lastTouchTime = time[bar2]; + g_trendlines[idx].broken = false; + g_trendlines[idx].retested = false; + g_trendlines[idx].retestCount = 0; + // Calculate strength & score + g_trendlines[idx].strength = CalculateTrendlineStrength(g_trendlines[idx]); + g_trendlines[idx].score = CalculateTrendlineScore(g_trendlines[idx]); + // Entry levels + bool isSupport = (type == TL_SUPPORT); + double tlPrice = g_trendlines[idx].currentPrice; + if(isSupport) + { + g_trendlines[idx].entryPrice = tlPrice + g_cachedATR * 0.1; + g_trendlines[idx].stopLoss = tlPrice - g_cachedATR * 1.0; + g_trendlines[idx].takeProfit = tlPrice + g_cachedATR * 3.0; + } + else + { + g_trendlines[idx].entryPrice = tlPrice - g_cachedATR * 0.1; + g_trendlines[idx].stopLoss = tlPrice + g_cachedATR * 1.0; + g_trendlines[idx].takeProfit = tlPrice - g_cachedATR * 3.0; + } + // Visual name + g_trendlines[idx].objName = "TL_" + (isSupport ? "BULL_" : "BEAR_") + IntegerToString(g_tlIdCounter); + // Status + g_trendlines[idx].active = true; + g_trendlines[idx].createdTime = TimeCurrent(); + g_trendlines[idx].expiryTime = TimeCurrent() + PeriodSeconds(_Period) * g_workingTrendline_MaxAge; + // Update count + if(idx >= g_trendlineCount) g_trendlineCount = idx + 1; + // Log + string typeStr = isSupport ? "SUPPORT" : "RESISTANCE"; + string strengthStr = GetTrendlineStrengthString(g_trendlines[idx].strength); + Print("[UP] ", typeStr, " Trendline [", strengthStr, "] ", + "Touches: ", touches, " Score: ", DoubleToString(g_trendlines[idx].score, 0), + " Angle: ", DoubleToString(g_trendlines[idx].angle, 1), "deg"); +} +//+------------------------------------------------------------------+ +//| Check Trendline Breaks | +//+------------------------------------------------------------------+ +void CheckTrendlineBreaks(const double &high[], const double &low[]) +{ + if(!Trendline_Enabled) return; + g_trendlineBreakBull = false; + g_trendlineBreakBear = false; + for(int i = 0; i < g_trendlineCount; i++) + { + if(!g_trendlines[i].active || g_trendlines[i].broken) continue; + double tlPrice = g_trendlines[i].endPrice + + g_trendlines[i].slope * g_trendlines[i].endBar; + // Check for break + if(g_trendlines[i].type == 1 && low[0] < tlPrice) + { + g_trendlines[i].broken = true; + g_trendlineBreakBear = true; + } + else if(g_trendlines[i].type == -1 && high[0] > tlPrice) + { + g_trendlines[i].broken = true; + g_trendlineBreakBull = true; + } + } +} +//+------------------------------------------------------------------+ +//| Draw Trendlines on Chart | +//+------------------------------------------------------------------+ +void DrawTrendlinesOnChart(const datetime &time[]) +{ + for(int i = 0; i < g_trendlineCount; i++) + { + if(!g_trendlines[i].active) continue; + string name = g_trendlines[i].objName; + color tlColor = (g_trendlines[i].type == 1) ? Trendline_BullColor : Trendline_BearColor; + if(g_trendlines[i].broken) + tlColor = Trendline_BreakColor; + ObjectDelete(0, name); + ObjectCreate(0, name, OBJ_TREND, 0, + g_trendlines[i].startTime, g_trendlines[i].startPrice, + g_trendlines[i].endTime, g_trendlines[i].endPrice); + ObjectSetInteger(0, name, OBJPROP_COLOR, tlColor); + ObjectSetInteger(0, name, OBJPROP_WIDTH, 2); + ObjectSetInteger(0, name, OBJPROP_RAY_RIGHT, true); + // [v6.42] Trendline_ShowZones - draw zone area around trendline + if(Trendline_ShowZones && g_cachedATR > 0) + { + string zoneName = name + "_Zone"; + if(ObjectFind(0, zoneName) < 0) + ObjectCreate(0, zoneName, OBJ_TREND, 0, + g_trendlines[i].startTime, g_trendlines[i].startPrice + g_cachedATR * 0.2, + g_trendlines[i].endTime, g_trendlines[i].endPrice + g_cachedATR * 0.2); + ObjectSetInteger(0, zoneName, OBJPROP_COLOR, tlColor); + ObjectSetInteger(0, zoneName, OBJPROP_STYLE, STYLE_DOT); + ObjectSetInteger(0, zoneName, OBJPROP_WIDTH, 1); + } + } +} +//+------------------------------------------------------------------+ +//| Cleanup Trendlines - ENHANCED | +//+------------------------------------------------------------------+ +void CleanupTrendlines() +{ + // Delete all trendline objects + ObjectsDeleteAll(0, "TL_"); + // Free array + ArrayFree(g_trendlines); + // Reset counters + g_trendlineCount = 0; + g_tlIdCounter = 0; + // Reset all flags + g_tlSupportActive = false; + g_tlResistanceActive = false; + g_tlBreakBullish = false; + g_tlBreakBearish = false; + g_tlRetestBullish = false; + g_tlRetestBearish = false; + g_bullTrendlineActive = false; + g_bearTrendlineActive = false; + g_trendlineBreakBull = false; + g_trendlineBreakBear = false; +} +void InitializeCRT() +{ + ArrayResize(g_crtSetups, 0); + g_crtCount = 0; + g_hasCRT = false; + ZeroMemory(g_currentCRT); +} +void DetectCRTSetups(const datetime &time[], const double &open[], + const double &high[], const double &low[], const double &close[]) +{ + if(!CRT_Enabled) return; + int lookback = MathMin(g_workingCRT_Lookback, ArraySize(time) - 10); + for(int i = 1; i < lookback; i++) + { + double candleRange = high[i] - low[i]; + double rangeInATR = (g_cachedATR > 0) ? candleRange / g_cachedATR : 0; + if(rangeInATR < (g_workingCRT_MinRangeATR > 0 ? g_workingCRT_MinRangeATR : CRT_MinRangeATR) || + rangeInATR > (g_workingCRT_MaxRangeATR > 0 ? g_workingCRT_MaxRangeATR : CRT_MaxRangeATR)) continue; // * FIX#82 + double bodySize = MathAbs(close[i] - open[i]); + double wickRatio = candleRange > 0 ? bodySize / candleRange : 0; + if(wickRatio < 0.3) continue; + bool brokeHigh = (i > 0 && high[i-1] > high[i]); + bool brokeLow = (i > 0 && low[i-1] < low[i]); + if(!brokeHigh && !brokeLow) continue; + // [v6.42] CRT_RequireBreak filter + if(CRT_RequireBreak && !brokeHigh && !brokeLow) continue; + // [v6.42] CRT_RequireRetrace filter + if(CRT_RequireRetrace) + { + double retrace = 0; + if(brokeHigh) retrace = (high[i] - close[i-1]) / candleRange; + else retrace = (close[i-1] - low[i]) / candleRange; + if(retrace < 0.25) continue; // Need at least 25% retrace + } + // [v6.42] CRT_RequireConfluence + CRT_MinConfluence + if(CRT_RequireConfluence) + { + int crtConf = 0; + int tmpIdx; + double midP = (high[i] + low[i]) / 2.0; + if(IsPriceInFVG(midP, tmpIdx, true) || IsPriceInFVG(midP, tmpIdx, false)) crtConf++; + if(IsPriceNearOB(midP, tmpIdx, true) || IsPriceNearOB(midP, tmpIdx, false)) crtConf++; + if(crtConf < CRT_MinConfluence) continue; + } + ENUM_CRT_TYPE crtType = CRT_NEUTRAL; + if(brokeHigh && !brokeLow) crtType = CRT_BULLISH; + else if(brokeLow && !brokeHigh) crtType = CRT_BEARISH; + else continue; + bool exists = false; + for(int j = 0; j < MathMin(g_crtCount, ArraySize(g_crtSetups)); j++) + { + if(g_crtSetups[j].rangeTime == time[i]) { exists = true; break; } + } + if(exists) continue; + CRTSetup newCRT; + ZeroMemory(newCRT); + newCRT.id = g_crtCount; + newCRT.objName = "CRT_" + IntegerToString(g_crtCount); + newCRT.rangeTime = time[i]; + newCRT.rangeBar = i; + newCRT.rangeHigh = high[i]; + newCRT.rangeLow = low[i]; + newCRT.rangeOpen = open[i]; + newCRT.rangeClose = close[i]; + newCRT.rangeSize = candleRange; + newCRT.rangeATR = g_cachedATR; + newCRT.rangeSizeATR = rangeInATR; + if(crtType == CRT_BULLISH) + { + newCRT.projectionUp = high[i] + candleRange * CRT_ProjectionMultiplier; + newCRT.projectionDown = low[i]; + newCRT.projection50Up = high[i] + candleRange * 0.5 * CRT_ProjectionMultiplier; + newCRT.extensionUp = high[i] + candleRange * CRT_ExtensionLevel; + newCRT.entryPrice = (high[i] + low[i]) / 2; + newCRT.stopLoss = low[i] - g_cachedATR * 0.2; + newCRT.takeProfit1 = newCRT.projection50Up; + newCRT.takeProfit2 = newCRT.projectionUp; + newCRT.takeProfit3 = newCRT.extensionUp; + } + else + { + newCRT.projectionDown = low[i] - candleRange * CRT_ProjectionMultiplier; + newCRT.projectionUp = high[i]; + newCRT.projection50Down = low[i] - candleRange * 0.5 * CRT_ProjectionMultiplier; + newCRT.extensionDown = low[i] - candleRange * CRT_ExtensionLevel; + newCRT.entryPrice = (high[i] + low[i]) / 2; + newCRT.stopLoss = high[i] + g_cachedATR * 0.2; + newCRT.takeProfit1 = newCRT.projection50Down; + newCRT.takeProfit2 = newCRT.projectionDown; + newCRT.takeProfit3 = newCRT.extensionDown; + } + double slDistance = MathAbs(newCRT.entryPrice - newCRT.stopLoss); + double tpDistance = MathAbs(newCRT.takeProfit2 - newCRT.entryPrice); + newCRT.riskReward = (slDistance > 0) ? tpDistance / slDistance : 0; + newCRT.type = crtType; + newCRT.status = CRT_CONFIRMED; + newCRT.breakConfirmed = true; + newCRT.breakTime = time[i-1]; + newCRT.breakBar = i - 1; + if(rangeInATR >= 1.5) newCRT.quality = CRT_QUALITY_A; + else if(rangeInATR >= 1.2) newCRT.quality = CRT_QUALITY_B; + else if(rangeInATR >= 0.9) newCRT.quality = CRT_QUALITY_C; + else newCRT.quality = CRT_QUALITY_D; + newCRT.active = true; + newCRT.createdTime = TimeCurrent(); + newCRT.lastUpdate = TimeCurrent(); + ArrayResize(g_crtSetups, g_crtCount + 1); + g_crtSetups[g_crtCount] = newCRT; + g_crtCount++; + g_currentCRT = newCRT; + g_hasCRT = true; + } + UpdateCRTSetups(time, high, low, close); +} +void UpdateCRTSetups(const datetime &time[], const double &high[], + const double &low[], const double &close[]) +{ + double currentPrice = close[0]; + for(int i = 0; i < MathMin(g_crtCount, ArraySize(g_crtSetups)); i++) + { + if(!g_crtSetups[i].active) continue; + g_crtSetups[i].barsActive++; + if(g_crtSetups[i].barsActive > g_workingCRT_Expiry) + { + g_crtSetups[i].status = CRT_EXPIRED; + g_crtSetups[i].active = false; + continue; + } + if(g_crtSetups[i].type == CRT_BULLISH) + { + double favorable = high[0] - g_crtSetups[i].entryPrice; + double adverse = g_crtSetups[i].entryPrice - low[0]; + if(favorable > g_crtSetups[i].maxFavorable) g_crtSetups[i].maxFavorable = favorable; + if(adverse > g_crtSetups[i].maxAdverse) g_crtSetups[i].maxAdverse = adverse; + if(currentPrice >= g_crtSetups[i].takeProfit2) + { + g_crtSetups[i].status = CRT_TRIGGERED; + g_crtSetups[i].active = false; + } + else if(currentPrice < g_crtSetups[i].stopLoss) + { + g_crtSetups[i].status = CRT_INVALIDATED; + g_crtSetups[i].active = false; + } + } + else + { + double favorable = g_crtSetups[i].entryPrice - low[0]; + double adverse = high[0] - g_crtSetups[i].entryPrice; + if(favorable > g_crtSetups[i].maxFavorable) g_crtSetups[i].maxFavorable = favorable; + if(adverse > g_crtSetups[i].maxAdverse) g_crtSetups[i].maxAdverse = adverse; + if(currentPrice <= g_crtSetups[i].takeProfit2) + { + g_crtSetups[i].status = CRT_TRIGGERED; + g_crtSetups[i].active = false; + } + else if(currentPrice > g_crtSetups[i].stopLoss) + { + g_crtSetups[i].status = CRT_INVALIDATED; + g_crtSetups[i].active = false; + } + } + g_crtSetups[i].lastUpdate = TimeCurrent(); + } +} +void DrawCRT(const CRTSetup &crt) +{ + if(!CRT_ShowOnChart) return; + color boxColor = (crt.type == CRT_BULLISH) ? CRT_BullColor : CRT_BearColor; + // * v9.16 FIX#48: Wire CRT_Transparency via alpha channel (OBJPROP_TRANSPARENCY doesn't exist in MQL5) + int alpha = (int)MathRound(255 * (100 - CRT_Transparency) / 100.0); // CRT_Transparency=70 -> alpha=76 (30% opaque) + uint argbColor = ColorToARGB(boxColor, (uchar)alpha); + string boxName = crt.objName + "_BOX"; + ObjectCreate(0, boxName, OBJ_RECTANGLE, 0, crt.rangeTime, crt.rangeHigh, TimeCurrent(), crt.rangeLow); + ObjectSetInteger(0, boxName, OBJPROP_COLOR, argbColor); + ObjectSetInteger(0, boxName, OBJPROP_FILL, true); + ObjectSetInteger(0, boxName, OBJPROP_BACK, true); + string projName = crt.objName + "_PROJ"; + double projTarget = (crt.type == CRT_BULLISH) ? crt.projectionUp : crt.projectionDown; + ObjectCreate(0, projName, OBJ_TREND, 0, crt.rangeTime, (crt.rangeHigh + crt.rangeLow) / 2, TimeCurrent(), projTarget); + ObjectSetInteger(0, projName, OBJPROP_COLOR, CRT_ProjectionColor); + ObjectSetInteger(0, projName, OBJPROP_STYLE, STYLE_DASH); + ObjectSetInteger(0, projName, OBJPROP_WIDTH, 2); + ObjectSetInteger(0, projName, OBJPROP_RAY_RIGHT, true); + string labelName = crt.objName + "_LABEL"; + string labelText = StringFormat("CRT %s [%.1fATR]", (crt.type == CRT_BULLISH) ? "[^]" : "[v]", crt.rangeSizeATR); + ObjectCreate(0, labelName, OBJ_TEXT, 0, crt.rangeTime, crt.rangeHigh + g_cachedATR * 0.1); + ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); + ObjectSetInteger(0, labelName, OBJPROP_COLOR, boxColor); + ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 8); +} +//+===================================================================+ +//| | +//| SECTION 6: TBS FUNCTIONS | +//| | +//+===================================================================+ +void InitializeTBS() +{ + ArrayResize(g_tbsSetups, 0); + g_tbsCount = 0; + g_hasTBS = false; + ZeroMemory(g_currentTBS); +} +// [v6.42] Uses TBS_ConfirmationBars, TBS_RequireOB, TBS_RequireOTE, TBS_RequireKillzone +void DetectTBSSetups(const datetime &time[], const double &open[], + const double &high[], const double &low[], const double &close[]) +{ + if(!TBS_Enabled) return; + int lookback = MathMin(TBS_LiquidityLookback, ArraySize(time) - 5); + double swingHigh = 0, swingLow = DBL_MAX; + int swingHighBar = 0, swingLowBar = 0; + for(int i = 1; i < lookback; i++) + { + if(high[i] > swingHigh) { swingHigh = high[i]; swingHighBar = i; } + if(low[i] < swingLow) { swingLow = low[i]; swingLowBar = i; } + } + // Bullish TBS + if(low[1] < swingLow && close[1] > swingLow && close[0] > close[1]) + { + double sweepDist = swingLow - low[1]; + double sweepATR = (g_cachedATR > 0) ? sweepDist / g_cachedATR : 0; + if(sweepATR >= (g_workingTBS_MinSweepATR > 0 ? g_workingTBS_MinSweepATR : TBS_MinSweepATR) && sweepATR <= (g_workingTBS_MaxSweepATR > 0 ? g_workingTBS_MaxSweepATR : TBS_MaxSweepATR)) + { + // * v9.16 FIX#48: TBS Filters (were dead inputs) + bool tbsValid = true; + // TBS_ConfirmationBars: require N consecutive bars closing above sweep level + if(g_workingTBS_ConfirmBars > 1) // * FIX#48: use working var (AutoOpt TF-scaled) + { + for(int cb = 0; cb < MathMin(g_workingTBS_ConfirmBars, ArraySize(close) - 1); cb++) + { + if(close[cb] <= swingLow) { tbsValid = false; break; } + } + } + // TBS_RequireKillzone: only during killzone hours + if(tbsValid && TBS_RequireKillzone && !g_isInKillzone) tbsValid = false; + // TBS_RequireOB: must have active OB nearby + if(tbsValid && TBS_RequireOB) + { + bool hasOB = false; + for(int ob = 0; ob < g_obCount && !hasOB; ob++) + { + if(!OB_Array[ob].mitigated && OB_Array[ob].isBullish) hasOB = true; + } + if(!hasOB) tbsValid = false; + } + // TBS_RequireOTE: price must be in OTE zone (61.8%-78.6% retracement) + if(tbsValid && TBS_RequireOTE && g_fibData.isValid) + { + double ote618 = g_fibData.swingLow + g_fibData.range * 0.618; + double ote786 = g_fibData.swingLow + g_fibData.range * 0.786; + if(close[0] > ote618 || close[0] < ote786) tbsValid = false; // Outside OTE + } + if(tbsValid) + CreateTBSSetup(TBS_BULLISH, swingLow, low[1], time[1], 1, time, open, high, low, close); + } + } + // Bearish TBS + if(high[1] > swingHigh && close[1] < swingHigh && close[0] < close[1]) + { + double sweepDist = high[1] - swingHigh; + double sweepATR = (g_cachedATR > 0) ? sweepDist / g_cachedATR : 0; + if(sweepATR >= (g_workingTBS_MinSweepATR > 0 ? g_workingTBS_MinSweepATR : TBS_MinSweepATR) && sweepATR <= (g_workingTBS_MaxSweepATR > 0 ? g_workingTBS_MaxSweepATR : TBS_MaxSweepATR)) + { + // * v9.16 FIX#48: TBS Filters (mirror of bullish) + bool tbsValid = true; + if(g_workingTBS_ConfirmBars > 1) // * FIX#48: use working var (AutoOpt TF-scaled) + { + for(int cb = 0; cb < MathMin(g_workingTBS_ConfirmBars, ArraySize(close) - 1); cb++) + { + if(close[cb] >= swingHigh) { tbsValid = false; break; } + } + } + if(tbsValid && TBS_RequireKillzone && !g_isInKillzone) tbsValid = false; + if(tbsValid && TBS_RequireOB) + { + bool hasOB = false; + for(int ob = 0; ob < g_obCount && !hasOB; ob++) + { + if(!OB_Array[ob].mitigated && !OB_Array[ob].isBullish) hasOB = true; + } + if(!hasOB) tbsValid = false; + } + if(tbsValid && TBS_RequireOTE && g_fibData.isValid) + { + double ote618 = g_fibData.swingHigh - g_fibData.range * 0.618; + double ote786 = g_fibData.swingHigh - g_fibData.range * 0.786; + if(close[0] < ote618 || close[0] > ote786) tbsValid = false; + } + if(tbsValid) + CreateTBSSetup(TBS_BEARISH, swingHigh, high[1], time[1], 1, time, open, high, low, close); + } + } + UpdateTBSSetups(time, high, low, close); +} +void CreateTBSSetup(ENUM_TBS_TYPE type, double liqLevel, double sweepExtreme, + datetime sweepTime, int sweepBar, const datetime &time[], + const double &open[], const double &high[], + const double &low[], const double &close[]) +{ + TBSSetup newTBS; + ZeroMemory(newTBS); + newTBS.id = g_tbsCount; + newTBS.objName = "TBS_" + IntegerToString(g_tbsCount); + newTBS.type = type; + newTBS.status = TBS_PENDING; + newTBS.liquidityLevel = liqLevel; + newTBS.sweepTime = sweepTime; + newTBS.sweepBar = sweepBar; + if(type == TBS_BULLISH) + { + newTBS.falseBreakLow = sweepExtreme; + newTBS.sweepDistance = liqLevel - sweepExtreme; + newTBS.entryLevel = liqLevel + g_cachedATR * 0.1; + newTBS.stopLoss = sweepExtreme - g_cachedATR * TBS_DefaultSL_ATR; // [v6.41] was hardcoded 0.2 + newTBS.stopDistance = newTBS.entryLevel - newTBS.stopLoss; + } + else + { + newTBS.falseBreakHigh = sweepExtreme; + newTBS.sweepDistance = sweepExtreme - liqLevel; + newTBS.entryLevel = liqLevel - g_cachedATR * 0.1; + newTBS.stopLoss = sweepExtreme + g_cachedATR * TBS_DefaultSL_ATR; // [v6.41] was hardcoded 0.2 + newTBS.stopDistance = newTBS.stopLoss - newTBS.entryLevel; + } + newTBS.stopATR = (g_cachedATR > 0) ? newTBS.stopDistance / g_cachedATR : 1.5; + int dir = (type == TBS_BULLISH) ? 1 : -1; + newTBS.tp1 = newTBS.entryLevel + dir * newTBS.stopDistance * ((AutoOpt_Enabled && g_workingTBS_TP1_RR > 0) ? g_workingTBS_TP1_RR : TBS_TP1_RR); // * FIX#82 + newTBS.tp2 = newTBS.entryLevel + dir * newTBS.stopDistance * ((AutoOpt_Enabled && g_workingTBS_TP2_RR > 0) ? g_workingTBS_TP2_RR : TBS_TP2_RR); // * FIX#82 + newTBS.tp3 = newTBS.entryLevel + dir * newTBS.stopDistance * ((AutoOpt_Enabled && g_workingTBS_TP3_RR > 0) ? g_workingTBS_TP3_RR : TBS_TP3_RR); // * FIX#82 + int qualityScore = 0; + if(newTBS.sweepDistance >= g_cachedATR * 0.5) qualityScore += 25; + if(qualityScore >= 75) newTBS.quality = TBS_QUALITY_PREMIUM; + else if(qualityScore >= 50) newTBS.quality = TBS_QUALITY_HIGH; + else if(qualityScore >= 25) newTBS.quality = TBS_QUALITY_MEDIUM; + else newTBS.quality = TBS_QUALITY_LOW; + newTBS.score = qualityScore; + newTBS.active = true; + newTBS.createdTime = TimeCurrent(); + newTBS.expiryTime = TimeCurrent() + g_workingTBS_Expiry * PeriodSeconds(); + ArrayResize(g_tbsSetups, g_tbsCount + 1); + g_tbsSetups[g_tbsCount] = newTBS; + g_tbsCount++; + g_currentTBS = newTBS; + g_hasTBS = true; +} +void UpdateTBSSetups(const datetime &time[], const double &high[], + const double &low[], const double &close[]) +{ + double currentPrice = close[0]; + for(int i = 0; i < MathMin(g_tbsCount, ArraySize(g_tbsSetups)); i++) + { + if(!g_tbsSetups[i].active) continue; + g_tbsSetups[i].barsActive++; + if(TimeCurrent() > g_tbsSetups[i].expiryTime) + { + g_tbsSetups[i].status = TBS_EXPIRED; + g_tbsSetups[i].active = false; + continue; + } + if(g_tbsSetups[i].status == TBS_PENDING) + { + if(g_tbsSetups[i].type == TBS_BULLISH && currentPrice >= g_tbsSetups[i].entryLevel) + { + g_tbsSetups[i].status = TBS_TRIGGERED; + g_tbsSetups[i].entryTriggered = true; + g_tbsSetups[i].triggerTime = TimeCurrent(); + g_tbsSetups[i].triggerPrice = currentPrice; + } + else if(g_tbsSetups[i].type == TBS_BEARISH && currentPrice <= g_tbsSetups[i].entryLevel) + { + g_tbsSetups[i].status = TBS_TRIGGERED; + g_tbsSetups[i].entryTriggered = true; + g_tbsSetups[i].triggerTime = TimeCurrent(); + g_tbsSetups[i].triggerPrice = currentPrice; + } + } + if(g_tbsSetups[i].status == TBS_TRIGGERED || g_tbsSetups[i].status == TBS_ACTIVE) + { + if(g_tbsSetups[i].type == TBS_BULLISH) + { + if(low[0] <= g_tbsSetups[i].stopLoss) + { + g_tbsSetups[i].status = TBS_INVALIDATED; + g_tbsSetups[i].active = false; + } + else if(high[0] >= g_tbsSetups[i].tp2) + { + g_tbsSetups[i].status = TBS_COMPLETED; + g_tbsSetups[i].active = false; + } + } + else + { + if(high[0] >= g_tbsSetups[i].stopLoss) + { + g_tbsSetups[i].status = TBS_INVALIDATED; + g_tbsSetups[i].active = false; + } + else if(low[0] <= g_tbsSetups[i].tp2) + { + g_tbsSetups[i].status = TBS_COMPLETED; + g_tbsSetups[i].active = false; + } + } + } + } +} +void DrawTBS(const TBSSetup &tbs) +{ + if(!TBS_ShowOnChart) return; + color tbsColor = (tbs.type == TBS_BULLISH) ? TBS_BullColor : TBS_BearColor; + string liqName = tbs.objName + "_LIQ"; + ObjectCreate(0, liqName, OBJ_HLINE, 0, 0, tbs.liquidityLevel); + ObjectSetInteger(0, liqName, OBJPROP_COLOR, clrGray); + ObjectSetInteger(0, liqName, OBJPROP_STYLE, STYLE_DOT); + string arrowName = tbs.objName + "_ARROW"; + ENUM_OBJECT arrowType = (tbs.type == TBS_BULLISH) ? OBJ_ARROW_UP : OBJ_ARROW_DOWN; + double arrowPrice = (tbs.type == TBS_BULLISH) ? tbs.falseBreakLow : tbs.falseBreakHigh; + ObjectCreate(0, arrowName, arrowType, 0, tbs.sweepTime, arrowPrice); + ObjectSetInteger(0, arrowName, OBJPROP_COLOR, TBS_SweepColor); + ObjectSetInteger(0, arrowName, OBJPROP_WIDTH, 3); + string entryName = tbs.objName + "_ENTRY"; + ObjectCreate(0, entryName, OBJ_HLINE, 0, 0, tbs.entryLevel); + ObjectSetInteger(0, entryName, OBJPROP_COLOR, tbsColor); + string slName = tbs.objName + "_SL"; + ObjectCreate(0, slName, OBJ_HLINE, 0, 0, tbs.stopLoss); + ObjectSetInteger(0, slName, OBJPROP_COLOR, clrRed); + ObjectSetInteger(0, slName, OBJPROP_STYLE, STYLE_DASH); + string tp2Name = tbs.objName + "_TP2"; + ObjectCreate(0, tp2Name, OBJ_HLINE, 0, 0, tbs.tp2); + ObjectSetInteger(0, tp2Name, OBJPROP_COLOR, clrLime); + ObjectSetInteger(0, tp2Name, OBJPROP_STYLE, STYLE_DOT); +} +//+===================================================================+ +//| | +//| SECTION 7: AMD FUNCTIONS | +//| | +//+===================================================================+ +void InitializeAMD() +{ + ZeroMemory(g_amdData); + g_amdData.phase = AMD_NONE; + g_amdData.valid = false; + g_hasAMD = false; +} +void DetectAMDPhase(const datetime &time[], const double &open[], + const double &high[], const double &low[], const double &close[]) +{ + if(!AMD_Enabled) return; + + // ── New-bar guard ───────────────────────────────────────────────── + // AMD phase transitions are bar-level events, not tick-level. + // Using closed bar data (index 1) avoids look-ahead bias and prevents + // false triggers from intrabar wicks that retrace before bar close. + static datetime s_lastAMDBar = 0; + datetime curBar = time[0]; + if(curBar == s_lastAMDBar) return; // already processed this bar + s_lastAMDBar = curBar; + + // ── New-day reset ───────────────────────────────────────────────── + // Each AMD cycle belongs to one trading day. + // When a new day starts (00:00 server time), reset so a fresh + // Accumulation can be detected from the new Asian session. + // Exception: if DISTRIBUTION is still in progress, let it run + // (price doesn't respect calendar — let the move complete). + MqlDateTime dt; + TimeToStruct(time[0], dt); + int currentHour = dt.hour; + int currentDay = dt.day; + + static int s_lastAMDDay = -1; + if(currentDay != s_lastAMDDay) + { + if(g_amdData.phase != AMD_DISTRIBUTION) // don't interrupt active move + { + InitializeAMD(); + s_lastAMDDay = currentDay; + if(g_verboseLog) + Print("[AMD] New day reset | cycle cleared"); + } + else + { + s_lastAMDDay = currentDay; // update day tracker but keep distribution running + } + } + + // ── TF guard: AMD only valid on sub-H4 timeframes ──────────────── + // AMD concept = Asian session (8h) → London sweep (1-3h) → NY move. + // H4: barsPerHour=0 → asianBars=1 always → 1-bar "range" = meaningless. + // D1: 1 bar = 1 full day → sessions indistinguishable. + // M1 and below: too noisy, AMD_AccumMaxRange not calibrated. + // Valid range: M5, M15, H1. + if(_Period >= PERIOD_H4) + { + g_amdData.valid = false; + g_amdData.active = false; + g_hasAMD = false; + return; + } + + // ── All subsequent reads use CLOSED bars (index >= 1) ───────────── + // bar[0] = current bar still forming (intrabar data, unreliable) + // bar[1] = last CLOSED bar (confirmed OHLC) + int totalBars = ArraySize(time); + if(totalBars < 3) return; // need at least 3 closed bars + + g_amdData.previousPhase = g_amdData.phase; + + // ═══════════════════════════════════════════════════════════════════ + // PHASE 1: ACCUMULATION (Asian Session = tight range) + // Condition: we are IN the Asian session AND range <= AMD_AccumMaxRange×ATR + // Uses ALL closed bars of the current Asian session (bar[1] .. bar[N]) + // ═══════════════════════════════════════════════════════════════════ + if(AMD_UseSessionTiming && + currentHour >= AMD_AsianStartHour && currentHour < AMD_AsianEndHour) + { + // How many closed H1 bars have elapsed since Asian session start? + int barsPerHour = (int)MathRound(3600.0 / MathMax(1, PeriodSeconds())); + int hoursElapsed = currentHour - AMD_AsianStartHour; + int asianBars = hoursElapsed * barsPerHour + 1; // +1 for partial current hour + asianBars = MathMax(1, MathMin(asianBars, totalBars - 1)); + + // Scan closed bars only (start from bar[1]) + double asianHigh = high[1], asianLow = low[1]; + for(int i = 1; i <= asianBars && i < totalBars; i++) + { + if(high[i] > asianHigh) asianHigh = high[i]; + if(low[i] < asianLow) asianLow = low[i]; + } + + double asianRange = asianHigh - asianLow; + double rangeATR = (g_cachedATR > 0) ? asianRange / g_cachedATR : 1.0; + + if(rangeATR <= AMD_AccumMaxRange) + { + // First time entering accumulation for this session + if(g_amdData.phase != AMD_ACCUMULATION) + { + g_amdData.phase = AMD_ACCUMULATION; + g_amdData.phaseStartTime = time[1]; // closed bar timestamp + g_amdData.accumStart = (asianBars < totalBars) ? time[asianBars] : time[1]; + g_amdData.barsInPhase = 0; + } + // Update accumulation levels every bar (range may widen slightly) + g_amdData.accumHigh = asianHigh; + g_amdData.accumLow = asianLow; + g_amdData.accumMid = (asianHigh + asianLow) * 0.5; + g_amdData.accumRange = asianRange; + g_amdData.accumBars = asianBars; + g_amdData.isAsianAccum = true; + g_amdData.stage = (asianBars < AMD_AccumMinBars / 2) ? AMD_STAGE_EARLY : + (asianBars < AMD_AccumMinBars) ? AMD_STAGE_MIDDLE : + AMD_STAGE_LATE; + g_amdData.confidence = AMD_CONF_HIGH; + g_amdData.confidencePercent = MathMin(100.0, 80.0 + (AMD_AccumMaxRange - rangeATR) * 20.0); + } + } + + // ═══════════════════════════════════════════════════════════════════ + // PHASE 2: MANIPULATION (Sweep of Accumulation range) + // Condition: previous phase was ACCUMULATION AND last CLOSED bar + // pierced ABOVE accumHigh OR BELOW accumLow but CLOSED back inside. + // Uses bar[1] (last closed bar) — confirmed sweep, not intrabar wick. + // ═══════════════════════════════════════════════════════════════════ + if(g_amdData.phase == AMD_ACCUMULATION && + g_amdData.accumHigh > g_amdData.accumLow) // need valid accumulation + { + // bar[1] = last closed bar + bool sweptHigh = (high[1] > g_amdData.accumHigh) && (close[1] < g_amdData.accumHigh); + bool sweptLow = (low[1] < g_amdData.accumLow) && (close[1] > g_amdData.accumLow); + + if(sweptHigh || sweptLow) + { + double sweepDist = sweptHigh ? (high[1] - g_amdData.accumHigh) + : (g_amdData.accumLow - low[1]); + double sweepATR = (g_cachedATR > 0) ? sweepDist / g_cachedATR : 0.0; + + if(sweepATR >= AMD_ManipMinSweep) + { + g_amdData.phase = AMD_MANIPULATION; + g_amdData.phaseStartTime = time[1]; + g_amdData.manipStart = time[1]; + g_amdData.highSwept = sweptHigh; + g_amdData.lowSwept = sweptLow; + g_amdData.sweepLevel = sweptHigh ? g_amdData.accumHigh : g_amdData.accumLow; + g_amdData.sweepDistance = sweepDist; + g_amdData.manipHigh = high[1]; + g_amdData.manipLow = low[1]; + g_amdData.manipBars = 0; // will be incremented at end of phase block + g_amdData.stage = AMD_STAGE_EARLY; + g_amdData.confidence = AMD_CONF_HIGH; + g_amdData.confidencePercent = 85.0; + g_amdData.isLondonManip = (currentHour >= AMD_LondonStartHour && currentHour < AMD_NYStartHour); + // Direction is OPPOSITE of sweep (that is the ICT insight) + g_amdData.distDirection = sweptHigh ? CRT_BEARISH : CRT_BULLISH; + g_amdData.barsInPhase = 0; + if(g_verboseLog) + PrintFormat("[AMD] MANIPULATION detected | %s swept | sweep=%.1f×ATR | dir=%s", + sweptHigh ? "HIGH" : "LOW", sweepATR, + sweptHigh ? "BEARISH" : "BULLISH"); + } + } + } + + // ═══════════════════════════════════════════════════════════════════ + // PHASE 3: DISTRIBUTION TRANSITION (from Manipulation) + // Condition: in MANIPULATION AND (close has crossed accumMid in + // the expected direction, OR max manipulation bars exceeded). + // Uses bar[1] close — confirmed directional commitment. + // ═══════════════════════════════════════════════════════════════════ + if(g_amdData.phase == AMD_MANIPULATION) + { + g_amdData.manipBars++; + + bool distConfirmed = false; + if(g_amdData.highSwept && close[1] < g_amdData.accumMid) + { + distConfirmed = true; + g_amdData.distDirection = CRT_BEARISH; + } + else if(g_amdData.lowSwept && close[1] > g_amdData.accumMid) + { + distConfirmed = true; + g_amdData.distDirection = CRT_BULLISH; + } + + bool maxBarsExceeded = (g_amdData.manipBars >= AMD_ManipMaxBars); + + if(distConfirmed || maxBarsExceeded) + { + g_amdData.phase = AMD_DISTRIBUTION; + g_amdData.phaseStartTime = time[1]; + g_amdData.distStartTime = time[1]; + g_amdData.distStart = close[1]; // closed bar price + g_amdData.distCurrent = close[1]; + g_amdData.distBars = 0; // incremented below + g_amdData.distProgress = 0.0; + // Target = sweep extreme ± accumulation range + if(g_amdData.distDirection == CRT_BULLISH) + g_amdData.distTarget = g_amdData.manipHigh + g_amdData.accumRange; + else + g_amdData.distTarget = g_amdData.manipLow - g_amdData.accumRange; + g_amdData.stage = AMD_STAGE_EARLY; + g_amdData.confidence = AMD_CONF_HIGH; + g_amdData.confidencePercent = distConfirmed ? 75.0 : 55.0; // lower if forced by timeout + g_amdData.isNYDist = (currentHour >= AMD_NYStartHour); + g_amdData.barsInPhase = 0; + if(g_verboseLog) + PrintFormat("[AMD] DISTRIBUTION started | dir=%s target=%.5f | %s", + g_amdData.distDirection == CRT_BULLISH ? "BULLISH" : "BEARISH", + g_amdData.distTarget, + distConfirmed ? "confirmed" : "timeout"); + } + } + + // ═══════════════════════════════════════════════════════════════════ + // PHASE 4: UPDATE DISTRIBUTION progress + // Uses bar[1] close to track how far price has moved toward target. + // ═══════════════════════════════════════════════════════════════════ + if(g_amdData.phase == AMD_DISTRIBUTION) + { + g_amdData.distBars++; + g_amdData.distCurrent = close[1]; // confirmed closed bar + + double totalDist = MathAbs(g_amdData.distTarget - g_amdData.distStart); + double currentDist = MathAbs(close[1] - g_amdData.distStart); + g_amdData.distProgress = (totalDist > 0.0) ? (currentDist / totalDist) * 100.0 : 0.0; + + if (g_amdData.distProgress < 33.0) g_amdData.stage = AMD_STAGE_EARLY; + else if(g_amdData.distProgress < 66.0) g_amdData.stage = AMD_STAGE_MIDDLE; + else if(g_amdData.distProgress < 100.0) g_amdData.stage = AMD_STAGE_LATE; + else g_amdData.stage = AMD_STAGE_TRANSITION; + + // Cycle complete: target reached + if(g_amdData.distProgress >= 100.0) + { + if(g_verboseLog) + PrintFormat("[AMD] DISTRIBUTION complete | %.0f%% | bars=%d", + g_amdData.distProgress, g_amdData.distBars); + InitializeAMD(); // full reset — clean slate for next cycle + g_hasAMD = false; + return; + } + + // TF-aware timeout: distribution should complete within 24 hours. + // Convert 24h to bars for the current timeframe. + // M5=288 bars, M15=96 bars, H1=24 bars. + int _bph = (int)MathRound(3600.0 / MathMax(1, PeriodSeconds())); + int _maxDistBars = MathMax(24, _bph * 24); // 24h in bars, minimum 24 + if(g_amdData.distBars > _maxDistBars) + { + if(g_verboseLog) + PrintFormat("[AMD] DISTRIBUTION timeout (%d bars = 24h) — cycle reset", _maxDistBars); + InitializeAMD(); + g_hasAMD = false; + return; + } + } + + // ── Final state update ──────────────────────────────────────────── + g_amdData.barsInPhase++; + g_amdData.lastUpdate = time[1]; + g_amdData.valid = (g_amdData.phase != AMD_NONE); + g_amdData.active = g_amdData.valid; + g_hasAMD = g_amdData.valid; +} +void DrawAMD() +{ + if(!AMD_ShowOnChart || !g_amdData.valid) return; + color phaseColor; + switch(g_amdData.phase) + { + case AMD_ACCUMULATION: phaseColor = AMD_AccumColor; break; + case AMD_MANIPULATION: phaseColor = AMD_ManipColor; break; + case AMD_DISTRIBUTION: phaseColor = AMD_DistColor; break; + default: phaseColor = clrGray; break; + } + if(g_amdData.accumHigh > 0) + { + string rangeName = "AMD_RANGE"; + ObjectCreate(0, rangeName, OBJ_RECTANGLE, 0, g_amdData.accumStart, g_amdData.accumHigh, TimeCurrent(), g_amdData.accumLow); + ObjectSetInteger(0, rangeName, OBJPROP_COLOR, AMD_AccumColor); + ObjectSetInteger(0, rangeName, OBJPROP_FILL, true); + ObjectSetInteger(0, rangeName, OBJPROP_BACK, true); + } + string labelName = "AMD_LABEL"; + string labelText = StringFormat("AMD: %s [%d%%]", GetAMDPhaseString(g_amdData.phase), (int)g_amdData.confidencePercent); + ObjectCreate(0, labelName, OBJ_LABEL, 0, 0, 0); + ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); + ObjectSetInteger(0, labelName, OBJPROP_COLOR, phaseColor); + ObjectSetInteger(0, labelName, OBJPROP_CORNER, CORNER_LEFT_UPPER); + ObjectSetInteger(0, labelName, OBJPROP_XDISTANCE, 10); + ObjectSetInteger(0, labelName, OBJPROP_YDISTANCE, 50); + ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 10); +} +void CleanupAMD() +{ + ObjectsDeleteAll(0, "AMD_"); + ZeroMemory(g_amdData); + g_hasAMD = false; +} +//+===================================================================+ +//| | +//| SECTION 8: JUDAS SWING FUNCTIONS | +//| | +//+===================================================================+ +void InitializeJudas() +{ + ArrayResize(g_judasSetups, 0); + g_judasCount = 0; + g_hasJudas = false; + ZeroMemory(g_currentJudas); +} +void DetectJudasSwing(const datetime &time[], const double &open[], + const double &high[], const double &low[], const double &close[], + double pdh, double pdl, double asianHigh, double asianLow) +{ + if(!Judas_Enabled) return; + MqlDateTime dt; + TimeToStruct(time[0], dt); + int currentHour = dt.hour; + bool inLondonJudas = Judas_LondonEnabled && currentHour >= Judas_LondonStartHour && currentHour <= Judas_LondonEndHour; + bool inNYJudas = Judas_NYEnabled && currentHour >= Judas_NYStartHour && currentHour <= Judas_NYEndHour; + if(!inLondonJudas && !inNYJudas) return; + ENUM_JUDAS_SESSION session = inLondonJudas ? JUDAS_LONDON : JUDAS_NY; + // BULLISH JUDAS + bool sweptPDL = Judas_RequireSweep && low[1] < pdl && close[1] > pdl; + bool sweptAsianLow = Judas_RequireAsianSweep && low[1] < asianLow && close[1] > asianLow; + if(sweptPDL || sweptAsianLow) + { + double fakeDistance = (sweptPDL ? pdl : asianLow) - low[1]; + double fakeATR = (g_cachedATR > 0) ? fakeDistance / g_cachedATR : 0; + if(fakeATR >= Judas_MinFakeMove && fakeATR <= Judas_MaxFakeMove) + { + bool reversalConfirmed = close[0] > open[0] && close[0] > close[1]; + if(!Judas_RequireReversal || reversalConfirmed) + CreateJudasSetup(JUDAS_BULLISH, session, low[1], close[0], time[1], 1, + pdh, pdl, asianHigh, asianLow, sweptPDL, sweptAsianLow, + time, open, high, low, close); + } + } + // BEARISH JUDAS + bool sweptPDH = Judas_RequireSweep && high[1] > pdh && close[1] < pdh; + bool sweptAsianHigh = Judas_RequireAsianSweep && high[1] > asianHigh && close[1] < asianHigh; + if(sweptPDH || sweptAsianHigh) + { + double fakeDistance = high[1] - (sweptPDH ? pdh : asianHigh); + double fakeATR = (g_cachedATR > 0) ? fakeDistance / g_cachedATR : 0; + if(fakeATR >= Judas_MinFakeMove && fakeATR <= Judas_MaxFakeMove) + { + bool reversalConfirmed = close[0] < open[0] && close[0] < close[1]; + if(!Judas_RequireReversal || reversalConfirmed) + CreateJudasSetup(JUDAS_BEARISH, session, high[1], close[0], time[1], 1, + pdh, pdl, asianHigh, asianLow, sweptPDH, sweptAsianHigh, + time, open, high, low, close); + } + } + UpdateJudasSetups(time, high, low, close); +} +void CreateJudasSetup(ENUM_JUDAS_TYPE type, ENUM_JUDAS_SESSION session, + double fakeSwingPrice, double reversalPrice, + datetime fakeTime, int fakeBar, + double pdh, double pdl, double asianHigh, double asianLow, + bool sweptPDHL, bool sweptAsian, + const datetime &time[], const double &open[], + const double &high[], const double &low[], const double &close[]) +{ + JudasSwingData newJudas; + ZeroMemory(newJudas); + newJudas.id = g_judasCount; + newJudas.objName = "JUDAS_" + IntegerToString(g_judasCount); + newJudas.type = type; + newJudas.status = JUDAS_CONFIRMED; + newJudas.session = session; + newJudas.fakeSwingPrice = fakeSwingPrice; + newJudas.fakeSwingTime = fakeTime; + newJudas.fakeSwingBar = fakeBar; + newJudas.reversalPrice = reversalPrice; + newJudas.reversalTime = time[0]; + newJudas.reversalConfirmed = true; + newJudas.pdh = pdh; + newJudas.pdl = pdl; + newJudas.asianHigh = asianHigh; + newJudas.asianLow = asianLow; + newJudas.sweptPDHL = sweptPDHL; + newJudas.sweptAsian = sweptAsian; + if(type == JUDAS_BULLISH) + { + newJudas.fakeSwingLow = fakeSwingPrice; + newJudas.fakeDistance = reversalPrice - fakeSwingPrice; + newJudas.entryPrice = reversalPrice; + newJudas.stopLoss = fakeSwingPrice - g_cachedATR * (g_workingJudas_SL_ATR > 0 ? g_workingJudas_SL_ATR : Judas_SL_ATR); + newJudas.stopDistance = newJudas.entryPrice - newJudas.stopLoss; + // [v6.41] Use structure TPs or R:R TPs (whichever is larger) + double slDist = MathAbs(newJudas.entryPrice - newJudas.stopLoss); + double rrTP1 = newJudas.entryPrice + slDist * ((AutoOpt_Enabled && g_workingJudas_TP1_RR > 0) ? g_workingJudas_TP1_RR : Judas_TP1_RR); // * FIX#82 + double rrTP2 = newJudas.entryPrice + slDist * ((AutoOpt_Enabled && g_workingJudas_TP2_RR > 0) ? g_workingJudas_TP2_RR : Judas_TP2_RR); // * FIX#82 + double rrTP3 = newJudas.entryPrice + slDist * ((AutoOpt_Enabled && g_workingJudas_TP3_RR > 0) ? g_workingJudas_TP3_RR : Judas_TP3_RR); // * FIX#82 + newJudas.tp1 = MathMax(asianHigh, rrTP1); + newJudas.tp2 = MathMax(pdh, rrTP2); + newJudas.tp3 = MathMax(pdh + (pdh - pdl), rrTP3); + newJudas.tpMax = MathMax(newJudas.tp2, newJudas.tp3); + } + else + { + newJudas.fakeSwingHigh = fakeSwingPrice; + newJudas.fakeDistance = fakeSwingPrice - reversalPrice; + newJudas.entryPrice = reversalPrice; + newJudas.stopLoss = fakeSwingPrice + g_cachedATR * (g_workingJudas_SL_ATR > 0 ? g_workingJudas_SL_ATR : Judas_SL_ATR); + newJudas.stopDistance = newJudas.stopLoss - newJudas.entryPrice; + // [v6.41] Use structure TPs or R:R TPs (whichever is further) + double slDistB = MathAbs(newJudas.entryPrice - newJudas.stopLoss); + double rrTP1b = newJudas.entryPrice - slDistB * ((AutoOpt_Enabled && g_workingJudas_TP1_RR > 0) ? g_workingJudas_TP1_RR : Judas_TP1_RR); // * FIX#82 + double rrTP2b = newJudas.entryPrice - slDistB * ((AutoOpt_Enabled && g_workingJudas_TP2_RR > 0) ? g_workingJudas_TP2_RR : Judas_TP2_RR); // * FIX#82 + double rrTP3b = newJudas.entryPrice - slDistB * ((AutoOpt_Enabled && g_workingJudas_TP3_RR > 0) ? g_workingJudas_TP3_RR : Judas_TP3_RR); // * FIX#82 + newJudas.tp1 = MathMin(asianLow, rrTP1b); + newJudas.tp2 = MathMin(pdl, rrTP2b); + newJudas.tp3 = MathMin(pdl - (pdh - pdl), rrTP3b); + newJudas.tpMax = MathMin(newJudas.tp2, newJudas.tp3); + } + int confScore = 0; + if(sweptPDHL) confScore += 30; + if(sweptAsian) confScore += 25; + newJudas.confluenceScore = confScore; + if(confScore >= 70) newJudas.quality = QUALITY_A; + else if(confScore >= 50) newJudas.quality = QUALITY_B; + else if(confScore >= 30) newJudas.quality = QUALITY_C; + else newJudas.quality = QUALITY_D; + newJudas.score = confScore; + newJudas.active = true; + newJudas.createdTime = TimeCurrent(); + newJudas.expiryTime = TimeCurrent() + 4 * 3600; + ArrayResize(g_judasSetups, g_judasCount + 1); + g_judasSetups[g_judasCount] = newJudas; + g_judasCount++; + g_currentJudas = newJudas; + g_hasJudas = true; +} +void UpdateJudasSetups(const datetime &time[], const double &high[], + const double &low[], const double &close[]) +{ + for(int i = 0; i < g_judasCount; i++) + { + if(!g_judasSetups[i].active) continue; + g_judasSetups[i].barsActive++; + if(TimeCurrent() > g_judasSetups[i].expiryTime) + { + g_judasSetups[i].status = JUDAS_FAILED; + g_judasSetups[i].active = false; + continue; + } + if(g_judasSetups[i].type == JUDAS_BULLISH) + { + if(low[0] <= g_judasSetups[i].stopLoss) + { + g_judasSetups[i].status = JUDAS_FAILED; + g_judasSetups[i].active = false; + } + else if(high[0] >= g_judasSetups[i].tp2) + { + g_judasSetups[i].status = JUDAS_COMPLETED; + g_judasSetups[i].active = false; + } + } + else + { + if(high[0] >= g_judasSetups[i].stopLoss) + { + g_judasSetups[i].status = JUDAS_FAILED; + g_judasSetups[i].active = false; + } + else if(low[0] <= g_judasSetups[i].tp2) + { + g_judasSetups[i].status = JUDAS_COMPLETED; + g_judasSetups[i].active = false; + } + } + } +} +void DrawJudas(const JudasSwingData &judas) +{ + if(!Judas_ShowOnChart) return; + color judasColor = (judas.type == JUDAS_BULLISH) ? Judas_BullColor : Judas_BearColor; + string arrowName = judas.objName + "_ARROW"; + ENUM_OBJECT arrowType = (judas.type == JUDAS_BULLISH) ? OBJ_ARROW_UP : OBJ_ARROW_DOWN; + ObjectCreate(0, arrowName, arrowType, 0, judas.fakeSwingTime, judas.fakeSwingPrice); + ObjectSetInteger(0, arrowName, OBJPROP_COLOR, Judas_ArrowColor); + ObjectSetInteger(0, arrowName, OBJPROP_WIDTH, 3); + string revName = judas.objName + "_REV"; + ObjectCreate(0, revName, OBJ_TREND, 0, judas.fakeSwingTime, judas.fakeSwingPrice, judas.reversalTime, judas.reversalPrice); + ObjectSetInteger(0, revName, OBJPROP_COLOR, judasColor); + ObjectSetInteger(0, revName, OBJPROP_WIDTH, 2); + string slName = judas.objName + "_SL"; + ObjectCreate(0, slName, OBJ_HLINE, 0, 0, judas.stopLoss); + ObjectSetInteger(0, slName, OBJPROP_COLOR, clrRed); + ObjectSetInteger(0, slName, OBJPROP_STYLE, STYLE_DASH); + string tp2Name = judas.objName + "_TP2"; + ObjectCreate(0, tp2Name, OBJ_HLINE, 0, 0, judas.tp2); + ObjectSetInteger(0, tp2Name, OBJPROP_COLOR, clrLime); + ObjectSetInteger(0, tp2Name, OBJPROP_STYLE, STYLE_DASH); + string labelName = judas.objName + "_LABEL"; + string sessionStr = (judas.session == JUDAS_LONDON) ? "LDN" : "NY"; + string labelText = StringFormat("JUDAS %s [%s]", (judas.type == JUDAS_BULLISH) ? "[^]" : "[v]", sessionStr); + ObjectCreate(0, labelName, OBJ_TEXT, 0, judas.fakeSwingTime, judas.fakeSwingPrice + (judas.type == JUDAS_BULLISH ? -1 : 1) * g_cachedATR * 0.3); + ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); + ObjectSetInteger(0, labelName, OBJPROP_COLOR, judasColor); +} +void CleanupJudas() +{ + ObjectsDeleteAll(0, "JUDAS_"); + ArrayResize(g_judasSetups, 0); + g_judasCount = 0; + g_hasJudas = false; +} +//+===================================================================+ +//| | +//| SECTION 9: MARKET REGIME FUNCTIONS | +//| | +//+===================================================================+ +void DetectMarketRegime(const datetime &time[], const double &open[], + const double &high[], const double &low[], const double &close[]) +{ + if(!Regime_Enabled) return; + int lookback = MathMin(g_workingRegime_Lookback, ArraySize(time) - 1); + g_regimeData.previousRegime = g_regimeData.regime; + // ADX calculation + double plusDM = 0, minusDM = 0, trSum = 0; + for(int i = 0; i < Regime_ADXPeriod && i < lookback; i++) + { + double tr = MathMax(high[i] - low[i], MathMax(MathAbs(high[i] - close[i+1]), MathAbs(low[i] - close[i+1]))); + trSum += tr; + double upMove = high[i] - high[i+1]; + double downMove = low[i+1] - low[i]; + if(upMove > downMove && upMove > 0) plusDM += upMove; + if(downMove > upMove && downMove > 0) minusDM += downMove; + } + double plusDI = (trSum > 0) ? (plusDM / trSum) * 100 : 0; + double minusDI = (trSum > 0) ? (minusDM / trSum) * 100 : 0; + double dx = (plusDI + minusDI > 0) ? MathAbs(plusDI - minusDI) / (plusDI + minusDI) * 100 : 0; + g_regimeData.trendStrength = dx; + g_cachedADX = dx; + // * v10.36 FIX#335: When DI+/DI- are close (spread < 3 pts), direction is ambiguous. + // Old: always defaulted to -1 when plusDI <= minusDI — with near-equal DI the choice was + // arbitrary, caused false TREND_DOWN assignment, then FIX#290 saw structure conflict and + // could downgrade to CHOPPY even on high-ADX bars. + // Fix: if spread < 3 AND structure is available, break the DI tie with structure direction. + // This ensures strong-ADX bars with ambiguous DI don't create a spurious conflict. + // Also fixes the log "dir=0" display bug: trendDirection was double printed as %d (see FIX#290 print). + { + double diSpread = MathAbs(plusDI - minusDI); + if(diSpread < 3.0 && EnableStructure && g_regimeInitialized) + { + // Direction genuinely ambiguous — structure is the more reliable signal + g_regimeData.trendDirection = g_isBullishStructure ? 1.0 : -1.0; + if(g_verboseLog) + PrintFormat("[FIX#335] DI spread=%.1f < 3 → trendDirection from structure (%s)", + diSpread, g_isBullishStructure ? "BULL" : "BEAR"); + } + else + { + g_regimeData.trendDirection = (plusDI > minusDI) ? 1.0 : -1.0; + } + } + // Efficiency ratio + double totalMove = MathAbs(close[0] - close[lookback-1]); + double sumMoves = 0; + for(int i = 0; i < lookback - 1; i++) sumMoves += MathAbs(close[i] - close[i+1]); + g_regimeData.trendEfficiency = (sumMoves > 0) ? totalMove / sumMoves : 0; + // Range + double rangeHigh = high[0], rangeLow = low[0]; + for(int i = 0; i < lookback; i++) + { + if(high[i] > rangeHigh) rangeHigh = high[i]; + if(low[i] < rangeLow) rangeLow = low[i]; + } + g_regimeData.rangeHigh = rangeHigh; + g_regimeData.rangeLow = rangeLow; + g_regimeData.rangeWidth = rangeHigh - rangeLow; + g_regimeData.rangeWidthATR = (g_cachedATR > 0) ? g_regimeData.rangeWidth / g_cachedATR : 0; + // Volatility + g_regimeData.currentVolatility = g_cachedATR; + double avgATR = 0; + for(int i = 0; i < lookback * 2 && i < ArraySize(time) - 1; i++) avgATR += high[i] - low[i]; + avgATR /= (lookback * 2); + g_regimeData.avgVolatility = avgATR; + g_regimeData.volatilityRatio = (avgATR > 0) ? g_cachedATR / avgATR : 1; + g_regimeData.isHighVol = (g_regimeData.volatilityRatio > 1.2); + g_regimeData.isVolExpanding = (g_cachedATR > avgATR * 1.1); + g_regimeData.isVolContracting = (g_cachedATR < avgATR * 0.9); + // Determine regime + ENUM_MARKET_REGIME newRegime = REGIME_UNKNOWN; + if(dx >= Regime_StrongTrendADX && g_regimeData.trendEfficiency > 0.7) + { + newRegime = (g_regimeData.trendDirection > 0) ? REGIME_STRONG_TREND_UP : REGIME_STRONG_TREND_DOWN; + g_regimeData.regimeConfidence = 90; + } + else if(dx >= (g_workingRegime_ADXMin > 0 ? g_workingRegime_ADXMin : Regime_TrendADXMin) && g_regimeData.trendEfficiency > 0.5) // * FIX#82 + { + newRegime = (g_regimeData.trendDirection > 0) ? REGIME_TREND_UP : REGIME_TREND_DOWN; + g_regimeData.regimeConfidence = 75; + } + else if(dx >= (g_workingRegime_ADXMin > 0 ? g_workingRegime_ADXMin : Regime_TrendADXMin) * 0.7) // * FIX#82 + { + newRegime = (g_regimeData.trendDirection > 0) ? REGIME_WEAK_TREND_UP : REGIME_WEAK_TREND_DOWN; + g_regimeData.regimeConfidence = 60; + } + else if(dx < (g_workingRegime_ADXMin > 0 ? g_workingRegime_ADXMin : Regime_TrendADXMin) && g_regimeData.rangeWidthATR < Regime_RangeATRRatio * 5) // * FIX#82 + { + newRegime = (g_regimeData.rangeWidthATR < Regime_RangeATRRatio * 2) ? REGIME_RANGING_TIGHT : REGIME_RANGING_WIDE; + g_regimeData.regimeConfidence = 70; + } + else if(g_regimeData.isHighVol && g_regimeData.trendEfficiency < 0.3) + { + newRegime = REGIME_VOLATILE; + g_regimeData.regimeConfidence = 65; + } + else + { + newRegime = REGIME_CHOPPY; + g_regimeData.regimeConfidence = 50; + } + if(g_regimeData.previousRegime == REGIME_RANGING_TIGHT && (newRegime == REGIME_TREND_UP || newRegime == REGIME_TREND_DOWN)) + { + newRegime = REGIME_BREAKOUT; + g_regimeData.transition = TRANSITION_RANGE_TO_TREND; + g_regimeData.regimeConfidence = 80; + } + if(newRegime != g_regimeData.previousRegime) + { + g_regimeData.barsInRegime = 0; + g_regimeData.regimeStartTime = TimeCurrent(); + } + else g_regimeData.barsInRegime++; + + // * v10.14 FIX#290: STRUCTURE-REGIME ALIGNMENT CHECK + // Problem: Regime says TREND_UP (ADX+ER) but Structure says BEAR (CHoCH). + // This contradiction caused the system to keep entering SELL trades in + // "WEAK UPTREND" — ADX saw directional energy but BOS/CHoCH saw bearish structure. + // Fix: If regime direction contradicts structure, DOWNGRADE regime confidence. + // STRONG_TREND_UP + bearish structure → WEAK_TREND_UP (not fully trend up) + // TREND_UP + bearish structure → CHOPPY (conflicting signals) + // Same logic inverted for bearish regime + bullish structure. + // This also ensures CHOPPY correctly blocks counter-trend entries (FIX#292 below). + if(EnableStructure && g_regimeInitialized) + { + bool regimeBullish = (newRegime == REGIME_STRONG_TREND_UP || newRegime == REGIME_TREND_UP || newRegime == REGIME_WEAK_TREND_UP); + bool regimeBearish = (newRegime == REGIME_STRONG_TREND_DOWN || newRegime == REGIME_TREND_DOWN || newRegime == REGIME_WEAK_TREND_DOWN); + bool structureConflict = (regimeBullish && !g_isBullishStructure) || (regimeBearish && g_isBullishStructure); + if(structureConflict) + { + // * v10.38 FIX#337: MTF+Structure veto — when MTF and Structure BOTH agree on direction, + // the DI conflict is a false signal. The indicator is lagging; the higher-timeframe + // context is correct. Skip all FIX#290 downgrade when they are unanimous. + // Example: ADX=45 dir=DOWN, MTF=STRONG_BULL, Structure=BULL → all three say BULL + // except DI direction. FIX#290 was downgrading to CHOPPY, blocking 10+ OBs for days. + bool mtfBull = (g_mtfInitialized && (g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH || + g_mtfAnalysis.overallDirection == MTF_BULLISH)); + bool mtfBear = (g_mtfInitialized && (g_mtfAnalysis.overallDirection == MTF_STRONG_BEARISH || + g_mtfAnalysis.overallDirection == MTF_BEARISH)); + bool mtfStructureAgree = (g_isBullishStructure && mtfBull) || (!g_isBullishStructure && mtfBear); + if(mtfStructureAgree) + { + // MTF and Structure are unanimous — DI direction is the outlier. + // Redirect regime to follow structure, no CHOPPY penalty. + if(regimeBullish && !g_isBullishStructure) + newRegime = REGIME_WEAK_TREND_DOWN; + else if(regimeBearish && g_isBullishStructure) + newRegime = REGIME_WEAK_TREND_UP; + g_regimeData.regimeConfidence = MathMax(55, g_regimeData.regimeConfidence - 5); + if(g_verboseLog) + PrintFormat("[FIX#337] MTF+Structure veto: ADX=%d DI-conflict overruled | MTF=%s Struct=%s → %s (no CHOPPY)", + (int)dx, g_isBullishStructure ? "BULL" : "BEAR", + mtfBull ? "BULL" : "BEAR", GetRegimeString(newRegime)); + } + // * v10.38 FIX#336: Lower WEAK_TREND threshold 50→38. + // FIX#331 raised it 40→50 but left ADX=38-49 unprotected — those bars still hit CHOPPY. + // Backtest showed 17 FIX#290 downgrades in ADX=34-45 range, all losing opportunities. + // ADX=38+ is a real trend (above the typical 25 threshold with margin). CHOPPY at 38+ wrong. + else if(dx >= 38.0) + { + // Trending ADX + structure conflict: follow structure, keep WEAK_TREND + if(regimeBullish && !g_isBullishStructure) + newRegime = REGIME_WEAK_TREND_DOWN; + else if(regimeBearish && g_isBullishStructure) + newRegime = REGIME_WEAK_TREND_UP; + g_regimeData.regimeConfidence = MathMax(50, g_regimeData.regimeConfidence - 10); + if(g_verboseLog) + PrintFormat("[FIX#336] ADX=%d>=38 conflict: keeping WEAK_TREND (not CHOPPY) vs Structure=%s → %s", + (int)dx, g_isBullishStructure ? "BULL" : "BEAR", GetRegimeString(newRegime)); + } + else + { + // Low ADX (<38) + structure conflict = genuinely uncertain → CHOPPY + if(newRegime == REGIME_STRONG_TREND_UP) newRegime = REGIME_TREND_UP; + else if(newRegime == REGIME_STRONG_TREND_DOWN) newRegime = REGIME_TREND_DOWN; + else if(newRegime == REGIME_TREND_UP || newRegime == REGIME_TREND_DOWN) + newRegime = REGIME_CHOPPY; + else if(newRegime == REGIME_WEAK_TREND_UP || newRegime == REGIME_WEAK_TREND_DOWN) + newRegime = REGIME_CHOPPY; + g_regimeData.regimeConfidence = MathMax(40, g_regimeData.regimeConfidence - 20); + if(g_verboseLog) + PrintFormat("[FIX#290] Regime downgraded: ADX=%d dir=%g vs Structure=%s → %s (conf=%d)", + (int)dx, g_regimeData.trendDirection, + g_isBullishStructure ? "BULL" : "BEAR", + GetRegimeString(newRegime), (int)g_regimeData.regimeConfidence); + } + } + } + + g_regimeData.regime = newRegime; + SetRegimeParameters(); + g_regimeData.isTrending = (newRegime <= REGIME_WEAK_TREND_DOWN && newRegime != REGIME_RANGING_TIGHT && newRegime != REGIME_RANGING_WIDE); + g_regimeData.isRanging = (newRegime == REGIME_RANGING_TIGHT || newRegime == REGIME_RANGING_WIDE); + g_regimeData.lastUpdate = TimeCurrent(); + g_regimeData.valid = true; + g_regimeValid = true; + + // ── FIX#501: COMPOSITE RANGE CONFIRMATION SCORE (0-5) ───────── + // 5-level framework using 3-TF hierarchy: + // L1a: ADX_mid < 25 — middle TF (H4 for H1 EA) not trending + // L1b: ADX_high < 20 — higher TF (D1 for H1 EA) macro ranging + // L2: MA slope flat — current TF MA nearly horizontal + // L3: rangeWidthATR — price confined in narrow ATR-multiple band + // L4: ATR contracting — volatility compressing (squeeze) + // + // Score ≥ 3: RANGE CONFIRMED → premium/discount gate ACTIVE + // Score = 2: RANGE PROBABLE → soft penalty + // Score 0-1: TRENDING → no restriction + // + // TF-aware thresholds scale with _Period: + // Higher TFs have naturally wider ATR ranges and flatter MAs + { + double _rwATR_threshold; // rangeWidthATR < this = tight (Level 3) + double _maSlope_threshold; // MA slope < this = flat (Level 2) + if (_Period >= PERIOD_D1) { _rwATR_threshold = 6.0; _maSlope_threshold = 0.40; } + else if(_Period >= PERIOD_H4) { _rwATR_threshold = 5.0; _maSlope_threshold = 0.35; } + else if(_Period >= PERIOD_H1) { _rwATR_threshold = 4.0; _maSlope_threshold = 0.30; } + else if(_Period >= PERIOD_M15) { _rwATR_threshold = 3.5; _maSlope_threshold = 0.25; } + else { _rwATR_threshold = 3.0; _maSlope_threshold = 0.20; } + + int _score = 0; + // L1a: Middle TF ADX (H4 for H1 EA) — intermediate context not trending + bool _midRanging = (g_cachedADX_HTF_mid > 0 && g_cachedADX_HTF_mid < 25.0); + // L1b: Higher TF ADX (D1 for H1 EA) — macro context ranging (stricter < 20) + bool _highRanging = (g_cachedADX_HTF_high > 0 && g_cachedADX_HTF_high < 20.0); + if(_midRanging) _score++; + if(_highRanging) _score++; + // L2: MA slope — current TF MA flat? + bool _maFlat = (g_cachedMA_Slope > 0 && g_cachedMA_Slope < _maSlope_threshold); + if(_maFlat) _score++; + // L3: Price structure tight — range width vs ATR + bool _tightRange = (g_regimeData.rangeWidthATR > 0 && g_regimeData.rangeWidthATR < _rwATR_threshold); + if(_tightRange) _score++; + // L4: ATR contracting — volatility squeezing + if(g_regimeData.isVolContracting) _score++; + + g_rangeConfScore = _score; + + if(g_verboseLog && _score >= 2) + PrintFormat("[FIX#501] RangeConf=%d/5 | ADX_mid=%.0f(<25)=%s ADX_high=%.0f(<20)=%s | MAslope=%.2f(<%.2f)=%s | rngWidthATR=%.1f(<%.1f)=%s | volCont=%s", + _score, + g_cachedADX_HTF_mid, _midRanging ? "Y":"N", + g_cachedADX_HTF_high, _highRanging ? "Y":"N", + g_cachedMA_Slope, _maSlope_threshold, _maFlat ? "Y":"N", + g_regimeData.rangeWidthATR, _rwATR_threshold, _tightRange ? "Y":"N", + g_regimeData.isVolContracting ? "Y":"N"); + } + + // ── FIX#503: TREND EXHAUSTION SCORE (0-5) ───────────────────────── + // Composite score detecting Scenario 11 transition: Trend → Choppy. + // ROOT: Dec 20 2023 — system entered 4 BUY trades at FOMC rally top + // because MARKUP phase was still active. No mechanism existed to + // detect that the trend was overextended/exhausted. + // ARCHITECTURE: mirrors g_rangeConfScore — same 0-5 composite model. + // Each of 5 independent signals adds +1. Score ≥ 4 = overextended. + // Used by DeriveScenarioProfile to graduate response: + // 0-1: healthy trend → normal entry + // 2 : caution → OB/FVG only, lower confidence + // 3 : weakening → pullback entries only + // 4-5: overextended → block same-direction, activate reversal detection + // ── FIX#503b: TREND EXHAUSTION SCORE (0-5) — upgraded ──────────── + // Changes vs FIX#503: + // 1. Bar offset: CopyHigh(1,...) not (0,...) — bar[0]=still open, look-ahead bias + // 2. ADX: uses cached g_adxHandleExhaust (init once in OnInit) vs iADX() per bar + // 3. Rolling peak: g_exhaustionPeak = max of last EXHAUST_LOOKBACK bars + // 4. Trend score: g_exhaustionTrend = weighted mean (recent bars weighted more) + // DeriveScenarioProfile reads g_exhaustionPeak (more robust than raw per-bar score) + { + int _exScore = 0; + + // E1+E2: Momentum + Rejection — using CLOSED bars only (offset=1) + if(g_cachedATR > 0) + { + double _hi[], _lo[], _cl[], _op[]; + ArraySetAsSeries(_hi, true); ArraySetAsSeries(_lo, true); + ArraySetAsSeries(_cl, true); ArraySetAsSeries(_op, true); + // offset=1: skip bar[0] (still open), start from last closed bar + if(CopyHigh(_Symbol,_Period,1,4,_hi)>=4 && + CopyLow (_Symbol,_Period,1,4,_lo)>=4 && + CopyClose(_Symbol,_Period,1,4,_cl)>=4 && + CopyOpen (_Symbol,_Period,1,4,_op)>=4) + { + // E1: body[0] (last closed) < 0.5×ATR AND shrinking vs body[1] + double _body0 = MathAbs(_cl[0]-_op[0]); + double _body1 = MathAbs(_cl[1]-_op[1]); + if(_body0 < g_cachedATR * 0.5 && _body0 < _body1 * 0.80) + _exScore++; + + // E2: rejection wick at trend extreme (last closed bar) + // FIX#512: Context-aware wick threshold. + // AMD_ACCUMULATION = Asian session low-liquidity drift → lower threshold. + // Otherwise keep 0.55 to avoid noise on normal pullback wicks. + double _range0 = MathMax(_hi[0] - _lo[0], _Point); + double _upperWick = _hi[0] - MathMax(_cl[0], _op[0]); + double _lowerWick = MathMin(_cl[0], _op[0]) - _lo[0]; + bool _trendBull = (g_regimeData.trendDirection > 0); + double _wickThresh = (g_amdPhase.phase == AMD_ACCUMULATION) ? 0.40 : 0.55; + if(_trendBull && _upperWick > _range0 * _wickThresh) _exScore++; + if(!_trendBull && _lowerWick > _range0 * _wickThresh) _exScore++; + } + } + + // E3: ADX declining — uses cached handle (no per-bar alloc/free) + if(g_adxHandleExhaust != INVALID_HANDLE) + { + double _adxArr[]; + ArraySetAsSeries(_adxArr, true); + // offset=1: consistent with other E signals + if(CopyBuffer(g_adxHandleExhaust, 0, 1, 5, _adxArr) >= 5) + { + bool _adxWasHigh = (_adxArr[2] > 35.0 || _adxArr[3] > 35.0); + bool _adxDeclining = (_adxArr[0] < _adxArr[1] && _adxArr[1] < _adxArr[2]); + if(_adxWasHigh && _adxDeclining) + _exScore++; + } + } + + // E4: Trend efficiency + if(g_regimeData.trendEfficiency > 0 && g_regimeData.trendEfficiency < 0.40) + _exScore++; + + // E5: Overextension — use last closed bar (iClose offset=1) + if(g_cachedATR > 0 && g_regimeData.rangeHigh > g_regimeData.rangeLow) + { + double _rangePips = (g_regimeData.rangeHigh - g_regimeData.rangeLow) / g_cachedATR; + double _lastClose = iClose(_Symbol, _Period, 1); // closed bar + double _rangeSize = g_regimeData.rangeHigh - g_regimeData.rangeLow; + bool _atExtreme = false; + if(g_regimeData.trendDirection > 0) + _atExtreme = (_lastClose > g_regimeData.rangeLow + _rangeSize * 0.80); + else + _atExtreme = (_lastClose < g_regimeData.rangeLow + _rangeSize * 0.20); + if(_rangePips > 3.5 && _atExtreme) + _exScore++; + } + + _exScore = MathMin(_exScore, 5); + g_exhaustionScore = _exScore; + + // Rolling history: shift and store + for(int _ri = EXHAUST_LOOKBACK - 1; _ri > 0; _ri--) + g_exhaustionHistory[_ri] = g_exhaustionHistory[_ri-1]; + g_exhaustionHistory[0] = _exScore; + + // Peak: max of last EXHAUST_LOOKBACK bars + int _peak = 0; + for(int _ri = 0; _ri < EXHAUST_LOOKBACK; _ri++) + _peak = MathMax(_peak, g_exhaustionHistory[_ri]); + g_exhaustionPeak = _peak; + + // Trend score: weighted mean (0=most recent, EXHAUST_LOOKBACK-1=oldest) + double _wts[EXHAUST_LOOKBACK]; _wts[0]=0.50; _wts[1]=0.25; _wts[2]=0.15; _wts[3]=0.10; + double _wsum = 0.0; + for(int _ri = 0; _ri < EXHAUST_LOOKBACK; _ri++) + _wsum += g_exhaustionHistory[_ri] * _wts[_ri]; + g_exhaustionTrend = _wsum / 5.0; // normalize 0→1 + + if(g_verboseLog && (_exScore >= 2 || _peak >= 2)) + PrintFormat("[FIX#503] ExhaustionScore=%d/5 Peak=%d/5 Trend=%.2f | " + "efficiency=%.2f", + _exScore, _peak, g_exhaustionTrend, g_regimeData.trendEfficiency); + } + + // ── FIX#503b: BREAKOUT CONFIRMATION SCORE (0-4) ─────────────── + // Assesses quality of a breakout to distinguish genuine vs fakeout. + // Used by DeriveScenarioProfile BREAKOUT branch. + { + int _bScore = 0; + // B1: Regime confirms breakout + if(g_regimeData.regime == REGIME_BREAKOUT) _bScore++; + // B2: Volume expanding (momentum behind the move) + if(g_regimeData.isVolExpanding) _bScore++; + // B3: ADX increasing — trend gaining strength + if(g_cachedADX > 25.0 && g_regimeData.barsInRegime <= 5) _bScore++; + // B4: Strong body on breakout candle (not a wick-heavy spike) + { + double _hi1 = iHigh(_Symbol, _Period, 1); + double _lo1 = iLow (_Symbol, _Period, 1); + double _cl1 = iClose(_Symbol, _Period, 1); + double _op1 = iOpen (_Symbol, _Period, 1); + double _rng = _hi1 - _lo1; + if(_rng > _Point * 2) + { + double _bodyPct = MathAbs(_cl1 - _op1) / _rng; + if(_bodyPct > 0.60) _bScore++; + } + } + g_breakoutConfScore = MathMin(_bScore, 4); + } + + // ── FIX#503b: REVERSAL CONFIRMATION SCORE (0-4) ─────────────── + // Assesses quality of a reversal setup. + // Used by DeriveScenarioProfile ACCUMULATION/DISTRIBUTION branch. + { + int _rScore = 0; + // R1: CHoCH recently confirmed (structure shift) + if(HasRecentCHoCH()) _rScore++; + // R2: Divergence present (RSI diverging from price) + if(g_bullishDivergence || g_bearishDivergence) _rScore++; + // R3: Exhaustion confirms weakness (peak >= 3 = trend losing steam) + if(g_exhaustionPeak >= 3) _rScore++; + // R4: MTF and structure disagree (transition in progress) + { + bool _mtfBull = (g_mtfAnalysis.overallDirection == MTF_BULLISH || + g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH); + bool _mtfBear = (g_mtfAnalysis.overallDirection == MTF_BEARISH || + g_mtfAnalysis.overallDirection == MTF_STRONG_BEARISH); + if((_mtfBull && !g_isBullishStructure) || + (_mtfBear && g_isBullishStructure)) + _rScore++; + } + g_reversalConfScore = MathMin(_rScore, 4); + } +} +void SetRegimeParameters() +{ + switch(g_regimeData.regime) + { + case REGIME_STRONG_TREND_UP: + case REGIME_STRONG_TREND_DOWN: + g_regimeData.optimalRR = Regime_TrendRR; + g_regimeData.confidenceMultiplier = 1.3; + g_regimeData.requiredConfirmations = 2; + g_regimeData.allowCounterTrend = false; + g_regimeData.positionSizeMultiplier = 1.2; + g_regimeData.shouldTrade = true; + g_regimeData.strategyHint = "Follow trend, use pullbacks"; + break; + case REGIME_TREND_UP: + case REGIME_TREND_DOWN: + g_regimeData.optimalRR = Regime_TrendRR * 0.8; + g_regimeData.confidenceMultiplier = 1.1; + g_regimeData.requiredConfirmations = 3; + g_regimeData.allowCounterTrend = false; + g_regimeData.positionSizeMultiplier = 1.0; + g_regimeData.shouldTrade = true; + g_regimeData.strategyHint = "Trade with trend, OTE entries"; + break; + // * v7.9 FIX BUG#1: WEAK_TREND had NO case -> fell to default (shouldTrade=false, confidenceMultiplier=0.6) + // EvaluateSmartEntry() doesn't check shouldTrade so trades still opened but with -4 score penalty only! + // Now WEAK_TREND is properly configured: allowCounterTrend=false is the KEY setting + case REGIME_WEAK_TREND_UP: + case REGIME_WEAK_TREND_DOWN: + g_regimeData.optimalRR = Regime_TrendRR * 0.7; + g_regimeData.confidenceMultiplier = 0.9; // * v9.16 PERF: 1.0->0.9 (slight WP discount in weak trends) + g_regimeData.requiredConfirmations = 4; // * v9.16 PERF: 3->4 (need more confluence in weak trends) + g_regimeData.allowCounterTrend = false; // CRITICAL: block counter-trend in weak trends! + g_regimeData.positionSizeMultiplier = 0.7; // * v9.16 PERF: 0.9->0.7 (reduce exposure in weak trends, backtest showed 68% SL rate) + g_regimeData.shouldTrade = true; + g_regimeData.strategyHint = "Weak trend -- prefer direction, reduced size"; + break; + case REGIME_RANGING_TIGHT: + g_regimeData.optimalRR = Regime_RangeRR; + g_regimeData.confidenceMultiplier = 0.8; + g_regimeData.requiredConfirmations = 4; + g_regimeData.allowCounterTrend = true; + g_regimeData.positionSizeMultiplier = 0.7; + g_regimeData.shouldTrade = Regime_UseMeanReversion; + g_regimeData.useMeanReversion = Regime_UseMeanReversion; // * FIX#373 + g_regimeData.strategyHint = "Mean reversion at extremes"; + break; + case REGIME_RANGING_WIDE: + g_regimeData.optimalRR = Regime_RangeRR; + g_regimeData.confidenceMultiplier = 0.9; + g_regimeData.requiredConfirmations = 3; + g_regimeData.allowCounterTrend = true; + g_regimeData.positionSizeMultiplier = 0.8; + g_regimeData.shouldTrade = true; + g_regimeData.useMeanReversion = true; // * FIX#373: wide range = ideal MR environment + g_regimeData.strategyHint = "Trade range extremes"; + break; + case REGIME_BREAKOUT: + g_regimeData.optimalRR = Regime_TrendRR; + g_regimeData.confidenceMultiplier = 1.2; + g_regimeData.requiredConfirmations = 2; + g_regimeData.allowCounterTrend = false; + g_regimeData.positionSizeMultiplier = 1.1; + g_regimeData.shouldTrade = true; + g_regimeData.strategyHint = "Breakout confirmed, follow direction"; + break; + case REGIME_RANGING: // * FIX#373: add explicit RANGING case for useMeanReversion + g_regimeData.optimalRR = Regime_RangeRR; + g_regimeData.confidenceMultiplier = 0.85; + g_regimeData.requiredConfirmations = 3; + g_regimeData.allowCounterTrend = true; + g_regimeData.positionSizeMultiplier = 0.75; + g_regimeData.shouldTrade = true; + g_regimeData.useMeanReversion = true; // * FIX#373 + g_regimeData.strategyHint = "Range — trade extremes with MR"; + break; + case REGIME_CHOPPY: // * FIX#373: CHOPPY now has explicit case with useMeanReversion + { + g_regimeData.optimalRR = Regime_RangeRR; + g_regimeData.confidenceMultiplier = 0.75; + g_regimeData.requiredConfirmations = 4; + g_regimeData.allowCounterTrend = true; + g_regimeData.positionSizeMultiplier = 0.65; + g_regimeData.shouldTrade = true; + g_regimeData.useMeanReversion = true; // * FIX#373: CHOPPY = prime MR territory + g_regimeData.strategyHint = "Choppy — MR at range extremes only"; + break; + } + case REGIME_VOLATILE: + { + g_regimeData.optimalRR = Regime_VolatileRR; + g_regimeData.confidenceMultiplier = 0.7; + g_regimeData.requiredConfirmations = 5; + g_regimeData.allowCounterTrend = false; + g_regimeData.positionSizeMultiplier = 0.5; + // * v9.03 FIX: Allow trading in VOLATILE when strong HTF trend supports direction. + // Previously: shouldTrade=false always -> missed big moves where M15 was "volatile" + // but H1/H4 showed clear strong trend (e.g. Jan 27-28 EURUSD STRONG_BULL breakout). + // positionSizeMultiplier stays at 0.5 for safety (half size during volatile regime). + { + ENUM_MTF_DIRECTION volMTF = g_mtfAnalysis.overallDirection; + bool mtfSupportsBull = (volMTF == MTF_STRONG_BULLISH || volMTF == MTF_BULLISH); + bool mtfSupportsBear = (volMTF == MTF_STRONG_BEARISH || volMTF == MTF_BEARISH); + g_regimeData.shouldTrade = (mtfSupportsBull || mtfSupportsBear); + g_regimeData.strategyHint = g_regimeData.shouldTrade + ? "High volatility + HTF trend confirmed - trade at half size" + : "High volatility, no HTF direction - wait for clarity"; + } + break; + } + default: + g_regimeData.optimalRR = 1.5; + g_regimeData.confidenceMultiplier = 0.6; + g_regimeData.requiredConfirmations = 5; + g_regimeData.allowCounterTrend = false; + g_regimeData.positionSizeMultiplier = 0.5; + g_regimeData.shouldTrade = false; + g_regimeData.strategyHint = "Unclear - wait for clarity"; + } + g_regimeData.preferLongs = (g_regimeData.trendDirection > 0); + g_regimeData.preferShorts = (g_regimeData.trendDirection < 0); +} +string GetRegimeString(ENUM_MARKET_REGIME regime) +{ + switch(regime) + { + case REGIME_STRONG_TREND_UP: return "STRONG UPTREND"; + case REGIME_TREND_UP: return "UPTREND"; + case REGIME_WEAK_TREND_UP: return "WEAK UPTREND"; + case REGIME_RANGING_TIGHT: return "TIGHT RANGE"; + case REGIME_RANGING_WIDE: return "WIDE RANGE"; + case REGIME_WEAK_TREND_DOWN: return "WEAK DOWNTREND"; + case REGIME_TREND_DOWN: return "DOWNTREND"; + case REGIME_STRONG_TREND_DOWN: return "STRONG DOWNTREND"; + case REGIME_VOLATILE: return "VOLATILE"; + case REGIME_BREAKOUT: return "BREAKOUT"; + case REGIME_CHOPPY: return "CHOPPY"; + default: return "UNKNOWN"; + } +} +//+===================================================================+ +//| SECTION 10: WIN PROBABILITY | +//+===================================================================+ +void InitializeWinProbability() +{ + ZeroMemory(g_winProbData); + g_winProbValid = false; +} +void CalculateWinProbability_Detailed(bool isBullish, double trendScore, double structureScore, // * FIX v7.3: Renamed to avoid overload confusion + double zoneScore, double confluenceScore, + double timingScore, double patternScore) +{ + if(!WinProb_Enabled) return; + g_winProbData.trendScore = trendScore; + g_winProbData.structureScore = structureScore; + g_winProbData.zoneScore = zoneScore; + g_winProbData.confluenceScore = confluenceScore; + g_winProbData.timingScore = timingScore; + g_winProbData.patternScore = patternScore; + // * v9.16 FIX#47: Use working vars (AutoOpt-aware) instead of raw inputs + double weightedSum = trendScore * g_workingWP_TrendWeight + structureScore * g_workingWP_StructureWeight + + zoneScore * g_workingWP_ZoneWeight + confluenceScore * g_workingWP_ConfluenceWeight + + timingScore * g_workingWP_TimingWeight + patternScore * g_workingWP_PatternWeight; + g_winProbData.rawProbability = 0.50 + weightedSum * 0.35; + g_winProbData.rawProbability = MathMax(0.35, MathMin(0.85, g_winProbData.rawProbability)); + g_winProbData.adjustedProbability = g_winProbData.rawProbability; + // Regime adjustment + if(WinProb_AdjustForRegime && g_regimeValid) + { + if(g_regimeData.isTrending) + { + if((isBullish && g_regimeData.preferLongs) || (!isBullish && g_regimeData.preferShorts)) + g_winProbData.regimeAdjustment = 0.05; + else g_winProbData.regimeAdjustment = -0.10; + } + else if(g_regimeData.isRanging) g_winProbData.regimeAdjustment = -0.03; + else g_winProbData.regimeAdjustment = -0.08; + g_winProbData.adjustedProbability += g_winProbData.regimeAdjustment; + } + // News adjustment + if(WinProb_AdjustForNews && g_newsValid && !g_newsData.isNewsSafe) + { + g_winProbData.newsAdjustment = -0.10; + g_winProbData.adjustedProbability += g_winProbData.newsAdjustment; + } + // Time adjustment + if(WinProb_AdjustForTime && g_timeValid) + { + if(g_timeData.currentHourQuality == HOUR_EXCELLENT) g_winProbData.timeOfDayAdjust = 0.05; + else if(g_timeData.currentHourQuality == HOUR_GOOD) g_winProbData.timeOfDayAdjust = 0.02; + else if(g_timeData.currentHourQuality == HOUR_POOR) g_winProbData.timeOfDayAdjust = -0.05; + else if(g_timeData.currentHourQuality == HOUR_AVOID) g_winProbData.timeOfDayAdjust = -0.10; + else g_winProbData.timeOfDayAdjust = 0; + g_winProbData.adjustedProbability += g_winProbData.timeOfDayAdjust; + } + // Streak -- * FIX#18a: Cap losing streak penalty to prevent death spiral + if(g_currentStreak > 3) g_winProbData.streakAdjustment = MathMax(-0.04, -0.02 * (g_currentStreak - 3)); + else if(g_currentStreak < -3) g_winProbData.streakAdjustment = MathMax(-0.04, -0.02 * MathAbs(g_currentStreak + 3)); + else g_winProbData.streakAdjustment = 0; + g_winProbData.adjustedProbability += g_winProbData.streakAdjustment; + g_winProbData.finalProbability = MathMax(0.45, MathMin(0.80, g_winProbData.adjustedProbability)); // v9.04 FIX#1: floor 0.30->0.45 (30% floor caused mass REJECT in backtest with few trades) + // * v9.16 FIX#47d: Wire WinProb_MinThreshold + HighThreshold into quality classification + // BUG was: g_workingWP_MinThreshold bridged from AutoOpt but NEVER READ -> dead variable + // Now: MinThreshold gates quality tier -> used in dashboard + score weighting + if(g_winProbData.finalProbability >= WinProb_HighThreshold) + g_winProbData.qualityTier = 3; // HIGH quality signal + else if(g_winProbData.finalProbability >= g_workingWP_MinThreshold) + g_winProbData.qualityTier = 2; // MEDIUM quality signal + else + g_winProbData.qualityTier = 1; // LOW quality signal + if(g_totalHistoricalTrades >= WinProb_MinSamples) + { + g_winProbData.historicalWinRate = g_historicalWinRate; + g_winProbData.historicalSamples = g_totalHistoricalTrades; + // * v7.5b FIX: 30% blend of 16.1% historical WR was killing every signal. + // Historical WR is polluted by multi-TP counting bug and stale persistence data. + // Reduce to 15% blend AND only trust it if we have substantial recent data. + double blendWeight = (g_totalHistoricalTrades >= 50) ? 0.15 : 0.10; + g_winProbData.finalProbability = g_winProbData.finalProbability * (1.0 - blendWeight) + g_winProbData.historicalWinRate * blendWeight; + } + g_winProbData.confidence = 50 + MathMin(30, g_totalHistoricalTrades / 3) + (g_regimeValid ? 10 : 0) + (g_timeValid ? 10 : 0); + g_winProbData.confidence = MathMin(95, g_winProbData.confidence); + g_winProbData.standardError = 0.5 / MathSqrt(MathMax(1, g_totalHistoricalTrades)); + g_winProbData.lowerBound = g_winProbData.finalProbability - 2 * g_winProbData.standardError; + g_winProbData.upperBound = g_winProbData.finalProbability + 2 * g_winProbData.standardError; + g_winProbData.calculationTime = TimeCurrent(); + g_winProbValid = true; +} +//+===================================================================+ +//| SECTION 11: EXPECTED VALUE | +//+===================================================================+ +void InitializeExpectedValue() +{ + ZeroMemory(g_evData); + g_evValid = false; +} +void CalculateExpectedValue_Detailed(double winProbability, double avgWinR, double avgLossR) // * FIX v7.3: Renamed +{ + if(!EV_Enabled) return; + g_evData.winProbability = winProbability; + g_evData.avgWin = (avgWinR > 0) ? avgWinR : EV_DefaultAvgWin; + g_evData.avgLoss = (avgLossR > 0) ? avgLossR : EV_DefaultAvgLoss; + double lossProbability = 1.0 - winProbability; + g_evData.expectedValue = (winProbability * g_evData.avgWin) - (lossProbability * g_evData.avgLoss); + g_evData.evBestCase = (MathMin(winProbability + 0.1, 0.9) * g_evData.avgWin * 1.2) - (MathMax(lossProbability - 0.1, 0.1) * g_evData.avgLoss * 0.8); + g_evData.evWorstCase = (MathMax(winProbability - 0.1, 0.3) * g_evData.avgWin * 0.8) - (MathMin(lossProbability + 0.1, 0.7) * g_evData.avgLoss * 1.2); + g_evData.evMostLikely = g_evData.expectedValue; + double b = g_evData.avgWin / g_evData.avgLoss; + g_evData.kellyFraction = (b * winProbability - lossProbability) / b; + g_evData.kellyFraction = MathMax(0, MathMin(1, g_evData.kellyFraction)); + g_evData.optimalRisk = g_evData.kellyFraction * 100; + g_evData.maxRisk = EV_KellyFraction * g_evData.kellyFraction * 100; + g_evData.breakEvenWinRate = g_evData.avgLoss / (g_evData.avgWin + g_evData.avgLoss); + g_evData.expectedPer100 = g_evData.expectedValue * 100; + g_evData.variance = winProbability * lossProbability * MathPow(g_evData.avgWin + g_evData.avgLoss, 2); + g_evData.standardDev = MathSqrt(g_evData.variance); + g_evData.sharpeRatio = (g_evData.standardDev > 0) ? g_evData.expectedValue / g_evData.standardDev : 0; + g_evData.isPositiveEV = (g_evData.expectedValue > 0); + g_evData.isSignificant = (g_evData.expectedValue >= EV_MinPositive); + // * FIX#371: When EV_MinPositive == EV_HighEVThreshold (both 0.35 default), QUALITY_B + // is unreachable — every EV>=0.35 hits QUALITY_A, skipping QUALITY_B entirely. + // FIX: keep the existing if/else chain but document that user should set + // EV_MinPositive < EV_HighEVThreshold for QUALITY_B to be meaningful. + // Recommended: EV_MinPositive=0.10, EV_HighEVThreshold=0.35. + if(g_evData.expectedValue >= EV_HighEVThreshold) { g_evData.evQuality = QUALITY_A; g_evData.recommendation = "HIGH EV - Full position"; } + else if(g_evData.expectedValue >= EV_MinPositive) { g_evData.evQuality = QUALITY_B; g_evData.recommendation = "POSITIVE EV - Standard position"; } + else if(g_evData.expectedValue >= 0) { g_evData.evQuality = QUALITY_C; g_evData.recommendation = "MARGINAL - Reduced position"; } + else { g_evData.evQuality = QUALITY_F; g_evData.recommendation = "NEGATIVE EV - Do not trade"; } + g_evData.calculationTime = TimeCurrent(); + g_evData.sampleSize = g_totalHistoricalTrades; + g_evValid = true; +} +//+===================================================================+ +//| SECTION 12: POSITION SIZE | +//+===================================================================+ +void InitializePositionSize() { ZeroMemory(g_posSizeData); g_posSizeValid = false; } +void CalculatePositionSize(double stopLossPoints, int confidence) +{ + if(!PosSize_Enabled) return; + g_posSizeData.accountBalance = AccountInfoDouble(ACCOUNT_BALANCE); + // * FIX#P1a: Cap risk base at BacktestInitialBalance. + // PROBLEM: After wins equity compounds (e.g. $10k→$50k). Kelly then calculates + // lots on $50k equity → catastrophic loss on first big SL hit → EA removed itself at 7% of test. + // FIX: Never risk more $ than we would have on initial capital. + // In live trading: if equity < initial balance (drawdown), use current equity (conservative). + // In live trading: if equity > initial balance (profits), cap at initial (protect gains). + // IMPORTANT: accountEquity (capped) is used ONLY for lot sizing (baseRiskAmount, adjustedRiskAmount). + // The raw equity is preserved in _rawEquity and used for DD% monitoring — if we used the capped + // value there, the DD check would read false 80% drawdown after every win and incorrectly reduce lots. + double _rawEquity = AccountInfoDouble(ACCOUNT_EQUITY); + double _refEquity = (g_effectiveBIB > 0 && _rawEquity > g_effectiveBIB) + ? g_effectiveBIB : _rawEquity; + g_posSizeData.accountEquity = _refEquity; // capped — for lot sizing only + g_posSizeData.baseRiskPercent = EA_RiskPercent; + g_posSizeData.baseRiskAmount = g_posSizeData.accountEquity * (EA_RiskPercent / 100); + double totalMultiplier = 1.0; + // * v10.33 FIX#327: old PosSize_UseConfidence block removed — FIX#321 handles score-based sizing below. + // Double-counting was causing score=77 to get MORE lots than score=83 due to formula mismatch. + // * v10.33 FIX#327: old regime block removed — handled inside FIX#321 below. + // ================================================================ + // * v10.32 FIX#321: SCORE-BASED SIZING + EQUITY COMPOUNDING + // ================================================================ + // Score = ποιότητα setup = αποκλειστικός οδηγός lot size. + // Loss streak δεν κόβει lots — score και equity καθορίζουν το μέγεθος. + // Score=99 μετά 8 SLs → MAXIMUM lots. Score=45 μετά 8 wins → MINIMUM lots. + + // STEP 1: Score tier multiplier — primary driver + double scoreMultiplier; + if(confidence >= 90) scoreMultiplier = 1.40; // A+: maximum + else if(confidence >= 72) scoreMultiplier = 1.20; // A: strong + else if(confidence >= 58) scoreMultiplier = 1.00; // B: standard + else if(confidence >= 44) scoreMultiplier = 0.80; // C: marginal + else scoreMultiplier = 0.60; // D: minimum + g_posSizeData.confidenceMultiplier = scoreMultiplier; + totalMultiplier *= scoreMultiplier; + + // STEP 2: Equity compounding — real equity vs initial balance + // +20% equity → lots ×1.16 | -10% equity → lots ×0.92 + if(PosSize_UseCompounding) + { + double _initBal321 = (g_effectiveBIB > 0) ? g_effectiveBIB : g_posSizeData.accountBalance; + double _growth321 = (_initBal321 > 0) ? (_rawEquity - _initBal321) / _initBal321 : 0.0; + double compMult321 = 1.0 + (_growth321 * PosSize_CompoundFactor); + compMult321 = MathMax(PosSize_CompoundMaxCut, MathMin(PosSize_CompoundMaxBoost, compMult321)); + totalMultiplier *= compMult321; + if(MathAbs(_growth321) > 0.01) + PrintFormat("* FIX#321 Compound: equity=$%.0f init=$%.0f growth=%.1f%% x%.3f", + _rawEquity, _initBal321, _growth321*100.0, compMult321); + } + + // STEP 3: Drawdown protection — FIX#376: use g_currentDailyDD (consistent with FTMO gate) + // OLD: (balance - equity) / balance = floating-loss only. + // BUG: balance=$9k (realized loss), equity=$9k (no open trades) → step3 DD=0% ← WRONG. + // Completely blind to realized losses when no positions are open. + // NEW: g_currentDailyDD = CalculateDailyDrawdown() already captures BOTH realized AND floating + // losses via (startLevel - MathMin(balance,equity)) / startLevel. + // Updated by CheckDailyDrawdownLimit() which always runs before CalculatePositionSize. + // Consistent with CalculateDailyDrawdown(), g_currentDailyDD dashboard, and FTMO monitoring. + if(PosSize_UseDrawdown) + { + if(g_currentDailyDD >= PosSize_DDThreshold) // * FIX#376: was local (balance-equity)/balance formula + { + g_posSizeData.drawdownMultiplier = PosSize_DDReduction; + totalMultiplier *= g_posSizeData.drawdownMultiplier; + } + } + + // STEP 4: Regime multiplier + if(PosSize_UseRegime && g_regimeValid) + { + if(g_regimeData.isTrending) g_posSizeData.regimeMultiplier = g_workingPosSize_TrendingBonus; + else if(g_regimeData.isRanging) g_posSizeData.regimeMultiplier = g_workingPosSize_RangingPenalty; + else g_posSizeData.regimeMultiplier = 0.8; + totalMultiplier *= g_posSizeData.regimeMultiplier; + } + + // STEP 5: Correlation guard + if(g_corrValid && g_corrData.isOverexposed) + { + g_posSizeData.correlationMultiplier = Corr_ReductionFactor; + totalMultiplier *= g_posSizeData.correlationMultiplier; + } + + // Streak multiplier removed (FIX#321) — score tier handles quality + g_posSizeData.streakMultiplier = 1.0; + + // Cap: score-based + double multCap; + if(confidence >= 90) multCap = 3.0; // A+ + else if(confidence >= 72) multCap = 2.5; // A + else if(confidence >= 58) multCap = 2.0; // B + else multCap = 1.5; // C/D + g_posSizeData.finalMultiplier = MathMax(0.25, MathMin(multCap, totalMultiplier)); + // * v10.31 FIX#320/P1: CHOPPY regime lot reduction from pair table + // g_workingChoppyLotMult=0.60 for H4 in CHOPPY/RANGING — even valid setups get 60% lots + // Uncertainty premium: CHOPPY = lower conviction = smaller risk + if(g_workingChoppyLotMult > 0 && g_workingChoppyLotMult < 1.0 && g_regimeValid) + { + ENUM_MARKET_REGIME _regPS = g_regimeData.regime; + if(_regPS == REGIME_CHOPPY || _regPS == REGIME_RANGING || + _regPS == REGIME_RANGING_TIGHT || _regPS == REGIME_RANGING_WIDE) + { + g_posSizeData.finalMultiplier *= g_workingChoppyLotMult; + g_posSizeData.finalMultiplier = MathMax(0.25, g_posSizeData.finalMultiplier); + if(g_verboseLog) + PrintFormat("* FIX#320/P1 CHOPPY lot reduction: ×%.2f (regime=%s)", + g_workingChoppyLotMult, EnumToString(_regPS)); + } + } + // * FIX#386: DYNAMIC RISK — AutoOpt baseline × score tier multiplier. + // + // OLD (broken) model: + // tierRisk = _effectiveRiskMax × tierFraction (e.g. 1.8% × 1.00 = 1.8% for A+) + // autoOptFactor = autoOpt.risk_pct / _effectiveRiskMax (e.g. 1.0/1.8 = 0.556) + // effectiveRisk = tierRisk × autoOptFactor (1.8% × 0.556 = 1.0%) ← KILLS tier! + // Result: A+ trade and D trade get identical lots when AutoOpt reduces risk. + // + // NEW (correct) model: + // autoOpt_base = g_autoOptParams.risk_pct (reflects real-time conditions) + // set to EA_RiskPercent, then reduced by AutoOpt step-by-step: + // × 0.60 bad session | × 0.50 very bad | × 0.30 pre-news | etc. + // This IS the dynamic environment signal — it's what AutoOpt is for. + // tierMult = scoreMultiplier (1.40 A+, 1.20 A, 1.00 B, ...) + // effectiveRisk = autoOpt_base × tierMult + // → A+ in good session: 1.0% × 1.40 = 1.40% (capped at ceiling 1.80%) + // → A+ in bad session: 0.6% × 1.40 = 0.84% (still MORE than D in same session) + // → D in good session: 1.0% × 0.60 = 0.60% (correctly penalised) + // → D in bad session: 0.6% × 0.60 = 0.36% (doubly penalised — right) + // Hard ceiling: never exceeds _effectiveRiskMax (FTMO-safe absolute cap) + // Hard floor: never below PosSize_MinRisk (always take some risk on valid setups) + + string tierLabel; + // * FIX#390: Two-level ceiling system + // Level 1: g_workingRiskCeiling = BASE risk για ALL TFs = EA_RiskPercent (FIX#317b) + // AutoOpt reduces this based on session/spread/news + // Level 2: HARD CEILING per tier — A+ can go up to 5%, A up to 4%, B/C/D up to base + // This allows top setups to truly scale up, while DD protection (4.5% daily) keeps FTMO safe + // Floor: PosSize_MinRisk = 1.0% — never risk less than this on any valid setup + double _baseRisk = g_workingRiskCeiling; // = EA_RiskPercent για όλους τους TF (FIX#317b) + + // ── Pair table risk[tf] = single calibration point per symbol+TF ────────── + // g_autoOptParams.risk_pct holds cfg.risk[tf] from ApplyPairTFProfile. + // This is the B-tier baseline (score 58-72 = 100% of the TF budget). + // Score tier scales proportionally within it; regime/compounding also apply + // but are bounded so no multiplier stack can blow the FTMO limits. + // + // Ceiling = pairRisk × 1.50 (A+ + trending — hard max per TF) + // Floor = pairRisk × 0.40 (D + DD penalty — hard min per TF) + // + // Examples: EURUSD H1 (risk[2]=1.50%), EURUSD H4 (risk[3]=1.80%) + // H1 A+ trending: 1.50% × 1.40 × 1.20 = 2.52% → cap 2.25% (1.50×1.50) + // H1 D with DD: 1.50% × 0.60 × 0.70 = 0.63% → floor 0.60% (1.50×0.40) + // H4 A+ trending: 1.80% × 1.40 × 1.20 = 3.02% → cap 2.70% (1.80×1.50) + // H4 D with DD: 1.80% × 0.60 × 0.70 = 0.76% → floor 0.72% (1.80×0.40) + + double _pairRisk = (AutoOpt_Enabled && g_autoOptParams.risk_pct > 0) + ? g_autoOptParams.risk_pct // pair table risk[tf] via AutoOpt + : _baseRisk; // fallback: EA_RiskPercent + + // Hard ceiling and floor anchored to pair table risk + double _tfCeiling = _pairRisk * 1.50; + double _tfFloor = _pairRisk * 0.40; + + // Ensure floor never drops below the global PosSize_MinRisk (1.0%) + _tfFloor = MathMax(PosSize_MinRisk, _tfFloor); + + // Score tier label for logging + if(confidence >= 90) tierLabel = "A+"; + else if(confidence >= 72) tierLabel = "A"; + else if(confidence >= 55) tierLabel = "B"; + else if(confidence >= 44) tierLabel = "C"; + else tierLabel = "D"; + + // AutoOpt baseline — the dynamic environment signal for this bar. + // AutoOpt reduces from EA_RiskPercent based on session/spread/news. + // Clamped so it never exceeds the pair table risk (already the max budget). + double autoOpt_base = (AutoOpt_Enabled && g_autoOptParams.risk_pct > 0) + ? MathMin(g_autoOptParams.risk_pct, _pairRisk) + : _pairRisk; + autoOpt_base = MathMax(PosSize_MinRisk, autoOpt_base); + + // Dynamic risk: baseline × score tier (A+ gets ×1.40 of the budget, D gets ×0.60) + double effectiveRisk = autoOpt_base * g_posSizeData.confidenceMultiplier; + + // Apply all remaining multipliers (compounding, DD, regime, correlation) + // Both pre-clamp and post-multiply are bounded by [_tfFloor … _tfCeiling] + effectiveRisk = MathMax(_tfFloor, MathMin(_tfCeiling, effectiveRisk)); + + g_posSizeData.adjustedRiskPercent = MathMax(_tfFloor, + MathMin(_tfCeiling, + effectiveRisk * g_posSizeData.finalMultiplier)); + + if(g_verboseLog) + { + static double _s386LastRisk = -1; + if(MathAbs(g_posSizeData.adjustedRiskPercent - _s386LastRisk) > 0.001) + { + PrintFormat("[PosSize] tier=%s | pairRisk=%.2f%% | base=%.2f%% × score=%.2f → %.2f%% | ×mult=%.3f → %.2f%% | bounds=[%.2f%%..%.2f%%]", + tierLabel, _pairRisk, autoOpt_base, + g_posSizeData.confidenceMultiplier, + autoOpt_base * g_posSizeData.confidenceMultiplier, + g_posSizeData.finalMultiplier, + g_posSizeData.adjustedRiskPercent, + _tfFloor, _tfCeiling); + _s386LastRisk = g_posSizeData.adjustedRiskPercent; + } + } + g_posSizeData.adjustedRiskAmount = g_posSizeData.accountEquity * (g_posSizeData.adjustedRiskPercent / 100); // accountEquity already capped by FIX#P1a + double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); + double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); + g_posSizeData.pipValue = tickValue / tickSize * _Point; + g_posSizeData.riskPerLot = stopLossPoints * g_posSizeData.pipValue; + if(g_posSizeData.riskPerLot > 0) g_posSizeData.finalLotSize = g_posSizeData.adjustedRiskAmount / g_posSizeData.riskPerLot; + else g_posSizeData.finalLotSize = 0.01; + g_posSizeData.minLotSize = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); + g_posSizeData.maxLotSize = (g_workingMaxLot > 0) ? g_workingMaxLot : SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); // * FIX#314: pair table max lot cap + g_posSizeData.lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); + g_posSizeData.finalLotSize = MathFloor(g_posSizeData.finalLotSize / g_posSizeData.lotStep) * g_posSizeData.lotStep; + g_posSizeData.finalLotSize = MathMax(g_posSizeData.minLotSize, MathMin(g_posSizeData.maxLotSize, g_posSizeData.finalLotSize)); + g_posSizeData.calculationTime = TimeCurrent(); + g_posSizeValid = true; +} +//+===================================================================+ +//| SECTION 13: NEWS FILTER | +//+===================================================================+ +void CheckNewsImpact() +{ + if(!News_FilterEnabled) { g_newsData.isNewsSafe = true; g_newsValid = true; return; } + // * FIX#409: Use TimeGMT() not TimeCurrent(). + // ROOT CAUSE: TimeCurrent() = broker local time (FTMO = GMT+2). Hardcoded news times + // (13:30, 14:00, 14:30, 15:00) are UTC. On FTMO broker: NFP = 13:30 UTC = 15:30 broker. + // With TimeCurrent(): filter fired at 13:30 BROKER = 11:30 UTC — 2 hours BEFORE real news, + // blocking valid London Close setups. Real NFP window (15:30 broker) was NOT covered. + // FIX: TimeGMT() always returns UTC regardless of broker timezone. + MqlDateTime dt; TimeToStruct(TimeGMT(), dt); + int totalMinutes = dt.hour * 60 + dt.min; + g_newsData.isNewsSafe = true; + g_newsData.hasUpcomingHigh = false; + g_newsData.inNewsBlackout = false; + g_newsData.minsToNextHigh = 999999; + int usNewsTimes[] = {13*60+30, 14*60, 14*60+30, 15*60}; + for(int i = 0; i < ArraySize(usNewsTimes); i++) + { + int minsToNews = MathAbs(totalMinutes - usNewsTimes[i]); + if(minsToNews <= News_MinsBeforeHigh || (1440 - minsToNews) <= News_MinsBeforeHigh) + { + g_newsData.hasUpcomingHigh = true; + g_newsData.inNewsBlackout = true; + g_newsData.isNewsSafe = false; + g_newsData.minsToNextHigh = minsToNews; + break; + } + } + if(g_newsData.inNewsBlackout) + { + g_newsData.allowNewTrades = false; + g_newsData.positionMultiplier = News_ReducedRisk; + g_newsData.recommendation = "NEWS RISK - Avoid new trades"; + } + else + { + g_newsData.allowNewTrades = true; + g_newsData.positionMultiplier = 1.0; + g_newsData.recommendation = "News clear"; + } + string symbol = _Symbol; + g_newsData.affectsUSD = (StringFind(symbol, "USD") >= 0) && News_CheckUSD; + g_newsData.affectsEUR = (StringFind(symbol, "EUR") >= 0) && News_CheckEUR; + g_newsData.affectsGBP = (StringFind(symbol, "GBP") >= 0) && News_CheckGBP; + g_newsData.affectsJPY = (StringFind(symbol, "JPY") >= 0) && News_CheckJPY; + g_newsData.affectsCurrentPair = g_newsData.affectsUSD || g_newsData.affectsEUR || g_newsData.affectsGBP || g_newsData.affectsJPY; + g_newsData.lastUpdate = TimeCurrent(); + g_newsValid = true; +} +//+===================================================================+ +//| SECTION 14: CORRELATION FILTER | +//+===================================================================+ +void InitializeCorrelation() { ZeroMemory(g_corrData); g_corrValid = false; } +double CalculatePairCorrelation(string pair) +{ + if(_Symbol == pair) return 1.0; + // * v9.25 FIX#86: Silent existence check BEFORE SymbolSelect(true). + // SymbolSelect(true) prints "symbol X does not exist" to Journal for every non-existent symbol. + // On FTMO-Demo with 131 symbols, US500/US100 generate 2000+ errors per session. + // Fix: Check SYMBOL_DIGITS property silently (returns 0 if symbol doesn't exist on broker). + // Only attempt SymbolSelect if symbol is already known or digits > 0. + if(SymbolInfoInteger(pair, SYMBOL_DIGITS) == 0 && !SymbolSelect(pair, false)) return 0; + if(!SymbolSelect(pair, true)) return 0; + double c1[], c2[]; ArraySetAsSeries(c1, true); ArraySetAsSeries(c2, true); + if(CopyClose(_Symbol, PERIOD_H1, 0, Corr_Period, c1) < Corr_Period) return 0; + if(CopyClose(pair, PERIOD_H1, 0, Corr_Period, c2) < Corr_Period) return 0; + double mean1 = 0, mean2 = 0; + for(int i = 0; i < Corr_Period; i++) { mean1 += c1[i]; mean2 += c2[i]; } + mean1 /= Corr_Period; mean2 /= Corr_Period; + double sumXY = 0, sumX2 = 0, sumY2 = 0; + for(int i = 0; i < Corr_Period; i++) + { + double dx = c1[i] - mean1, dy = c2[i] - mean2; + sumXY += dx * dy; sumX2 += dx * dx; sumY2 += dy * dy; + } + if(sumX2 == 0 || sumY2 == 0) return 0; + return sumXY / MathSqrt(sumX2 * sumY2); +} +void CalculateCorrelations() +{ + if(!Corr_FilterEnabled) return; + // * v9.16 FIX#48: Corr_UpdateMinutes gates update frequency (was dead input) + static datetime lastCorrUpdate = 0; + if(g_workingCorr_UpdateMins > 0 && (TimeCurrent() - lastCorrUpdate) < g_workingCorr_UpdateMins * 60) return; // * FIX#48: use working var (AutoOpt TF-scaled) + lastCorrUpdate = TimeCurrent(); + g_corrData.symbol = _Symbol; + g_corrData.baseCurrency = StringSubstr(_Symbol, 0, 3); + g_corrData.quoteCurrency = StringSubstr(_Symbol, 3, 3); + // * v9.16 FIX#48: Corr_Pairs dynamic parsing (was dead input -- hardcoded EURUSD/GBPUSD/USDJPY/XAUUSD) + g_corrData.corrEURUSD = 0; g_corrData.corrGBPUSD = 0; g_corrData.corrUSDJPY = 0; g_corrData.corrXAUUSD = 0; + string corrPairsArray[]; + int numPairs = StringSplit(Corr_Pairs, ',', corrPairsArray); + for(int cp = 0; cp < numPairs; cp++) + { + string pair = corrPairsArray[cp]; + StringTrimLeft(pair); StringTrimRight(pair); + double corr = CalculatePairCorrelation(pair); + if(pair == "EURUSD") g_corrData.corrEURUSD = corr; + else if(pair == "GBPUSD") g_corrData.corrGBPUSD = corr; + else if(pair == "USDJPY") g_corrData.corrUSDJPY = corr; + else if(pair == "XAUUSD") g_corrData.corrXAUUSD = corr; + } + g_corrData.longPositions = 0; g_corrData.shortPositions = 0; + g_corrData.totalLongExposure = 0; g_corrData.totalShortExposure = 0; + g_corrData.usdExposure = 0; g_corrData.eurExposure = 0; g_corrData.gbpExposure = 0; g_corrData.jpyExposure = 0; + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + ulong ticket = PositionGetTicket(i); + if(ticket > 0 && PositionSelectByTicket(ticket)) + { + double lots = PositionGetDouble(POSITION_VOLUME); + ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); + if(posType == POSITION_TYPE_BUY) { g_corrData.longPositions++; g_corrData.totalLongExposure += lots; } + else { g_corrData.shortPositions++; g_corrData.totalShortExposure += lots; } + } + } + g_corrData.netExposure = g_corrData.totalLongExposure - g_corrData.totalShortExposure; + double totalExposure = MathAbs(g_corrData.totalLongExposure) + MathAbs(g_corrData.totalShortExposure); + if(totalExposure == 0) g_corrData.exposureLevel = EXPOSURE_NONE; + else if(totalExposure < Corr_MaxExposure * 0.3) g_corrData.exposureLevel = EXPOSURE_LOW; + else if(totalExposure < Corr_MaxExposure * 0.7) g_corrData.exposureLevel = EXPOSURE_MEDIUM; + else if(totalExposure < Corr_MaxExposure) g_corrData.exposureLevel = EXPOSURE_HIGH; + else g_corrData.exposureLevel = EXPOSURE_OVEREXPOSED; + g_corrData.isOverexposed = (g_corrData.exposureLevel == EXPOSURE_OVEREXPOSED); + if(g_corrData.isOverexposed) { g_corrData.allowNewPosition = false; g_corrData.positionMultiplier = 0; g_corrData.recommendation = "OVER-EXPOSED"; } + else if(g_corrData.exposureLevel == EXPOSURE_HIGH) { g_corrData.allowNewPosition = true; g_corrData.positionMultiplier = Corr_ReductionFactor; g_corrData.recommendation = "HIGH exposure"; } + else { g_corrData.allowNewPosition = true; g_corrData.positionMultiplier = 1.0; g_corrData.recommendation = "Exposure OK"; } + g_corrData.lastCalculation = TimeCurrent(); + g_corrData.dataValid = true; + g_corrValid = true; +} +//+===================================================================+ +//| SECTION 15: TIME ANALYSIS | +//+===================================================================+ +void CheckCurrentTimeQuality() +{ + if(!Time_AnalysisEnabled) { g_timeData.isOptimalTime = true; g_timeData.shouldAvoidNow = false; g_timeValid = true; return; } + // * FIX#413: Use TimeGMT() not TimeCurrent(). + // ROOT CAUSE: TimeCurrent() = broker local (GMT+2). Hour thresholds below are UTC-based + // (London 7-10, NY 13-16). With broker time: 7-10 broker = 5-8 UTC = pre-London gap. + // Fix: TimeGMT() always UTC — hours now match ICT session definitions correctly. + MqlDateTime dt; TimeToStruct(TimeGMT(), dt); + int currentHour = dt.hour, currentDay = dt.day_of_week; + if((currentHour >= 7 && currentHour <= 10) || (currentHour >= 13 && currentHour <= 16)) + { g_timeData.currentHourQuality = HOUR_EXCELLENT; g_timeData.currentTimeScore = 85; } + else if((currentHour >= 6 && currentHour <= 11) || (currentHour >= 12 && currentHour <= 17)) + { g_timeData.currentHourQuality = HOUR_GOOD; g_timeData.currentTimeScore = 70; } + else if(currentHour >= 18 && currentHour <= 21) + { g_timeData.currentHourQuality = HOUR_AVERAGE; g_timeData.currentTimeScore = 55; } + else if(currentHour >= Time_AvoidHoursStart || currentHour <= Time_AvoidHoursEnd) + { g_timeData.currentHourQuality = HOUR_AVOID; g_timeData.currentTimeScore = 30; } + else { g_timeData.currentHourQuality = HOUR_POOR; g_timeData.currentTimeScore = 45; } + if(currentDay >= 2 && currentDay <= 4) g_timeData.currentDayQuality = DAY_EXCELLENT; + else if(currentDay == 1 && currentHour >= 8) g_timeData.currentDayQuality = DAY_GOOD; + else if(currentDay == 5 && currentHour < 14) g_timeData.currentDayQuality = DAY_GOOD; + else if(currentDay == 5 && currentHour >= 14) g_timeData.currentDayQuality = DAY_AVOID; + else if(currentDay == 0 || currentDay == 6) g_timeData.currentDayQuality = DAY_AVOID; + else g_timeData.currentDayQuality = DAY_AVERAGE; + g_timeData.isOptimalTime = (g_timeData.currentHourQuality <= HOUR_GOOD && g_timeData.currentDayQuality <= DAY_GOOD); + g_timeData.shouldAvoidNow = (g_timeData.currentHourQuality == HOUR_AVOID || g_timeData.currentDayQuality == DAY_AVOID); + if(Time_AvoidFridayPM && currentDay == 5 && currentHour >= 14) g_timeData.shouldAvoidNow = true; + if(Time_AvoidMondayAM && currentDay == 1 && currentHour < 8) g_timeData.shouldAvoidNow = true; + if(Time_AvoidWeekends && (currentDay == 0 || currentDay == 6)) g_timeData.shouldAvoidNow = true; + if(g_timeData.currentHourQuality == HOUR_EXCELLENT) { g_timeData.confidenceMultiplier = Time_BestHourBonus; g_timeData.positionMultiplier = Time_BestHourBonus; } + else if(g_timeData.currentHourQuality == HOUR_AVOID) { g_timeData.confidenceMultiplier = Time_WorstHourPenalty; g_timeData.positionMultiplier = Time_WorstHourPenalty; } + else { g_timeData.confidenceMultiplier = 1.0; g_timeData.positionMultiplier = 1.0; } + if(g_timeData.shouldAvoidNow) g_timeData.recommendation = "AVOID - Poor time"; + else if(g_timeData.isOptimalTime) g_timeData.recommendation = "OPTIMAL - Best time"; + else g_timeData.recommendation = "OK - Acceptable"; + g_timeData.lastUpdate = TimeCurrent(); + g_timeValid = true; +} +//+===================================================================+ +//| SECTION 16: SMART ENTRY DECISION | +//+===================================================================+ +void MakeSmartEntryDecision(bool isBullish, double trendScore, double structureScore, + double zoneScore, double confluenceScore, double timingScore, double patternScore, int minScore) +{ + ZeroMemory(g_smartEntry); + g_smartEntry.trendScore = trendScore * 25; + g_smartEntry.structureScore = structureScore * 20; + g_smartEntry.zoneScore = zoneScore * 15; + g_smartEntry.confluenceScore = confluenceScore * 20; + g_smartEntry.timingScore = timingScore * 10; + g_smartEntry.patternScore = patternScore * 10; + g_smartEntry.rawScore = (int)(g_smartEntry.trendScore + g_smartEntry.structureScore + g_smartEntry.zoneScore + g_smartEntry.confluenceScore + g_smartEntry.timingScore + g_smartEntry.patternScore); + g_smartEntry.adjustedScore = g_smartEntry.rawScore; + g_smartEntry.showInfo = SmartEntry_ShowInfo; // [v6.42] + if(g_regimeValid) { g_smartEntry.regimeAdjustment = (g_regimeData.confidenceMultiplier - 1.0) * 10; g_smartEntry.adjustedScore += (int)g_smartEntry.regimeAdjustment; g_smartEntry.regime = g_regimeData.regime; } + if(g_newsValid && !g_newsData.isNewsSafe) { g_smartEntry.newsAdjustment = -15; g_smartEntry.adjustedScore += (int)g_smartEntry.newsAdjustment; g_smartEntry.newsImpact = true; } + if(g_corrValid && g_corrData.isOverexposed) { g_smartEntry.correlationAdjust = -20; g_smartEntry.adjustedScore += (int)g_smartEntry.correlationAdjust; } + if(g_timeValid) { g_smartEntry.timeAdjustment = (g_timeData.confidenceMultiplier - 1.0) * 15; g_smartEntry.adjustedScore += (int)g_smartEntry.timeAdjustment; } + // * v7.6 FIX: Cap loss streak penalty at -6 max (was streak*2 = up to -20!) + // Old: streak=-5 -> adjustedScore -= 10 -> score-70 became 60 -> REJECTED while score-47 still passed! + // New: max -6 penalty regardless of streak length -- prevents paradox of rejecting GOOD signals + if(g_currentStreak < -3) { g_smartEntry.streakAdjustment = MathMax(-6, g_currentStreak * 2); g_smartEntry.adjustedScore += (int)g_smartEntry.streakAdjustment; } + g_smartEntry.adjustedScore = MathMax(0, MathMin(100, g_smartEntry.adjustedScore)); + CalculateWinProbability_Detailed(isBullish, trendScore, structureScore, zoneScore, confluenceScore, timingScore, patternScore); // * FIX v7.3: Renamed + g_smartEntry.winProbability = g_winProbData.finalProbability; + CalculateExpectedValue_Detailed(g_smartEntry.winProbability, g_historicalAvgWin, g_historicalAvgLoss); // * FIX v7.3: Renamed + g_smartEntry.expectedValue = g_evData.expectedValue; + g_smartEntry.breakEvenWinRate = g_evData.breakEvenWinRate; + g_smartEntry.isPositiveEV = g_evData.isPositiveEV; + // * v9.22 FIX-F (Block 2/3): Recalibrated for EvaluateSmartEntry real max~153 + // OLD (FIX#17): adjustedScore>=50=A+ -> misleading (only 33% of real max) + if(g_smartEntry.adjustedScore >= 90) g_smartEntry.quality = QUALITY_A_PLUS; + else if(g_smartEntry.adjustedScore >= 72) g_smartEntry.quality = QUALITY_A; + else if(g_smartEntry.adjustedScore >= 58) g_smartEntry.quality = QUALITY_B; + else if(g_smartEntry.adjustedScore >= 44) g_smartEntry.quality = QUALITY_C; + else if(g_smartEntry.adjustedScore >= 32) g_smartEntry.quality = QUALITY_D; + else g_smartEntry.quality = QUALITY_F; + g_smartEntry.shouldEnter = false; + g_smartEntry.rejectReason = ""; + if(g_smartEntry.adjustedScore < minScore) g_smartEntry.rejectReason = StringFormat("Score %d < Min %d", g_smartEntry.adjustedScore, minScore); + else if(g_newsValid && !g_newsData.allowNewTrades) g_smartEntry.rejectReason = "News blackout"; + else if(g_corrValid && !g_corrData.allowNewPosition) g_smartEntry.rejectReason = "Over-exposed"; + else if(g_timeValid && g_timeData.shouldAvoidNow) g_smartEntry.rejectReason = "Poor trading time"; + else if(g_regimeValid && !g_regimeData.shouldTrade) g_smartEntry.rejectReason = "Unfavorable regime"; + else if(EV_RequirePositiveEV && !g_smartEntry.isPositiveEV) g_smartEntry.rejectReason = "Negative EV"; + else if(g_smartEntry.winProbability < g_workingWP_MinThreshold) g_smartEntry.rejectReason = StringFormat("Win prob %.1f%% < %.1f%%", g_smartEntry.winProbability * 100, g_workingWP_MinThreshold * 100); // * FIX#47 + else + { + g_smartEntry.shouldEnter = true; + g_smartEntry.entryReason = StringFormat("Score:%d WinP:%.0f%% EV:%.2fR", g_smartEntry.adjustedScore, g_smartEntry.winProbability * 100, g_smartEntry.expectedValue); + } + if(g_smartEntry.shouldEnter) + { + // * v9.26 FIX#91: Use ACTUAL trade SL distance, not ATRx1.5 approximation. + // MakeSmartEntryDecision has no entryPrice/slPrice params -> use g_ea_signal which is + // already populated by the caller (SelectBestCandidate sets g_ea_signal before calling here). + // On EURUSD H4: structural SL=14p, ATRx1.5=28p -> old code doubled intended risk per trade. + double actualSLPoints = (g_ea_signal.stopLoss > 0 && g_ea_signal.entryPrice > 0) ? + MathAbs(g_ea_signal.entryPrice - g_ea_signal.stopLoss) / _Point : + g_cachedATR * 1.5 / _Point; // fallback only if signal not yet set + CalculatePositionSize(actualSLPoints, g_smartEntry.adjustedScore); + g_smartEntry.recommendedRisk = g_posSizeData.adjustedRiskPercent; + g_smartEntry.recommendedLots = g_posSizeData.finalLotSize; + g_smartEntry.positionMultiplier = g_posSizeData.finalMultiplier; + g_smartEntry.kellyFraction = g_evData.kellyFraction; + g_smartEntry.optimalRR = g_regimeValid ? g_regimeData.optimalRR : 2.0; + } + g_smartEntry.amdPhase = g_amdData.phase; + g_smartEntry.decisionTime = TimeCurrent(); + g_smartEntry.barsUntilExpiry = 10; + g_smartEntryValid = true; +} +string GetEntryQualityString(ENUM_ENTRY_QUALITY quality) +{ + switch(quality) { case QUALITY_A_PLUS: return "A+"; case QUALITY_A: return "A"; case QUALITY_B: return "B"; case QUALITY_C: return "C"; case QUALITY_D: return "D"; default: return "F"; } +} +//+===================================================================+ +//| SECTION 17: UTILITY & MASTER FUNCTIONS | +//+===================================================================+ +void UpdateCachedATR(int period) +{ + double atrBuf[]; + ArraySetAsSeries(atrBuf, true); + // Use existing global handle if available + if(g_atrHandle != INVALID_HANDLE) + { + if(CopyBuffer(g_atrHandle, 0, 0, 1, atrBuf) > 0) + { + g_cachedATR = atrBuf[0]; + } + } + else + { + // Create temporary handle only if global doesn't exist + int handle = iATR(_Symbol, PERIOD_CURRENT, period); + if(handle != INVALID_HANDLE) + { + if(CopyBuffer(handle, 0, 0, 1, atrBuf) > 0) + g_cachedATR = atrBuf[0]; + IndicatorRelease(handle); + } + } +} +void UpdateTradingStreak(bool isWin) +{ + if(isWin) + { + g_currentStreak = (g_currentStreak > 0) ? g_currentStreak + 1 : 1; + if(g_currentStreak > g_maxWinStreak) g_maxWinStreak = g_currentStreak; + // * v9.13 FIX#23a: Changed from 2->1 consecutive wins to reset. + // BUG: Multi-TP creates 3 deals per entry (TP1=Win, TP2=SL=Loss, TP3=SL=Loss). + // With "2 consecutive wins" requirement, the TP2/TP3 SL losses ALWAYS interrupt + // the win counter before 2 wins accumulate -> lossStreak=18, 12 days without trades. + // One genuine TP1 win proves the strategy is working -- reset immediately. + g_winsToResetMTF++; + if(g_winsToResetMTF >= 1) + { + g_lossStreakForMTF = 0; + g_winsToResetMTF = 0; + Print("* v9.13 FIX#23a MTF STREAK RESET | win detected | lossStreak -> 0"); + } + } + else + { + g_currentStreak = (g_currentStreak < 0) ? g_currentStreak - 1 : -1; + if(MathAbs(g_currentStreak) > g_maxLossStreak) g_maxLossStreak = MathAbs(g_currentStreak); + // * v9.07 FIX#6: Any loss resets the win counter and increases MTF loss streak + g_winsToResetMTF = 0; + g_lossStreakForMTF++; + // * v9.13 FIX#23b: CAP lossStreak at 8 (~=3 full SL entries). + // BUG: Multi-TP entries generate 2-3 loss deals per SL event, inflating streak + // to 18+ which applies -25 score penalty for days. Cap at 8. + if(g_lossStreakForMTF > 8) + g_lossStreakForMTF = 8; + if(g_lossStreakForMTF == 3) + Print("* v9.13 LOSS STREAK=3: Score -15 applied | lossStreak=", g_lossStreakForMTF); + else if(g_lossStreakForMTF == 5) + Print("* v9.13 LOSS STREAK=5: Score -25 applied | lossStreak=", g_lossStreakForMTF); + else if(g_lossStreakForMTF > 5) + Print("* v9.13 LOSS STREAK=", g_lossStreakForMTF, "/8: Score -25 active (capped)"); + } +} +void UpdateHistoricalPerformance(bool isWin, double rMultiple, double peakRR = 0.0) +{ + if(isWin) { ArrayResize(g_historicalWins, ArraySize(g_historicalWins) + 1); g_historicalWins[ArraySize(g_historicalWins) - 1] = rMultiple; } + else { ArrayResize(g_historicalLosses, ArraySize(g_historicalLosses) + 1); g_historicalLosses[ArraySize(g_historicalLosses) - 1] = MathAbs(rMultiple); } + g_totalHistoricalTrades++; + int wins = ArraySize(g_historicalWins), losses = ArraySize(g_historicalLosses); + if(g_totalHistoricalTrades > 0) g_historicalWinRate = (double)wins / g_totalHistoricalTrades; + if(wins > 0) { double sumWins = 0; for(int i = 0; i < wins; i++) sumWins += g_historicalWins[i]; g_historicalAvgWin = sumWins / wins; } + if(losses > 0) { double sumLosses = 0; for(int i = 0; i < losses; i++) sumLosses += g_historicalLosses[i]; g_historicalAvgLoss = sumLosses / losses; } + // * v9.49 FIX#204: Record peak RR alongside outcome. + // peakRR=0 means caller didn't provide it (backward compatibility — treated as unknown). + // When provided, update rolling averages separately for wins and losses. + if(peakRR > 0.01) + { + if(isWin) + { + int sz = ArraySize(g_historicalPeakWins); + ArrayResize(g_historicalPeakWins, sz + 1); + g_historicalPeakWins[sz] = peakRR; + double s = 0; for(int i = 0; i < sz + 1; i++) s += g_historicalPeakWins[i]; + g_historicalAvgPeakWin = s / (sz + 1); + } + else + { + int sz = ArraySize(g_historicalPeakLosses); + ArrayResize(g_historicalPeakLosses, sz + 1); + g_historicalPeakLosses[sz] = peakRR; + double s = 0; for(int i = 0; i < sz + 1; i++) s += g_historicalPeakLosses[i]; + g_historicalAvgPeakLoss = s / (sz + 1); + } + } + UpdateTradingStreak(isWin); +} +void InitializeAllAdvancedSystems() +{ + Print("==============================================================="); + Print("| OPLOINDI ADVANCED MODULES v5.0 - INITIALIZING |"); + Print("==============================================================="); + InitializeCRT(); + InitializeTBS(); + InitializeAMD(); + InitializeJudas(); + InitializeMarketRegime(); + InitializeWinProbability(); + InitializeExpectedValue(); + InitializePositionSize(); + InitializeNewsFilter(); + InitializeCorrelation(); + InitializeTimeAnalysis(); + Print("[OK] All 11 Advanced Systems Initialized"); + Print("==============================================================="); +} +void CleanupAllAdvancedSystems() +{ + CleanupCRT(); + CleanupTBS(); + CleanupAMD(); + CleanupJudas(); + ObjectsDeleteAll(0, "NEWS_"); + ObjectsDeleteAll(0, "CORR_"); + ObjectsDeleteAll(0, "TIME_"); + ObjectsDeleteAll(0, "SMART_"); + Print("All Advanced Systems cleaned up"); +} +void UpdateAllAdvancedModules(const datetime &time[], const double &open[], + const double &high[], const double &low[], const double &close[], + double pdh, double pdl, double asianHigh, double asianLow) +{ + CheckNewsImpact(); + CalculateCorrelations(); + CheckCurrentTimeQuality(); + DetectMarketRegime(time, open, high, low, close); + DetectCRTSetups(time, open, high, low, close); + DetectTBSSetups(time, open, high, low, close); + DetectAMDPhase(time, open, high, low, close); + DetectJudasSwing(time, open, high, low, close, pdh, pdl, asianHigh, asianLow); + g_lastCalculation = TimeCurrent(); +} +//+------------------------------------------------------------------+ +//| 4. HELPER FUNCTIONS - Add these at the end of your file | +//+------------------------------------------------------------------+ +// Get Quality Stars String +string GetQualityStars(double quality) +{ + if(quality >= 85) return "****"; + if(quality >= 75) return "***"; + if(quality >= 65) return "**"; + if(quality >= 50) return "*"; + return "*"; +} +// Get Quality Color +color GetQualityColor(double quality) +{ + if(quality >= 85) return clrGold; + if(quality >= 75) return clrLime; + if(quality >= 65) return clrYellow; + if(quality >= 50) return clrOrange; + return clrGray; +} +// Get FVG Quality Stars +string GetFVGQualityStars(ENUM_FVG_QUALITY quality) +{ + switch(quality) + { + case FVG_QUALITY_PREMIUM: return "***"; + case FVG_QUALITY_HIGH: return "**"; + case FVG_QUALITY_MEDIUM: return "*"; + default: return ""; + } +} +// Get OB Strength Stars +string GetOBStrengthStars(double strength) +{ + if(strength >= 2.5) return "***"; + if(strength >= 1.5) return "**"; + if(strength >= 1.0) return "*"; + return ""; +} +//+------------------------------------------------------------------+ +//| Initialize VSA Module | +//+------------------------------------------------------------------+ +void InitializeVSA() +{ + if(!VSA_Enabled) return; + ArrayResize(g_vsaPatterns, 0, g_maxVSAPatterns); + g_vsaCount = 0; + ZeroMemory(g_currentVSA); + g_currentVSA.type = VSA_NONE; + g_currentVSA.signal = VSA_NO_SIGNAL; + g_vsaInitialized = true; +} +//+------------------------------------------------------------------+ +//| Initialize MTF Module | +//+------------------------------------------------------------------+ +void InitializeMTF() +{ + if(!MTF_Enabled) return; + // Determine primary timeframe + if(MTF_AutoTimeframe) + { + g_primaryTF = Period(); + if(g_primaryTF == PERIOD_CURRENT || g_primaryTF <= 0) + g_primaryTF = PERIOD_H1; + } + else + { + g_primaryTF = MTF_ManualTF; + } + // Build active timeframes array + ArrayResize(g_activeTimeframes, 0); + ArrayResize(g_tfBiases, 0); + ArrayResize(g_mtfMAHandles, 0); + ArrayResize(g_mtfMA20Handles, 0); // * v9.09 FIX#16e + if(MTF_UseM15) AddActiveTimeframe(PERIOD_M15); + if(MTF_UseM30) AddActiveTimeframe(PERIOD_M30); + if(MTF_UseH1) AddActiveTimeframe(PERIOD_H1); + if(MTF_UseH4) AddActiveTimeframe(PERIOD_H4); + if(MTF_UseD1) AddActiveTimeframe(PERIOD_D1); + if(MTF_UseW1) AddActiveTimeframe(PERIOD_W1); + g_activeTFCount = ArraySize(g_activeTimeframes); + ZeroMemory(g_mtfAnalysis); + g_mtfAnalysis.alignment = "INITIALIZING"; + g_mtfInitialized = true; +} +//+------------------------------------------------------------------+ +//| Add Active Timeframe | +//+------------------------------------------------------------------+ +void AddActiveTimeframe(ENUM_TIMEFRAMES tf) +{ + int size = ArraySize(g_activeTimeframes); + ArrayResize(g_activeTimeframes, size + 1); + ArrayResize(g_tfBiases, size + 1); + ArrayResize(g_mtfMAHandles, size + 1); + g_activeTimeframes[size] = tf; + g_mtfMAHandles[size] = iMA(_Symbol, tf, 50, 0, MODE_EMA, PRICE_CLOSE); + // * v9.09 FIX#16e: EMA(20) for slope detection + int ma20Size = ArraySize(g_mtfMA20Handles); + if(ma20Size <= size) ArrayResize(g_mtfMA20Handles, size + 1); + g_mtfMA20Handles[size] = iMA(_Symbol, tf, 20, 0, MODE_EMA, PRICE_CLOSE); + ZeroMemory(g_tfBiases[size]); + g_tfBiases[size].timeframe = tf; + g_tfBiases[size].isValid = false; +} +//+------------------------------------------------------------------+ +//| Analyze VSA Pattern at Bar | +//+------------------------------------------------------------------+ +VSA_Pattern AnalyzeVSAPattern(int barIndex) +{ + VSA_Pattern pattern; + ZeroMemory(pattern); + pattern.type = VSA_NONE; + pattern.signal = VSA_NO_SIGNAL; + if(!VSA_Enabled) return pattern; + double high = iHigh(_Symbol, _Period, barIndex); + double low = iLow(_Symbol, _Period, barIndex); + double close = iClose(_Symbol, _Period, barIndex); + double open = iOpen(_Symbol, _Period, barIndex); + long volume = iVolume(_Symbol, _Period, barIndex); + if(high <= 0 || low <= 0 || volume <= 0) return pattern; + double range = high - low; + if(range == 0) return pattern; + double body = MathAbs(close - open); + double upperWick = high - MathMax(open, close); + double lowerWick = MathMin(open, close) - low; + long avgVolume = CalculateAverageVolumeBars(barIndex, 20); + if(avgVolume == 0) return pattern; + double volumeRatio = (double)volume / (double)avgVolume; + double closePosition = (close - low) / range; + double atr = g_cachedATR > 0 ? g_cachedATR : g_pipValue * 20; + bool isWideSpread = (range > atr * VSA_WideSpreadMultiplier); + bool isNarrowSpread = (range < atr * VSA_NarrowSpreadMult); + bool isHighVolume = (volumeRatio > VSA_HighVolumeThreshold); + bool isLowVolume = (volumeRatio < VSA_LowVolumeThreshold); + bool isUltraHighVolume = (volumeRatio > 2.0); + pattern.volumeRatio = volumeRatio; + pattern.volume = (double)volume; + pattern.price = close; + pattern.time = iTime(_Symbol, _Period, barIndex); + pattern.barIndex = barIndex; + // UPTHRUST (Bearish) + if(isHighVolume && isWideSpread && closePosition < 0.3 && upperWick > body * 1.5) + { + pattern.type = VSA_UPTHRUST; + pattern.signal = VSA_BEARISH; + pattern.isBearish = true; + pattern.strength = 70 + MathMin(20, (volumeRatio - VSA_HighVolumeThreshold) * 10); + pattern.description = "UPTHRUST - Failed Rally (SELL)"; + } + // SPRING (Bullish) + else if(isHighVolume && isWideSpread && closePosition > 0.7 && lowerWick > body * 1.5) + { + pattern.type = VSA_SPRING; + pattern.signal = VSA_BULLISH; + pattern.isBullish = true; + pattern.strength = 70 + MathMin(20, (volumeRatio - VSA_HighVolumeThreshold) * 10); + pattern.description = "SPRING - Shakeout Reversal (BUY)"; + } + // NO DEMAND (Bearish) + else if(isLowVolume && isNarrowSpread && close > open) + { + pattern.type = VSA_NO_DEMAND; + pattern.signal = VSA_BEARISH; + pattern.isBearish = true; + pattern.strength = 60; + pattern.description = "NO DEMAND - Weak Buying (SELL)"; + } + // NO SUPPLY (Bullish) + else if(isLowVolume && isNarrowSpread && close < open) + { + pattern.type = VSA_NO_SUPPLY; + pattern.signal = VSA_BULLISH; + pattern.isBullish = true; + pattern.strength = 60; + pattern.description = "NO SUPPLY - Weak Selling (BUY)"; + } + // STOPPING VOLUME + else if(isUltraHighVolume && isWideSpread) + { + pattern.type = VSA_STOPPING_VOLUME; + pattern.signal = VSA_NEUTRAL; + pattern.strength = 75; + pattern.description = "STOPPING VOLUME - Potential Reversal"; + } + // CLIMAX + else if(volumeRatio > 2.5 && isWideSpread) + { + pattern.type = VSA_CLIMAX; + if(close > open) + { + pattern.signal = VSA_BEARISH; + pattern.isBearish = true; + pattern.description = "BUYING CLIMAX - Exhaustion (SELL)"; + } + else + { + pattern.signal = VSA_BULLISH; + pattern.isBullish = true; + pattern.description = "SELLING CLIMAX - Capitulation (BUY)"; + } + pattern.strength = 80; + } + // TEST + else if(isLowVolume && !isWideSpread && lowerWick > body) + { + pattern.type = VSA_TEST; + pattern.signal = VSA_BULLISH; + pattern.isBullish = true; + pattern.strength = 65; + pattern.description = "TEST - Successful Test of Supply (BUY)"; + } + // EFFORT NO RESULT + else if(isHighVolume && isNarrowSpread) + { + pattern.type = VSA_EFFORT_NO_RESULT; + pattern.signal = VSA_NEUTRAL; + pattern.strength = 55; + pattern.description = "EFFORT NO RESULT - Absorption"; + } + // ABSORPTION + else if(isHighVolume && body < range * 0.3) + { + pattern.type = VSA_ABSORPTION; + pattern.signal = VSA_NEUTRAL; + pattern.strength = 60; + pattern.description = "ABSORPTION - Smart Money Activity"; + } + return pattern; +} +//+------------------------------------------------------------------+ +//| Calculate Average Volume for Bars | +//+------------------------------------------------------------------+ +long CalculateAverageVolumeBars(int startBar, int period) +{ + long totalVolume = 0; + int count = 0; + for(int i = startBar + 1; i <= startBar + period; i++) + { + long vol = iVolume(_Symbol, _Period, i); + if(vol > 0) + { + totalVolume += vol; + count++; + } + } + if(count == 0) return 0; + return totalVolume / count; +} +//+------------------------------------------------------------------+ +//| Detect VSA Patterns | +//+------------------------------------------------------------------+ +void DetectVSAPatterns(int limit) +{ + if(!VSA_Enabled || !g_vsaInitialized) return; + int maxBars = MathMin(limit, 100); + for(int i = 1; i < maxBars; i++) + { + VSA_Pattern pattern = AnalyzeVSAPattern(i); + if(pattern.type != VSA_NONE && pattern.strength >= (g_workingVSA_MinStrength > 0 ? g_workingVSA_MinStrength : VSA_MinStrength)) // * FIX#82 + { + bool exists = false; + for(int j = 0; j < g_vsaCount; j++) + { + if(g_vsaPatterns[j].time == pattern.time) + { + exists = true; + break; + } + } + if(!exists && g_vsaCount < g_maxVSAPatterns) + { + ArrayResize(g_vsaPatterns, g_vsaCount + 1); + pattern.objName = "ICT_VSA_" + IntegerToString(g_vsaCount); + g_vsaPatterns[g_vsaCount] = pattern; + g_vsaCount++; + if(VSA_ShowOnChart) + { + DrawVSAPattern(pattern); + } + } + } + } + g_currentVSA = AnalyzeVSAPattern(0); +} +//+------------------------------------------------------------------+ +//| Draw VSA Pattern on Chart | +//+------------------------------------------------------------------+ +void DrawVSAPattern(VSA_Pattern &pattern) +{ + if(!VSA_ShowOnChart) return; + string objName = pattern.objName; + color patternColor = pattern.isBullish ? VSA_BullishColor : + (pattern.isBearish ? VSA_BearishColor : clrGray); + string arrowName = objName + "_Arrow"; + int arrowCode = pattern.isBullish ? 233 : (pattern.isBearish ? 234 : 251); + double arrowPrice = pattern.isBullish ? + pattern.price - g_cachedATR * 0.5 : + pattern.price + g_cachedATR * 0.5; + if(ObjectFind(0, arrowName) >= 0) ObjectDelete(0, arrowName); + if(ObjectCreate(0, arrowName, OBJ_ARROW, 0, pattern.time, arrowPrice)) + { + ObjectSetInteger(0, arrowName, OBJPROP_ARROWCODE, arrowCode); + ObjectSetInteger(0, arrowName, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, arrowName, OBJPROP_WIDTH, 2); + ObjectSetString(0, arrowName, OBJPROP_TOOLTIP, pattern.description); + } + string labelName = objName + "_Label"; + if(ObjectFind(0, labelName) >= 0) ObjectDelete(0, labelName); + if(ObjectCreate(0, labelName, OBJ_TEXT, 0, pattern.time, arrowPrice)) + { + string vsaText = GetVSAPatternName(pattern.type); + ObjectSetString(0, labelName, OBJPROP_TEXT, vsaText); + ObjectSetInteger(0, labelName, OBJPROP_COLOR, patternColor); + ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 7); + ObjectSetString(0, labelName, OBJPROP_FONT, "Arial"); + ObjectSetInteger(0, labelName, OBJPROP_ANCHOR, pattern.isBullish ? ANCHOR_TOP : ANCHOR_BOTTOM); + } +} +//+------------------------------------------------------------------+ +//| Get VSA Pattern Name | +//+------------------------------------------------------------------+ +string GetVSAPatternName(ENUM_VSA_PATTERN type) +{ + switch(type) + { + case VSA_UPTHRUST: return "UPTHRUST"; + case VSA_NO_DEMAND: return "NO DEMAND"; + case VSA_NO_SUPPLY: return "NO SUPPLY"; + case VSA_STOPPING_VOLUME: return "STOPPING VOL"; + case VSA_CLIMAX: return "CLIMAX"; + case VSA_TEST: return "TEST"; + case VSA_SPRING: return "SPRING"; + case VSA_EFFORT_NO_RESULT:return "EFFORT/NR"; + case VSA_ABSORPTION: return "ABSORPTION"; + default: return ""; + } +} +//+------------------------------------------------------------------+ +//| Analyze Multiple Timeframes | +//+------------------------------------------------------------------+ +MTF_Analysis AnalyzeMultipleTimeframes() +{ + MTF_Analysis mtf; + ZeroMemory(mtf); + mtf.alignment = "NEUTRAL"; + mtf.overallDirection = MTF_NEUTRAL; + mtf.overallConfidence = 50.0; + mtf.lastUpdate = TimeCurrent(); + if(!MTF_Enabled || g_activeTFCount == 0) + { + mtf.alignment = "DISABLED"; + return mtf; + } + // * v9.09 FIX#16e: WEIGHTED MTF ANALYSIS WITH EMA SLOPE + // Root cause: Old system gave EQUAL weight to all TFs -> M15 pullback cancelled D1 trend. + // Evidence: M15=BEAR + H1=BEAR + H4=BULL + D1=BULL = 2v2 = NEUTRAL -> no protection! + // + // New system: + // 1. Higher TFs get more weight (D1=4, H4=3, H1=2, M15=1, M30=1.5) + // 2. EMA slope check: Is the trend ACCELERATING or DECELERATING? + // 3. Both price-position AND slope must agree for full weight + string details = ""; + double weightedBullScore = 0; + double weightedBearScore = 0; + double totalWeight = 0; + for(int i = 0; i < g_activeTFCount; i++) + { + ENUM_TIMEFRAMES tf = g_activeTimeframes[i]; + // -- TF Weight -- + double tfWeight = 1.0; + if(tf >= PERIOD_D1) tfWeight = 4.0; + else if(tf >= PERIOD_H4) tfWeight = 3.0; + else if(tf >= PERIOD_H1) tfWeight = 2.0; + else if(tf >= PERIOD_M30) tfWeight = 1.5; + else tfWeight = 1.0; // M15 and below + // -- Get EMA(50) value -- + double ma50Value = 0; + if(g_mtfMAHandles[i] != INVALID_HANDLE) + { + double maBuffer[]; + ArraySetAsSeries(maBuffer, true); + if(CopyBuffer(g_mtfMAHandles[i], 0, 0, 3, maBuffer) >= 3) + { + ma50Value = maBuffer[0]; + } + } + // -- Get EMA(20) for slope -- + double ma20Now = 0, ma20Prev = 0; + if(i < ArraySize(g_mtfMA20Handles) && g_mtfMA20Handles[i] != INVALID_HANDLE) + { + double ma20Buf[]; + ArraySetAsSeries(ma20Buf, true); + if(CopyBuffer(g_mtfMA20Handles[i], 0, 0, 3, ma20Buf) >= 3) + { + ma20Now = ma20Buf[0]; + ma20Prev = ma20Buf[2]; // 2 bars ago for slope + } + } + double currentPrice = iClose(_Symbol, tf, 0); + g_tfBiases[i].maValue = ma50Value; + g_tfBiases[i].currentPrice = currentPrice; + if(ma50Value > 0 && currentPrice > 0) + { + g_tfBiases[i].isValid = true; + // -- Price Position Score -- + // Price above EMA(50) = bullish bias, below = bearish + double positionScore = 0; + if(currentPrice > ma50Value) + positionScore = 1.0; + else if(currentPrice < ma50Value) + positionScore = -1.0; + // -- EMA Slope Score -- + // EMA(20) rising = momentum bullish, falling = bearish + double slopeScore = 0; + if(ma20Now > 0 && ma20Prev > 0) + { + double slopeChange = (ma20Now - ma20Prev) / ma20Prev * 10000; // in pips-like units + if(slopeChange > 0.5) slopeScore = 1.0; // clearly rising + else if(slopeChange < -0.5) slopeScore = -1.0; // clearly falling + else slopeScore = 0.0; // flat + } + // -- Combined Direction -- + // Both agree = full weight, disagree = half weight + // This catches: price above EMA but EMA falling = weakening trend + double dirScore = 0; + if(positionScore > 0 && slopeScore >= 0) + dirScore = 1.0; // Price above + EMA rising/flat = BULLISH + else if(positionScore < 0 && slopeScore <= 0) + dirScore = -1.0; // Price below + EMA falling/flat = BEARISH + else if(positionScore > 0 && slopeScore < 0) + dirScore = 0.3; // Price above but EMA falling = weak bull + else if(positionScore < 0 && slopeScore > 0) + dirScore = -0.3; // Price below but EMA rising = weak bear + // Apply direction vote with weight + if(dirScore > 0) + { + weightedBullScore += tfWeight * dirScore; + g_tfBiases[i].direction = 1; + mtf.bullishTFs++; + details += EnumToString(tf) + ":[GREEN] "; + } + else if(dirScore < 0) + { + weightedBearScore += tfWeight * MathAbs(dirScore); + g_tfBiases[i].direction = -1; + mtf.bearishTFs++; + details += EnumToString(tf) + ":[RED] "; + } + else + { + g_tfBiases[i].direction = 0; + mtf.neutralTFs++; + details += EnumToString(tf) + ":[O] "; + } + totalWeight += tfWeight; + } + else + { + g_tfBiases[i].isValid = false; + mtf.neutralTFs++; + } + } + mtf.totalTFs = mtf.bullishTFs + mtf.bearishTFs + mtf.neutralTFs; + mtf.details = details; + // -- Determine Overall Direction (weighted) -- + if(totalWeight > 0) + { + double bullPct = weightedBullScore / totalWeight * 100.0; + double bearPct = weightedBearScore / totalWeight * 100.0; + double netScore = weightedBullScore - weightedBearScore; + double netPct = netScore / totalWeight; // -1.0 to +1.0 + // * FIX#16e: Use net weighted score for direction + // netPct > 0.5 = strong (D1+H4 agree), > 0.1 = moderate, < 0.1 = neutral + if(netPct > 0.50) + { + mtf.overallDirection = MTF_STRONG_BULLISH; + mtf.alignment = "STRONG BULLISH ^^"; + mtf.overallConfidence = 50.0 + netPct * 50.0; + } + else if(netPct > 0.10) + { + mtf.overallDirection = MTF_BULLISH; + mtf.alignment = "BULLISH ^"; + mtf.overallConfidence = 50.0 + netPct * 50.0; + } + else if(netPct < -0.50) + { + mtf.overallDirection = MTF_STRONG_BEARISH; + mtf.alignment = "STRONG BEARISH vv"; + mtf.overallConfidence = 50.0 + MathAbs(netPct) * 50.0; + } + else if(netPct < -0.10) + { + mtf.overallDirection = MTF_BEARISH; + mtf.alignment = "BEARISH v"; + mtf.overallConfidence = 50.0 + MathAbs(netPct) * 50.0; + } + else + { + mtf.overallDirection = MTF_NEUTRAL; + mtf.overallConfidence = 50.0; + mtf.alignment = "NEUTRAL <->"; + } + // Diagnostic log + if(g_verboseLog) + { + PrintFormat("* FIX#16e MTF: Bull=%.1f Bear=%.1f Net=%.2f (%.0f%%) -> %s | %s", + weightedBullScore, weightedBearScore, netPct, mtf.overallConfidence, + mtf.alignment, details); + } + } + return mtf; +} +//+------------------------------------------------------------------+ +//| Update MTF Analysis | +//+------------------------------------------------------------------+ +void UpdateMTFAnalysis() +{ + if(!MTF_Enabled || !g_mtfInitialized) return; + if(TimeCurrent() - g_lastMTFUpdate < 60) return; + g_mtfAnalysis = AnalyzeMultipleTimeframes(); + g_lastMTFUpdate = TimeCurrent(); +} +//+------------------------------------------------------------------+ +//| Check MTF Filter | +//+------------------------------------------------------------------+ +bool PassesMTFFilter(bool isBullish) +{ + if(!MTF_Enabled || !MTF_ConfluenceFilter) return true; + if(g_mtfAnalysis.overallConfidence < (g_workingMTF_MinConfidence > 0 ? g_workingMTF_MinConfidence : MTF_MinConfidence)) return false; // * FIX#82 + if(isBullish) + { + return (g_mtfAnalysis.overallDirection == MTF_BULLISH || + g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH); + } + else + { + return (g_mtfAnalysis.overallDirection == MTF_BEARISH || + g_mtfAnalysis.overallDirection == MTF_STRONG_BEARISH); + } +} +//+------------------------------------------------------------------+ +//| Get VSA Confluence Score | +//+------------------------------------------------------------------+ +double GetVSAConfluenceScore(bool isBullish) +{ + if(!VSA_Enabled) return 0; + double score = 0; + if(g_currentVSA.type != VSA_NONE) + { + if(isBullish && g_currentVSA.isBullish) + { + score = g_currentVSA.strength * 0.2; + } + else if(!isBullish && g_currentVSA.isBearish) + { + score = g_currentVSA.strength * 0.2; + } + } + return MathMin(score, 30); +} +//+------------------------------------------------------------------+ +//| Get MTF Confluence Score | +//+------------------------------------------------------------------+ +double GetMTFConfluenceScore(bool isBullish) +{ + if(!MTF_Enabled) return 0; + if(isBullish) + { + if(g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH) + return g_mtfAnalysis.overallConfidence * 0.25; + else if(g_mtfAnalysis.overallDirection == MTF_BULLISH) + return g_mtfAnalysis.overallConfidence * 0.15; + } + else + { + if(g_mtfAnalysis.overallDirection == MTF_STRONG_BEARISH) + return g_mtfAnalysis.overallConfidence * 0.25; + else if(g_mtfAnalysis.overallDirection == MTF_BEARISH) + return g_mtfAnalysis.overallConfidence * 0.15; + } + return 0; +} +//+------------------------------------------------------------------+ +//| Get MTF Direction (Added for EA compatibility) | +//+------------------------------------------------------------------+ +ENUM_MTF_DIRECTION GetMTFDirection() +{ + if(!MTF_Enabled) return MTF_NEUTRAL; + return g_mtfAnalysis.overallDirection; +} +//+------------------------------------------------------------------+ +//| Draw VSA/MTF Dashboard | +//+------------------------------------------------------------------+ +void DrawVSAMTFDashboard(int &yPos, int xOffset) +{ + int lineHeight = 18; + // VSA STATUS + if(VSA_Enabled && VSA_ShowPanel) + { + CreateLabel("ICT_DASH_VSA_TITLE", xOffset, yPos, "--- VSA ---", clrSilver, 8); + yPos += lineHeight; + if(g_currentVSA.type != VSA_NONE) + { + color vsaColor = g_currentVSA.isBullish ? clrLime : + (g_currentVSA.isBearish ? clrRed : clrGray); + string vsaText = GetVSAPatternName(g_currentVSA.type); + vsaText += StringFormat(" (%.0f%%)", g_currentVSA.strength); + CreateLabel("ICT_DASH_VSA_PATTERN", xOffset, yPos, "[CHART] " + vsaText, vsaColor, 8); + } + else + { + CreateLabel("ICT_DASH_VSA_PATTERN", xOffset, yPos, "[CHART] No Pattern", clrGray, 8); + } + yPos += lineHeight; + string volText = StringFormat("Vol: %.2fx", g_currentVSA.volumeRatio); + color volColor = g_currentVSA.volumeRatio > 1.5 ? clrGold : clrWhite; + CreateLabel("ICT_DASH_VSA_VOL", xOffset, yPos, volText, volColor, 8); + yPos += lineHeight; + } + // MTF STATUS + if(MTF_Enabled && MTF_ShowAlignment) + { + yPos += 5; + CreateLabel("ICT_DASH_MTF_TITLE", xOffset, yPos, "--- MTF ---", clrSilver, 8); + yPos += lineHeight; + color mtfColor = clrGray; + if(g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH || + g_mtfAnalysis.overallDirection == MTF_BULLISH) + mtfColor = clrLime; + else if(g_mtfAnalysis.overallDirection == MTF_STRONG_BEARISH || + g_mtfAnalysis.overallDirection == MTF_BEARISH) + mtfColor = clrRed; + string mtfText = StringFormat("[TARGET] %s (%.0f%%)", + g_mtfAnalysis.alignment, + g_mtfAnalysis.overallConfidence); + CreateLabel("ICT_DASH_MTF_ALIGN", xOffset, yPos, mtfText, mtfColor, 8); + yPos += lineHeight; + string tfBreakdown = StringFormat("Bull:%d Bear:%d Neut:%d", + g_mtfAnalysis.bullishTFs, + g_mtfAnalysis.bearishTFs, + g_mtfAnalysis.neutralTFs); + CreateLabel("ICT_DASH_MTF_BREAKDOWN", xOffset, yPos, tfBreakdown, clrWhite, 8); + yPos += lineHeight; + } +} +//+------------------------------------------------------------------+ +//| Cleanup VSA | +//+------------------------------------------------------------------+ +void CleanupVSA() +{ + for(int i = 0; i < g_vsaCount; i++) + { + ObjectDelete(0, g_vsaPatterns[i].objName + "_Arrow"); + ObjectDelete(0, g_vsaPatterns[i].objName + "_Label"); + } + ObjectsDeleteAll(0, "ICT_VSA_"); + ObjectsDeleteAll(0, "ICT_DASH_VSA_"); + ArrayFree(g_vsaPatterns); + g_vsaCount = 0; +} +//+------------------------------------------------------------------+ +//| Cleanup MTF | +//+------------------------------------------------------------------+ +void CleanupMTF() +{ + for(int i = 0; i < ArraySize(g_mtfMAHandles); i++) + { + if(g_mtfMAHandles[i] != INVALID_HANDLE) + { + IndicatorRelease(g_mtfMAHandles[i]); + } + } + // * v9.09 FIX#16e: Clean up EMA(20) handles + for(int i = 0; i < ArraySize(g_mtfMA20Handles); i++) + { + if(g_mtfMA20Handles[i] != INVALID_HANDLE) + IndicatorRelease(g_mtfMA20Handles[i]); + } + ObjectsDeleteAll(0, "ICT_DASH_MTF_"); + ArrayFree(g_mtfMAHandles); + ArrayFree(g_mtfMA20Handles); + ArrayFree(g_activeTimeframes); + ArrayFree(g_tfBiases); +} +//+------------------------------------------------------------------+ +//| ENHANCED FEATURES - DIVERGENCE, TRENDLINE, SILVER BULLET | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| Draw Divergence - ENHANCED | +//+------------------------------------------------------------------+ +void DrawDivergenceEnhanced(DivergenceStruct &div, const datetime &time[], + const double &high[], const double &low[]) +{ + bool isBullish = (div.type == DIV_REGULAR_BULLISH || div.type == DIV_HIDDEN_BULLISH); + bool isHidden = (div.type == DIV_HIDDEN_BULLISH || div.type == DIV_HIDDEN_BEARISH); + // Select color + color lineColor; + if(isHidden) + lineColor = Divergence_HiddenColor; + else if(isBullish) + lineColor = Divergence_BullColor; + else + lineColor = Divergence_BearColor; + ENUM_LINE_STYLE lineStyle = isHidden ? STYLE_DOT : STYLE_SOLID; + int lineWidth = (div.strength == DIV_STRONG) ? 2 : 1; + string prefix = "DIV_" + IntegerToString(div.id) + "_"; + //=== PRICE LINE === + if(Divergence_ShowLines) + { + div.priceLine = prefix + "Price"; + if(ObjectFind(0, div.priceLine) >= 0) ObjectDelete(0, div.priceLine); + ObjectCreate(0, div.priceLine, OBJ_TREND, 0, div.time1, div.price1, div.time2, div.price2); + ObjectSetInteger(0, div.priceLine, OBJPROP_COLOR, lineColor); + ObjectSetInteger(0, div.priceLine, OBJPROP_STYLE, lineStyle); + ObjectSetInteger(0, div.priceLine, OBJPROP_WIDTH, lineWidth); + ObjectSetInteger(0, div.priceLine, OBJPROP_RAY_RIGHT, false); + } + //=== ARROW === + if(Divergence_ShowArrows) + { + div.arrowObj = prefix + "Arrow"; + int arrowCode = isBullish ? 233 : 234; + double arrowPrice = isBullish ? low[div.bar1] - g_cachedATR * 0.3 + : high[div.bar1] + g_cachedATR * 0.3; + if(ObjectFind(0, div.arrowObj) >= 0) ObjectDelete(0, div.arrowObj); + ObjectCreate(0, div.arrowObj, OBJ_ARROW, 0, div.time1, arrowPrice); + ObjectSetInteger(0, div.arrowObj, OBJPROP_ARROWCODE, arrowCode); + ObjectSetInteger(0, div.arrowObj, OBJPROP_COLOR, lineColor); + ObjectSetInteger(0, div.arrowObj, OBJPROP_WIDTH, 2); + // Tooltip + string typeStr = GetDivergenceTypeString(div.type); + string strengthStr = (div.strength == DIV_STRONG) ? "***" : + (div.strength == DIV_MODERATE) ? "**" : "*"; + string tooltip = StringFormat("%s %s\nRSI: %.1f -> %.1f\nScore: %.0f\nEntry: %.5f\nSL: %.5f\nTP: %.5f", + typeStr, strengthStr, div.rsi1, div.rsi2, div.score, + div.entryPrice, div.stopLoss, div.takeProfit); + ObjectSetString(0, div.arrowObj, OBJPROP_TOOLTIP, tooltip); + } + //=== LABEL === + div.labelObj = prefix + "Label"; + string typeStr = GetDivergenceTypeString(div.type); + string strengthStr = (div.strength == DIV_STRONG) ? "***" : + (div.strength == DIV_MODERATE) ? "**" : "*"; + double labelPrice = isBullish ? low[div.bar1] - g_cachedATR * 0.5 + : high[div.bar1] + g_cachedATR * 0.5; + if(ObjectFind(0, div.labelObj) >= 0) ObjectDelete(0, div.labelObj); + ObjectCreate(0, div.labelObj, OBJ_TEXT, 0, div.time1, labelPrice); + ObjectSetString(0, div.labelObj, OBJPROP_TEXT, typeStr + " " + strengthStr); + ObjectSetInteger(0, div.labelObj, OBJPROP_COLOR, lineColor); + ObjectSetInteger(0, div.labelObj, OBJPROP_FONTSIZE, 8); + ObjectSetString(0, div.labelObj, OBJPROP_FONT, "Arial Bold"); + ObjectSetInteger(0, div.labelObj, OBJPROP_ANCHOR, isBullish ? ANCHOR_TOP : ANCHOR_BOTTOM); +} +void DeleteDivergenceObjects(DivergenceStruct &div) +{ + if(div.priceLine != "") ObjectDelete(0, div.priceLine); + if(div.arrowObj != "") ObjectDelete(0, div.arrowObj); + if(div.labelObj != "") ObjectDelete(0, div.labelObj); +} +//+------------------------------------------------------------------+ +//| Delete Divergence Objects | +//+------------------------------------------------------------------+ +void DeleteDivergenceObjects(string objName) +{ + ObjectDelete(0, objName + "_P"); + ObjectDelete(0, objName + "_L"); + ObjectDelete(0, objName + "_A"); +} +//+------------------------------------------------------------------+ +//| Manage Divergences - ENHANCED | +//+------------------------------------------------------------------+ +void ManageDivergences(const double &high[], const double &low[], const double &close[]) +{ + datetime currentTime = TimeCurrent(); + for(int i = 0; i < g_divergenceCount; i++) + { + if(!g_divergences[i].active) continue; + bool isBullish = (g_divergences[i].type == DIV_REGULAR_BULLISH || + g_divergences[i].type == DIV_HIDDEN_BULLISH); + //=== CHECK EXPIRY === + if(currentTime > g_divergences[i].expiryTime) + { + g_divergences[i].active = false; + DeleteDivergenceObjects(g_divergences[i]); + continue; + } + //=== CHECK INVALIDATION === + if(isBullish) + { + if(low[0] < g_divergences[i].stopLoss) + { + g_divergences[i].active = false; + g_divergences[i].broken = true; + ObjectSetInteger(0, g_divergences[i].priceLine, OBJPROP_COLOR, clrGray); + ObjectSetInteger(0, g_divergences[i].arrowObj, OBJPROP_COLOR, clrGray); + ObjectSetInteger(0, g_divergences[i].labelObj, OBJPROP_COLOR, clrGray); + continue; + } + } + else + { + if(high[0] > g_divergences[i].stopLoss) + { + g_divergences[i].active = false; + g_divergences[i].broken = true; + ObjectSetInteger(0, g_divergences[i].priceLine, OBJPROP_COLOR, clrGray); + ObjectSetInteger(0, g_divergences[i].arrowObj, OBJPROP_COLOR, clrGray); + ObjectSetInteger(0, g_divergences[i].labelObj, OBJPROP_COLOR, clrGray); + continue; + } + } + //=== CHECK CONFIRMATION === + if(!g_divergences[i].confirmed) + { + if(isBullish && close[0] > g_divergences[i].price1 + g_cachedATR * 0.3) + { + g_divergences[i].confirmed = true; + ObjectSetInteger(0, g_divergences[i].priceLine, OBJPROP_COLOR, clrGold); + ObjectSetInteger(0, g_divergences[i].arrowObj, OBJPROP_COLOR, clrGold); + ObjectSetInteger(0, g_divergences[i].labelObj, OBJPROP_COLOR, clrGold); + Print("[OK] Bullish Divergence CONFIRMED #", g_divergences[i].id); + } + else if(!isBullish && close[0] < g_divergences[i].price1 - g_cachedATR * 0.3) + { + g_divergences[i].confirmed = true; + ObjectSetInteger(0, g_divergences[i].priceLine, OBJPROP_COLOR, clrGold); + ObjectSetInteger(0, g_divergences[i].arrowObj, OBJPROP_COLOR, clrGold); + ObjectSetInteger(0, g_divergences[i].labelObj, OBJPROP_COLOR, clrGold); + Print("[OK] Bearish Divergence CONFIRMED #", g_divergences[i].id); + } + } + } +} +//+------------------------------------------------------------------+ +//| Draw Trendline Break - ENHANCED | +//+------------------------------------------------------------------+ +void DrawTrendlineBreak(TrendlineStruct &tl, datetime breakTime, double breakPrice) +{ + if(!Trendline_ShowBreakArrow) return; + string breakName = tl.objName + "_BREAK"; + ObjectDelete(0, breakName); + if(ObjectCreate(0, breakName, OBJ_ARROW, 0, breakTime, breakPrice)) + { + int arrowCode = (tl.type == 1) ? 234 : 233; + ObjectSetInteger(0, breakName, OBJPROP_ARROWCODE, arrowCode); + ObjectSetInteger(0, breakName, OBJPROP_COLOR, Trendline_BreakColor); + ObjectSetInteger(0, breakName, OBJPROP_WIDTH, 4); + ObjectSetInteger(0, breakName, OBJPROP_SELECTABLE, false); + } + string labelText = (tl.type == 1) ? "TL BREAK v" : "TL BREAK ^"; + double labelPrice = breakPrice + (tl.type == 1 ? -g_cachedATR * 0.15 : g_cachedATR * 0.15); + string labelName = breakName + "_L"; + ObjectDelete(0, labelName); + if(ObjectCreate(0, labelName, OBJ_TEXT, 0, breakTime, labelPrice)) + { + ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); + ObjectSetInteger(0, labelName, OBJPROP_COLOR, Trendline_BreakColor); + ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 10); + ObjectSetString(0, labelName, OBJPROP_FONT, "Arial Bold"); + } +} +//+------------------------------------------------------------------+ +//| Delete Trendline Objects | +//+------------------------------------------------------------------+ +void DeleteTrendlineObjects(string objName) +{ + ObjectDelete(0, objName); + ObjectDelete(0, objName + "_L"); + ObjectDelete(0, objName + "_BREAK"); + ObjectDelete(0, objName + "_BREAK_L"); +} +//+------------------------------------------------------------------+ +//| Calculate Trendline Price at Given Time | +//+------------------------------------------------------------------+ +double CalculateTrendlinePrice(TrendlineStruct &tl, datetime targetTime) +{ + // Calculate bar shift from start time + int barShift = iBarShift(_Symbol, _Period, targetTime); + int startBarShift = iBarShift(_Symbol, _Period, tl.startTime); + // Calculate price using linear equation: price = startPrice + slope * barDiff + int barDiff = startBarShift - barShift; + double price = tl.startPrice + tl.slope * barDiff; + return price; +} +//+------------------------------------------------------------------+ +//| [OK] IMPROVED: Manage Trendlines with Complete Cleanup | +//+------------------------------------------------------------------+ +void ManageTrendlines(const double &high[], const double &low[], + const double &close[], const datetime &time[]) +{ + if(!Trendline_Enabled) return; + for(int i = g_trendlineCount - 1; i >= 0; i--) + { + if(!g_trendlines[i].active) continue; + // Update current price + g_trendlines[i].currentPrice = CalculateTrendlinePrice(g_trendlines[i], time[0]); + double tlPrice = g_trendlines[i].currentPrice; + bool isSupport = (g_trendlines[i].type == 1); + // [OK] REMOVE OLD BROKEN TRENDLINES + if(g_trendlines[i].broken && g_trendlines[i].breakTime > 0) + { + int barsSinceBreak = iBarShift(_Symbol, _Period, g_trendlines[i].breakTime); + if(barsSinceBreak > Trendline_BreakExpiry) + { + DeleteTrendlineObjectsEnhanced(g_trendlines[i]); + g_trendlines[i].active = false; + // Remove from array + for(int j = i; j < g_trendlineCount - 1; j++) + g_trendlines[j] = g_trendlines[j + 1]; + g_trendlineCount--; + continue; + } + } + // Check for touches (only for non-broken trendlines) + if(!g_trendlines[i].broken) + { + double touchTol = g_cachedATR * Trendline_Tolerance; + if(isSupport && low[0] <= tlPrice + touchTol && low[0] >= tlPrice - touchTol) + { + g_trendlines[i].touches++; + g_trendlines[i].lastTouchTime = time[0]; + g_trendlines[i].status = TL_STATUS_TESTED; + g_trendlines[i].strength = CalculateTrendlineStrength(g_trendlines[i]); + g_trendlines[i].score = CalculateTrendlineScore(g_trendlines[i]); + } + else if(!isSupport && high[0] >= tlPrice - touchTol && high[0] <= tlPrice + touchTol) + { + g_trendlines[i].touches++; + g_trendlines[i].lastTouchTime = time[0]; + g_trendlines[i].status = TL_STATUS_TESTED; + g_trendlines[i].strength = CalculateTrendlineStrength(g_trendlines[i]); + g_trendlines[i].score = CalculateTrendlineScore(g_trendlines[i]); + } + } + } +} +//+------------------------------------------------------------------+ +//| Check Trendline Breaks - ENHANCED | +//+------------------------------------------------------------------+ +void CheckTrendlineBreaksEnhanced(const double &high[], const double &low[], + const double &close[], const datetime &time[]) +{ + if(!Trendline_Enabled) return; + for(int i = 0; i < g_trendlineCount; i++) + { + if(!g_trendlines[i].active || g_trendlines[i].broken) continue; + double tlPrice = g_trendlines[i].currentPrice; + if(g_trendlines[i].type == 1 && close[0] < tlPrice - g_cachedATR * 0.1) + { + g_trendlines[i].broken = true; + g_trendlines[i].breakTime = time[0]; + g_trendlines[i].breakPrice = close[0]; + DrawTrendlineBreak(g_trendlines[i], time[0], close[0]); + ObjectSetInteger(0, g_trendlines[i].objName, OBJPROP_COLOR, Trendline_BreakColor); + ObjectSetInteger(0, g_trendlines[i].objName, OBJPROP_STYLE, STYLE_DOT); + } + else if(g_trendlines[i].type == -1 && close[0] > tlPrice + g_cachedATR * 0.1) + { + g_trendlines[i].broken = true; + g_trendlines[i].breakTime = time[0]; + g_trendlines[i].breakPrice = close[0]; + DrawTrendlineBreak(g_trendlines[i], time[0], close[0]); + ObjectSetInteger(0, g_trendlines[i].objName, OBJPROP_COLOR, Trendline_BreakColor); + ObjectSetInteger(0, g_trendlines[i].objName, OBJPROP_STYLE, STYLE_DOT); + } + } +} +//+------------------------------------------------------------------+ +//| Check Silver Bullet Windows - ENHANCED | +//+------------------------------------------------------------------+ +void CheckSilverBulletWindows(const datetime &time[], const double &high[], + const double &low[], const double &open[], + const double &close[], int limit) +{ + if(!KZ_EnableSilverBullet) return; + MqlDateTime dt; + TimeCurrent(dt); + int nyHour = (dt.hour + SB_NYOffset + 24) % 24; + int nyTotalMins = nyHour * 60 + dt.min; + // London SB: 03:00-04:00 NY + if(SB_EnableLondon && nyTotalMins >= 180 && nyTotalMins < 240) + if(!HasActiveSilverBullet(SB_LONDON)) + CheckSBWindow(SB_LONDON, time, high, low, open, close, limit, "LONDON SB"); + // AM NY SB: 10:00-11:00 NY + if(SB_EnableAMNY && nyTotalMins >= 600 && nyTotalMins < 660) + if(!HasActiveSilverBullet(SB_AM_NY)) + CheckSBWindow(SB_AM_NY, time, high, low, open, close, limit, "AM NY SB"); + // PM NY SB: 14:00-15:00 NY + if(SB_EnablePMNY && nyTotalMins >= 840 && nyTotalMins < 900) + if(!HasActiveSilverBullet(SB_PM_NY)) + CheckSBWindow(SB_PM_NY, time, high, low, open, close, limit, "PM NY SB"); +} +//+------------------------------------------------------------------+ +//| Has Active Silver Bullet | +//+------------------------------------------------------------------+ +bool HasActiveSilverBullet(ENUM_SB_TYPE sbType) +{ + for(int i = 0; i < ArraySize(g_sbSetups); i++) + if(g_sbSetups[i].active && g_sbSetups[i].sbType == (int)sbType) + return true; + return false; +} +//+------------------------------------------------------------------+ +//| Check SB Window for FVG | +//+------------------------------------------------------------------+ +void CheckSBWindow(ENUM_SB_TYPE sbType, const datetime &time[], const double &high[], + const double &low[], const double &open[], const double &close[], + int limit, string label) +{ + for(int i = 2; i < MathMin(limit, 10); i++) + { + // Bullish FVG + if(low[i-2] > high[i]) + { + AddSilverBulletSetup(sbType, time[i], low[i-2], high[i], 1, label); + return; + } + // Bearish FVG + if(high[i-2] < low[i]) + { + AddSilverBulletSetup(sbType, time[i], low[i], high[i-2], -1, label); + return; + } + } +} +//+------------------------------------------------------------------+ +//| Add Silver Bullet Setup | +//+------------------------------------------------------------------+ +void AddSilverBulletSetup(ENUM_SB_TYPE sbType, datetime windowStart, + double fvgTop, double fvgBottom, int direction, string label) +{ + int size = ArraySize(g_sbSetups); + if(size >= 5) + { + for(int i = 0; i < size; i++) + if(!g_sbSetups[i].active) + { + ObjectDelete(0, g_sbSetups[i].objName); + ObjectDelete(0, g_sbSetups[i].objName + "_L"); + for(int j = i; j < size - 1; j++) g_sbSetups[j] = g_sbSetups[j + 1]; + ArrayResize(g_sbSetups, size - 1); + size--; + break; + } + } + ArrayResize(g_sbSetups, size + 1); + g_sbSetups[size].sbType = (int)sbType; + g_sbSetups[size].windowStart = windowStart; + g_sbSetups[size].fvgTop = fvgTop; + g_sbSetups[size].fvgBottom = fvgBottom; + g_sbSetups[size].direction = direction; + g_sbSetups[size].active = true; + g_sbSetups[size].objName = "ICT_SB_" + IntegerToString(sbType) + "_" + IntegerToString(size); + if(KZ_ShowSilverBullet) DrawSBZone(g_sbSetups[size], label); +} +//+------------------------------------------------------------------+ +//| Draw Silver Bullet Zone | +//+------------------------------------------------------------------+ +void DrawSBZone(SilverBulletSetup &sb, string label) +{ + double top = MathMax(sb.fvgTop, sb.fvgBottom); + double bottom = MathMin(sb.fvgTop, sb.fvgBottom); + datetime endTime = sb.windowStart + PeriodSeconds(_Period) * 20; + ObjectDelete(0, sb.objName); + if(ObjectCreate(0, sb.objName, OBJ_RECTANGLE, 0, sb.windowStart, top, endTime, bottom)) + { + ObjectSetInteger(0, sb.objName, OBJPROP_COLOR, SB_ZoneColor); + ObjectSetInteger(0, sb.objName, OBJPROP_FILL, true); + ObjectSetInteger(0, sb.objName, OBJPROP_BACK, true); + ObjectSetInteger(0, sb.objName, OBJPROP_SELECTABLE, false); + } + string labelName = sb.objName + "_L"; + ObjectDelete(0, labelName); + if(ObjectCreate(0, labelName, OBJ_TEXT, 0, sb.windowStart, top + g_cachedATR * 0.1)) + { + ObjectSetString(0, labelName, OBJPROP_TEXT, "* " + label); + ObjectSetInteger(0, labelName, OBJPROP_COLOR, SB_ZoneColor); + ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 9); + ObjectSetString(0, labelName, OBJPROP_FONT, "Arial Bold"); + } +} +//+------------------------------------------------------------------+ +//| [OK] IMPROVED: Manage Silver Bullets - Already Good! | +//+------------------------------------------------------------------+ +void ManageSilverBullets(const double &high[], const double &low[], + const double &close[], const datetime &time[]) +{ + for(int i = ArraySize(g_sbSetups) - 1; i >= 0; i--) + { + if(!g_sbSetups[i].active) continue; + double top = MathMax(g_sbSetups[i].fvgTop, g_sbSetups[i].fvgBottom); + double bottom = MathMin(g_sbSetups[i].fvgTop, g_sbSetups[i].fvgBottom); + bool filled = false; + if(g_sbSetups[i].direction == 1 && low[0] < bottom) filled = true; + if(g_sbSetups[i].direction == -1 && high[0] > top) filled = true; + int barsSince = iBarShift(_Symbol, _Period, g_sbSetups[i].windowStart); + if(barsSince > g_workingSB_MaxAge) filled = true; + if(filled) + { + // [OK] COMPLETE CLEANUP - This is already correct! + ObjectDelete(0, g_sbSetups[i].objName); + ObjectDelete(0, g_sbSetups[i].objName + "_L"); + g_sbSetups[i].active = false; + // Remove from array + for(int j = i; j < ArraySize(g_sbSetups) - 1; j++) + g_sbSetups[j] = g_sbSetups[j + 1]; + ArrayResize(g_sbSetups, ArraySize(g_sbSetups) - 1); + } + else + { + // Extend rectangle to current time + datetime newEnd = TimeCurrent() + PeriodSeconds(_Period) * 20; + ObjectSetInteger(0, g_sbSetups[i].objName, OBJPROP_TIME, 1, newEnd); + } + } +} +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| END OF ALL REMAINING FUNCTIONS | +//+------------------------------------------------------------------+ +//| EA-SPECIFIC FUNCTIONS (Added for Trading) | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| OnTick - Main EA Loop | +//+------------------------------------------------------------------+ +#ifdef COMPILE_AS_EA +//+------------------------------------------------------------------+ +void OnTick() +{ + if(!EA_Enabled) return; + if(!g_initSuccess) return; + // =============================================================== + // NEW BAR DETECTION + // =============================================================== + static datetime last_bar = 0; + datetime current_bar = iTime(_Symbol, _Period, 0); + bool isNewBar = (current_bar != last_bar); + if(isNewBar) + { + last_bar = current_bar; + g_ticksThisBar = 0; + } + g_ticksThisBar++; + // =============================================================== + // NEW BAR PROCESSING - Full analysis cycle + // =============================================================== + if(isNewBar) + { + OnNewBar(); + } + // =============================================================== + // EVERY TICK PROCESSING — correct architectural order: + // 1. MultiTP management first (detects TP/SL hits, applies ladder SL) + // 2. EA_ManagePositions (uses up-to-date peakRR from multiTPEntries) + // 3. DD check (after any closes) + // 4. ProtectTrade + EvaluateExit (our clean exit system) + // 5. DOL runner management + // =============================================================== + // Multi-TP Position Management — first, so TP hits are processed + // before any other management logic reads peakRR or trade state + if(MultiTP_Enabled) + { + double mtp_high[], mtp_low[], mtp_close[]; + datetime mtp_time[]; + ArraySetAsSeries(mtp_high, true); ArraySetAsSeries(mtp_low, true); + ArraySetAsSeries(mtp_close, true); ArraySetAsSeries(mtp_time, true); + if(CopyHigh(_Symbol, _Period, 0, 10, mtp_high) > 0 && + CopyLow (_Symbol, _Period, 0, 10, mtp_low) > 0 && + CopyClose(_Symbol, _Period, 0, 10, mtp_close) > 0 && + CopyTime (_Symbol, _Period, 0, 10, mtp_time) > 0) + { + ManageMultiTPPositions(mtp_high, mtp_low, mtp_close, mtp_time); + CleanupCompletedMultiTP(); + } + } + // EA position management (proximity close, peakRR tracking, hybrid trail) + EA_ManagePositions(); + // * FIX#DD_INTRABAR v2: Check daily DD on EVERY TICK after position management. + // PROBLEM v1: Guard `(g_ea_has_open_buy || g_ea_has_open_sell)` was WRONG. + // Sequence: SL hit → broker closes positions mid-bar → next tick: has_open=false → + // guard prevents DD check → 22.76% DD only detected at next bar open (12 min late). + // FIX v2: Remove the guard entirely. CheckDailyDrawdownLimit() is lightweight + // (reads 2 doubles, 1 comparison). Correct behavior: check AFTER positions close too. + if(EA_EnableDrawdownProtection) + { + if(CheckDailyDrawdownLimit()) return; + } + // * v9.38 FIX#166: INTRA-BAR ZONE DETECTION FOR H4+ + // Problem: EA_CheckSignals() runs only on new bar open (OnNewBar). On H4, price frequently + // enters a FVG/OB zone MID-BAR, shows reaction, then closes outside the zone -- the touch is + // never detected because bar-open price was outside. Result: valid setups silently missed. + // Fix: For H4+ TFs, run a lightweight zone-proximity check on every tick. If price enters + // any active FVG or OB zone, fire a full EA_CheckSignals() once per bar. + // Guards: (1) only H4+, (2) only intra-bar (not when OnNewBar already ran), (3) only once per + // bar (g_intraBarChecked flag), (4) only if no position open yet this bar, (5) all normal + // safety filters inside EA_CheckSignals() still apply (KZ, regime, MTF, score, etc). + // * FIX#452: RefreshRate throttle — intra-bar zone scan runs every g_workingRefreshRate ticks. + // RefreshRate=3 (default) → scan every 3rd tick. H4 has ~10-50 ticks/bar → light enough. + // Lower TFs (M5/M15) skip intra-bar entirely (_Period < PERIOD_H4). + // This makes RefreshRate actually do something useful instead of being a dead input. + if(_Period >= PERIOD_H4 && !isNewBar && !g_intraBarChecked && !g_ea_has_open_buy && !g_ea_has_open_sell + && (g_workingRefreshRate <= 1 || g_ticksThisBar % MathMax(1, g_workingRefreshRate) == 0)) + { + double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + bool inZone = false; + // Quick FVG proximity scan + int fvgTotal = ArraySize(FVG_Array); + for(int _z = 0; _z < fvgTotal && !inZone; _z++) + { + if(!g_fvgs[_z].active) continue; + double fvg_sz = g_fvgs[_z].top - g_fvgs[_z].bottom; + double fvg_tol = fvg_sz * 0.10; + if(bid >= (g_fvgs[_z].bottom - fvg_tol) && bid <= (g_fvgs[_z].top + fvg_tol)) + inZone = true; + } + // Quick OB proximity scan + if(!inZone) + { + int obTotal = ArraySize(OB_Array); + for(int _z = 0; _z < obTotal && !inZone; _z++) + { + if(!g_obs[_z].active || g_obs[_z].mitigated) continue; + double ob_sz = g_obs[_z].top - g_obs[_z].bottom; + double ob_tol = ob_sz * 0.10; + if(bid >= (g_obs[_z].bottom - ob_tol) && bid <= (g_obs[_z].top + ob_tol)) + inZone = true; + } + } + if(inZone) + { + g_intraBarChecked = true; // Set BEFORE call to prevent re-entry on this bar + if(g_verboseLog) + PrintFormat("[FIX#166] H4 intra-bar zone touch detected (bid=%.5f) -- running CheckSignals", bid); + EA_CheckSignals(); + if(g_ea_signal.isValid) + { + // Run same post-signal gates as OnNewBar + if(EA_HedgeMode == HEDGE_NO_HEDGE) + { + bool wouldHedge = (g_ea_signal.isBullish && g_ea_has_open_sell) || + (!g_ea_signal.isBullish && g_ea_has_open_buy); + if(wouldHedge) g_ea_signal.isValid = false; + } + if(g_ea_signal.isValid) + EA_ExecuteTrade(); + } + } + } + // * v6.33 FIX: Track trade results from deal history + // UpdateHistoricalPerformance() existed but was NEVER called! + // g_historicalWinRate stayed at 0.5 forever -> WinProbability always used static 50% base + // Now we scan recent deals and update win/loss tracking + EA_UpdateTradeResults(); + // Real-time Killzone update + if(EnableKillzones) + { + UpdateKillzones(TimeCurrent()); + if(ShowDashboard) + { + color kzColor = g_isInKillzone ? clrGold : clrGray; + string kzText = "Killzone: " + (g_isInKillzone ? g_currentKillzoneName : "NONE"); + CreateLabel("ICT_Dash_KZ", 10, 66, kzText, kzColor, 10); + } + } + // Multi-TP tick-level updates (needs precision) + if(MultiTP_Enabled) + { + double tick_high[], tick_low[], tick_close[]; + datetime tick_time[]; + ArraySetAsSeries(tick_high, true); + ArraySetAsSeries(tick_low, true); + ArraySetAsSeries(tick_close, true); + ArraySetAsSeries(tick_time, true); + if(CopyHigh(_Symbol, _Period, 0, 10, tick_high) > 0 && + CopyLow(_Symbol, _Period, 0, 10, tick_low) > 0 && + CopyClose(_Symbol, _Period, 0, 10, tick_close) > 0 && + CopyTime(_Symbol, _Period, 0, 10, tick_time) > 0) + { + UpdateMultiTPLevels(tick_high, tick_low, tick_close, tick_time); + } + } + // Dashboard updates (throttled - every 3 seconds) + static datetime lastDashUpdate = 0; + if(ShowDashboard && g_workingShowDashboard) + { + if(isNewBar || (TimeCurrent() - lastDashUpdate) >= 3) + { + UpdateDashboard(); + lastDashUpdate = TimeCurrent(); + } + } + // Professional Dashboard — throttled to every 2s (no flicker, still live) + { + static datetime _lastProfDash = 0; + if(Dash_Enabled && (isNewBar || TimeCurrent() - _lastProfDash >= 2)) + { + _lastProfDash = TimeCurrent(); + UpdateProfessionalDashboard(); + } + } + // TBS Real-Time Management + if(TBS_Enabled) + { + double tbs_high[], tbs_low[], tbs_close[]; + datetime tbs_time[]; + ArraySetAsSeries(tbs_high, true); + ArraySetAsSeries(tbs_low, true); + ArraySetAsSeries(tbs_close, true); + ArraySetAsSeries(tbs_time, true); + if(CopyHigh(_Symbol, _Period, 0, 30, tbs_high) > 0 && + CopyLow(_Symbol, _Period, 0, 30, tbs_low) > 0 && + CopyClose(_Symbol, _Period, 0, 30, tbs_close) > 0 && + CopyTime(_Symbol, _Period, 0, 30, tbs_time) > 0) + { + UpdateTBSSetups(tbs_time, tbs_high, tbs_low, tbs_close); + } + } + // Divergence management (every tick for precision) + if(Divergence_Enabled) + { + double div_high[], div_low[], div_close[]; + ArraySetAsSeries(div_high, true); + ArraySetAsSeries(div_low, true); + ArraySetAsSeries(div_close, true); + if(CopyHigh(_Symbol, _Period, 0, 30, div_high) > 0 && + CopyLow(_Symbol, _Period, 0, 30, div_low) > 0 && + CopyClose(_Symbol, _Period, 0, 30, div_close) > 0) + { + ManageDivergences(div_high, div_low, div_close); + } + } + // Trendline management (every tick) + if(Trendline_Enabled) + { + double tl_high[], tl_low[], tl_close[]; + datetime tl_time[]; + ArraySetAsSeries(tl_high, true); + ArraySetAsSeries(tl_low, true); + ArraySetAsSeries(tl_close, true); + ArraySetAsSeries(tl_time, true); + if(CopyHigh(_Symbol, _Period, 0, 50, tl_high) > 0 && + CopyLow(_Symbol, _Period, 0, 50, tl_low) > 0 && + CopyClose(_Symbol, _Period, 0, 50, tl_close) > 0 && + CopyTime(_Symbol, _Period, 0, 50, tl_time) > 0) + { + ManageTrendlines(tl_high, tl_low, tl_close, tl_time); + CheckTrendlineBreaksEnhanced(tl_high, tl_low, tl_close, tl_time); + } + } + // Silver Bullet management (every tick) + if(KZ_EnableSilverBullet) + { + double sb_high[], sb_low[], sb_close[]; + datetime sb_time[]; + ArraySetAsSeries(sb_high, true); + ArraySetAsSeries(sb_low, true); + ArraySetAsSeries(sb_close, true); + ArraySetAsSeries(sb_time, true); + if(CopyHigh(_Symbol, _Period, 0, 30, sb_high) > 0 && + CopyLow(_Symbol, _Period, 0, 30, sb_low) > 0 && + CopyClose(_Symbol, _Period, 0, 30, sb_close) > 0 && + CopyTime(_Symbol, _Period, 0, 30, sb_time) > 0) + { + ManageSilverBullets(sb_high, sb_low, sb_close, sb_time); + } + } + // Periodic maintenance + PerformMaintenanceTasks(TimeCurrent()); + // --- Trade protection and exit — every 5 seconds --- + // Unified exit system: + // 1. ProtectTrade: moves ALL legs to BE simultaneously (no close, structural) + // 2. SmartExit_Check (unified): 9 categories, 3-tier response: + // -1 (2 cats, below floor) → trail tightens in ProfitGuard_Trail + // >0 (3+ cats OR 2+override, above floor) → close position directly here + // Applies to ALL tranches (TP1, TP2, TP3) + // EvaluateExit removed — logic merged into SmartExit_Check categories + if(EA_SmartExit_Enable && TimeCurrent() - g_lastSmartExitCheck >= 5) + { + g_lastSmartExitCheck = TimeCurrent(); + for(int _ex = PositionsTotal()-1; _ex >= 0; _ex--) + { + ulong _exTkt = PositionGetTicket(_ex); + if(_exTkt == 0) continue; + if(!PositionSelectByTicket(_exTkt)) continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; + if((long)PositionGetInteger(POSITION_MAGIC) != EA_MagicNumber) continue; + if(CheckZoneInvalidation(_exTkt)) continue; + // Step 1: BE protection for all legs + ProtectTrade(_exTkt); + if(!PositionSelectByTicket(_exTkt)) continue; // may have been closed by ProtectTrade + // Step 2: Unified SmartExit — check for active close signal + { + ENUM_POSITION_TYPE _pt = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); + bool _isBuy = (_pt == POSITION_TYPE_BUY); + double _ep = PositionGetDouble(POSITION_PRICE_OPEN); + double _cp = _isBuy ? SymbolInfoDouble(_Symbol, SYMBOL_BID) + : SymbolInfoDouble(_Symbol, SYMBOL_ASK); + double _origSL = PositionGetDouble(POSITION_SL); + for(int _mi = 0; _mi < ArraySize(g_multiTPEntries); _mi++) + { + if(!g_multiTPEntries[_mi].active) continue; + if(MathAbs(g_multiTPEntries[_mi].entryPrice - _ep) > g_pipValue * 5) continue; + if(g_multiTPEntries[_mi].stopLoss > 0) _origSL = g_multiTPEntries[_mi].stopLoss; + break; + } + double _slDist = MathAbs(_ep - _origSL); + double _seRR = (_slDist > 0) ? (_isBuy ? (_cp-_ep)/_slDist : (_ep-_cp)/_slDist) : 0; + if(_seRR > 0) + { + int _seResult = SmartExit_Check(_isBuy, _seRR, _ep, _cp, g_cachedATR); + if(_seResult > 0) + { + CTrade _seCtr; + _seCtr.SetExpertMagicNumber(EA_MagicNumber); + _seCtr.SetDeviationInPoints(50); // 5 pip slippage allowance + if(_seCtr.PositionClose(_exTkt)) + { + PrintFormat("[SmartExit] Closed @ %.2fR | cats=%d | ticket=%llu", + _seRR, _seResult, _exTkt); + continue; // position closed — skip to next + } + else + { + PrintFormat("[SmartExit] CLOSE FAILED @ %.2fR | cats=%d | ticket=%llu | err=%d", + _seRR, _seResult, _exTkt, _seCtr.ResultRetcode()); + } + } + } + } + // TP1 only check removed — SmartExit now handles ALL tranches + } + } + + // Runner management: every 5 seconds, update TP2/TP3 to live DOL target. + // DOL = nearest unswept BSL/SSL = where price is being drawn by market makers. + // If DOL flips against position while in profit → close immediately. + // If DOL target moved → update TP via PositionModify. + if(EA_SmartExit_Enable && EA_UseMultipleTP && TimeCurrent() - g_lastDOLCheck >= 5) + { + g_lastDOLCheck = TimeCurrent(); + ComputeDrawOnLiquidity(); // refresh DOL with latest tick data + for(int _fd = PositionsTotal()-1; _fd >= 0; _fd--) + { + ulong _fdT = PositionGetTicket(_fd); + if(_fdT == 0) continue; + if(!PositionSelectByTicket(_fdT)) continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; + if((long)PositionGetInteger(POSITION_MAGIC) != EA_MagicNumber) continue; + string _fdCmt = PositionGetString(POSITION_COMMENT); + bool _fdIsTP2 = (StringFind(_fdCmt, "_TP2") >= 0); + bool _fdIsTP3 = (StringFind(_fdCmt, "_TP3") >= 0); + if(!_fdIsTP2 && !_fdIsTP3) continue; + bool _fdBuy = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY); + double _fdEntry = PositionGetDouble(POSITION_PRICE_OPEN); + double _fdCurTP = PositionGetDouble(POSITION_TP); + double _fdCurSL = PositionGetDouble(POSITION_SL); + double _fdPrice = _fdBuy ? SymbolInfoDouble(_Symbol, SYMBOL_BID) + : SymbolInfoDouble(_Symbol, SYMBOL_ASK); + // Use ORIGINAL SL for RR — live SL may be at BE (entry+0.5pip) after TP1, + // which would collapse slDist to near-zero and inflate RR massively. + double _fdOrigSL = _fdCurSL; + for(int _dm = 0; _dm < ArraySize(g_multiTPEntries); _dm++) + { + if(!g_multiTPEntries[_dm].active) continue; + bool _dSameDir = (g_multiTPEntries[_dm].direction == 1 && _fdBuy) || + (g_multiTPEntries[_dm].direction == -1 && !_fdBuy); + if(!_dSameDir) continue; + if(MathAbs(g_multiTPEntries[_dm].entryPrice - _fdEntry) > g_pipValue * 5) continue; + if(g_multiTPEntries[_dm].stopLoss > 0) _fdOrigSL = g_multiTPEntries[_dm].stopLoss; + break; + } + double _fdSlDist = MathAbs(_fdEntry - _fdOrigSL); + double _fdCurrRR = (_fdSlDist > 0) + ? (_fdBuy ? (_fdPrice - _fdEntry) : (_fdEntry - _fdPrice)) / _fdSlDist + : 0.0; + // --- CHECK 1: DOL flipped against position while in profit → close now --- + // "Price was being drawn our way, now it's being drawn the other way" + // TF-aware profit floor: higher TF runners need more room before DOL close + // M5=0.10R, M15=0.15R, H1=0.20R, H4=0.30R, D1=0.40R + double _dolRunnerFloor; + switch(_Period) + { + case PERIOD_M1: + case PERIOD_M5: _dolRunnerFloor = 0.10; break; + case PERIOD_M15: + case PERIOD_M30: _dolRunnerFloor = 0.15; break; + case PERIOD_H1: _dolRunnerFloor = 0.20; break; + case PERIOD_H4: _dolRunnerFloor = 0.30; break; + default: _dolRunnerFloor = 0.40; break; // D1+ + } + if(g_dolValid && g_dolDirection != 0 && _fdCurrRR >= _dolRunnerFloor) + { + bool _dolNowAgainst = (_fdBuy && g_dolDirection < 0) || + (!_fdBuy && g_dolDirection > 0); + if(_dolNowAgainst) + { + PrintFormat("[DOL Runner] Direction flipped — closing %s at %.2fR | DOL now %s", + _fdIsTP2 ? "_TP2" : "_TP3", _fdCurrRR, + g_dolDirection > 0 ? "UP" : "DOWN"); + if(g_ea_trade.PositionClose(_fdT)) continue; + } + } + // --- CHECK 2: Update TP to new DOL target if changed --- + if(!g_dolValid || g_dolTargetPrice <= 0) continue; + bool _dolAligned = (_fdBuy && g_dolDirection > 0) || (!_fdBuy && g_dolDirection < 0); + if(!_dolAligned) continue; + bool _dolBeyond = _fdBuy ? (g_dolTargetPrice > _fdPrice) + : (g_dolTargetPrice < _fdPrice); + if(!_dolBeyond) continue; + // For TP3: look for next liquidity level beyond DOL + double _newTP = g_dolTargetPrice; + if(_fdIsTP3) + { + double _nextLiq = 0, _nextDist = DBL_MAX; + for(int _lx = 0; _lx < ArraySize(LIQ_Array); _lx++) + { + if(!LIQ_Array[_lx].isValid || LIQ_Array[_lx].swept) continue; + double _lxP = LIQ_Array[_lx].price; + bool _lxDir = _fdBuy ? (_lxP > g_dolTargetPrice && LIQ_Array[_lx].isBSL) + : (_lxP < g_dolTargetPrice && !LIQ_Array[_lx].isBSL); + if(!_lxDir) continue; + double _lxD = MathAbs(_lxP - g_dolTargetPrice); + if(_lxD < _nextDist) { _nextDist = _lxD; _nextLiq = _lxP; } + } + _newTP = (_nextLiq > 0) ? _nextLiq + : g_dolTargetPrice + (_fdBuy ? 1 : -1) * g_cachedATR; + } + // Sanity: new TP must be meaningful distance from entry + if(_fdSlDist > 0 && MathAbs(_newTP - _fdEntry) < _fdSlDist * 0.5) continue; + // Only modify if meaningfully different (>0.5 pips) and not worsening + if(MathAbs(_newTP - _fdCurTP) < g_pipValue * 0.5) continue; + bool _worsening = _fdBuy ? (_newTP < _fdPrice) : (_newTP > _fdPrice); + if(_worsening) continue; + if(g_ea_trade.PositionModify(_fdT, _fdCurSL, _newTP)) + PrintFormat("[DOL Runner] TP updated %s | %s | %.5f → %.5f (DOL=%.5f)", + _fdIsTP2 ? "_TP2" : "_TP3", _fdBuy ? "BUY" : "SELL", + _fdCurTP, _newTP, g_dolTargetPrice); + } + } +} +//+------------------------------------------------------------------+ +//| On New Bar - COMPREHENSIVE EA ANALYSIS ENGINE v6.41 | +//| Contains ALL analysis logic + Diamond Pattern Detection | +//+------------------------------------------------------------------+ +#endif // COMPILE_AS_EA - OnTick + OnNewBar are EA-only +//+------------------------------------------------------------------+ +//| RunSharedAnalysis - SHARED between EA (OnNewBar) and Indicator | +//| Called from OnNewBar() for EA mode and OnCalculate() for Indicator | +//| Contains ALL ICT concept detection, drawing, and ML processing | +// (shared code below) +//+------------------------------------------------------------------+ +void RunSharedAnalysis(const datetime &time[], const double &open[], const double &high[], + const double &low[], const double &close[], const long &tick_volume[], + int rates_total, int detectionLimit) +{ + // =============================================================== + // UPDATE CACHED INDICATORS (MA, ATR, RSI) + // =============================================================== + UpdateCachedIndicators(); + // [OK] FIX v6.1: Update MTF Analysis BEFORE signal generation + if(MTF_Enabled) + UpdateMTFAnalysis(); + if(g_debugMode) + { + Print("[UP] Cached Indicators Updated:"); + Print(" MA: ", DoubleToString(g_cachedMA, _Digits)); + Print(" ATR: ", DoubleToString(g_cachedATR / g_pipValue, 2), " pips"); + Print(" RSI: ", DoubleToString(g_cachedRSI, 1)); + } + // =============================================================== + // UPDATE KILLZONES + // =============================================================== + if(EnableKillzones) + { + UpdateKillzones(TimeCurrent()); + DrawKillzones(time, high, low); + } + // =============================================================== + // PROCESS BACKTESTING + // =============================================================== + if(g_isBacktesting && BacktestMode != BACKTEST_DISABLED) + { + ProcessBacktestBar(0, time, open, high, low, close); + } + // =============================================================== + // DETECT MARKET STRUCTURE + // =============================================================== + if(EnableStructure) + { + DetectMarketStructure(time, high, low, close, detectionLimit); + UpdatePremiumDiscountZones(high, low, close); + AnalyzeStructureBreaks(time, high, low, close); + UpdateD1CHoChBias(); // update D1 CHoCH bias on each new bar + } + // =============================================================== + // DETECT ICT CONCEPTS + // =============================================================== + // FVG Detection & Management + if(EnableFVG) + { + DetectFVG(time, open, high, low, close, detectionLimit); + UpdateFVGStatus(time, high, low, close); + // * FIX#FVGCOUNT: g_fvgCount was only set in AddFVG() = ArraySize (includes EXPIRED/FILLED). + // CountActiveFVGs() counts only FVG_STATUS_ACTIVE entries. + // After UpdateFVGStatus() expires/fills FVGs, g_fvgCount must be resynced. + // Without this, g_fvgCount can be stale-high → EA thinks FVGs exist when none are active. + g_fvgCount = CountActiveFVGs(); + if(ShowFVG) DrawFVGs(time); + } + // Order Blocks Detection & Management + if(EnableOB) + { + DetectOrderBlocks(time, open, high, low, close, tick_volume, detectionLimit); + UpdateOrderBlockStatus(time, high, low, close); + ManageOrderBlocks(time, high, low, close); + if(ShowOB) DrawOrderBlocks(time); + } + // Liquidity Levels + if(EnableLiquidity) + { + DetectLiquidity(time, high, low, close, detectionLimit); + CheckLiquiditySweeps(time, high, low, close); + if(ShowLiquidity) DrawLiquidity(time); + } + // Draw On Liquidity direction — computed here so both LIQ_Array (above) and + // STRUCT_Array (above) are fresh. Used by SmartEntry gate, EvaluateExit signal, + // and DOL runner management (which refreshes it again every 5 sec). + ComputeDrawOnLiquidity(); + // Breaker Blocks + if(EnableBreakerBlocks) + { + DetectBreakerBlocks(time, open, high, low, close, detectionLimit); + if(ShowBreakerBlocks) DrawBreakerBlocks(time); + } + // Mitigation Blocks + if(EnableMitigationBlocks) + { + DetectMitigationBlocks(time, open, high, low, close, detectionLimit); + if(ShowMitigationBlocks) DrawMitigationBlocks(time); + } + // OTE Zones + if(EnableOTE) + { + DetectOTEZones(time, high, low, close, detectionLimit); + if(ShowOTE) DrawOTEZones(time); + } + // Order Flow Imbalance + if(EnableOFI) + { + DetectOrderFlowImbalance(time, open, high, low, close, tick_volume, detectionLimit); + // * v9.16 FIX#48: ShowOrderFlowImb draws OFI arrows on chart (was dead input) + if(ShowOrderFlowImb) + { + int ofiSize = ArraySize(OFI_Array); + for(int ofi = MathMax(0, ofiSize - 20); ofi < ofiSize; ofi++) + { + if(!OFI_Array[ofi].active) continue; + string ofiName = "ICT_OFI_" + IntegerToString(ofi); + int arrowCode = OFI_Array[ofi].isBullish ? 233 : 234; // up/down arrow + color ofiColor = OFI_Array[ofi].isBullish ? clrLime : clrRed; + double ofiPrice = OFI_Array[ofi].isBullish ? low[(int)MathMax(0, iBarShift(_Symbol, _Period, OFI_Array[ofi].time))] - g_cachedATR * 0.2 + : high[(int)MathMax(0, iBarShift(_Symbol, _Period, OFI_Array[ofi].time))] + g_cachedATR * 0.2; + ObjectCreate(0, ofiName, OBJ_ARROW, 0, OFI_Array[ofi].time, ofiPrice); + ObjectSetInteger(0, ofiName, OBJPROP_ARROWCODE, arrowCode); + ObjectSetInteger(0, ofiName, OBJPROP_COLOR, ofiColor); + ObjectSetInteger(0, ofiName, OBJPROP_WIDTH, 2); + ObjectSetInteger(0, ofiName, OBJPROP_BACK, true); + } + } + } + // Volume Profile + if(EnableVolumeProfile) + { + CalculateVolumeProfile(time, high, low, close, tick_volume, detectionLimit); + if(ShowVolumeProfile) DrawVolumeProfile(time); + } + // Market Maker Model + if(EnableMarketMaker) + { + DetectMarketMakerPhase(time, open, high, low, close, tick_volume, detectionLimit); + if(ShowMMPhases) DrawMarketMakerPhases(time); + // * v9.16 FIX#48: ShowMMModels gates MM model label display on chart (was dead input) + // When false, MM detection still runs for signals but no OBJ_TEXT labels drawn + if(!ShowMMModels) + { + for(int mm = 0; mm < ArraySize(MM_Phases); mm++) + { + string mmObj = "ICT_MM_Model_" + IntegerToString(mm); + ObjectDelete(0, mmObj); + } + } + } + // =============================================================== + // ML PREDICTIONS + // =============================================================== + if(EnableML) + { + ProcessMLPredictions(time, open, high, low, close, tick_volume); + } + // =============================================================== + // EXTENDED ANALYSIS (CRT, TBS, AMD, Judas, Patterns, etc.) + // =============================================================== + if(MultiTP_Enabled && MultiTP_ShowLevels) + { + DrawMultiTPLevels(time); + } + // =============================================================== + // NEW FEATURE DETECTIONS + // =============================================================== + // Market Regime Detection -- * v6.38: Always run + // * v7.5b FIX: CRITICAL! The 5-param DetectMarketRegime was supposed to be called + // from UpdateAllAdvancedModules(), but that function is NEVER CALLED from RunSharedAnalysis! + // v7.4 removed the 4-param call (thinking the 5-param version ran), leaving + // g_regimeData.regime = REGIME_UNKNOWN FOREVER -> no regime-adaptive SL/TP/trailing/BE. + // FIX: Call the 5-param version directly here. + if(Regime_Enabled) + DetectMarketRegime(time, open, high, low, close); + // * v7.0: Refresh pair thresholds (adapts to current volatility) + // * v6.31: MUST run BEFORE AutoOpt so pair profile provides BASE values + // and AutoOpt can then refine/override with dynamic optimization. + // Previous order: AutoOpt -> PairProfile = AutoOpt was overwritten! + if(g_gates.computed && PAIR_UseOptimalSettings && g_cachedATR > 0) + { + ApplyPairOptimalSettings(); + } + // * v9.16 FIX: CRITICAL RISK PIPELINE BUG + // ApplyPairOptimalSettings() resets ALL g_pairThresholds to static pair profile values + // on EVERY bar (SL=1.30, TP=2.50, Risk=EA_RiskPercent=1.00%, etc.). + // But AutoOpt only recalculates every 60min (M15 = max(15,15*4)=60min). + // Between recalcs, UpdateAutoOptimization() returns immediately -> pair profile values stick. + // Result: 59 of 60 bars use Risk=1.0% instead of AutoOpt's 0.50%. + // Fix: always re-sync pairThresholds from CACHED g_autoOptParams (persists between recalcs). + if(AutoOpt_Enabled && g_autoOptInitialized && g_autoOptParams.risk_pct > 0) + { + // * v9.16 FIX#46a: TF+pair-aware TP2/TP3 spacing (was hardcoded 1.35/1.75) + { + double _tp2R = 1.35, _tp3R = 1.75; + GetTP2TP3Ratios(g_autoOptParams.tf_category, g_autoOptParams.pair_category, _tp2R, _tp3R); + } + g_workingAccountRiskPercent = g_autoOptParams.risk_pct; + // * v9.24 FIX#81d: Re-sync position management working vars from cached AutoOpt. + // These are set in ApplyAutoOptToWorkingVars() (every 60min), but between recalcs + // we need them current. Adding here ensures no stale OnInit values persist. + // All use MathMax to enforce user input as floor (same logic as FIX#80). + g_workingBE_RR = g_autoOptParams.breakeven_rr; + g_workingBE_Ranging_RR = MathMin(g_autoOptParams.breakeven_rr, EA_BE_Ranging_RR); + g_workingBE_Volatile_RR = MathMax(g_autoOptParams.breakeven_rr, EA_BE_Volatile_RR); + // * FIX#278: keep TP2 BE in sync with session adjustments + { + double tp2BEFloor = (g_autoOptParams.tp1_rr > 0) ? g_autoOptParams.tp2_rr * 0.85 : EA_TP2_BE_Threshold; + g_workingBE_TP2_RR = MathMax(EA_TP2_BE_Threshold, tp2BEFloor); + } + g_workingSmartExit_MinRR = g_autoOptParams.smart_exit_min_rr; + g_workingSmartExit_Signals = g_autoOptParams.smart_exit_signals; + // Re-sync entry quality: pair table min_conf[tf] flows via min_conf_override. + // EA_MinEntryScore is the user floor (default=44, calibrated for H1 EURUSD). + { + double _pairTableConf = (g_autoOptParams.min_conf_override > 0) + ? g_autoOptParams.min_conf_override + : g_gates.minScore; + } + // Re-sync FVG quality from cached confluence + // [retired] minFVGQuality block — FVG quality handled by score threshold in g_gates + } + // * v9.16 FIX#44: smart_min_confidence = EA_MinEntryScore (single source, no AutoOpt override) + // Both Gate 1 (SelectBestCandidate) and Gate 2 (SmartEntryDecision) use the same threshold. + // =============================================================== + // AUTO-OPTIMIZATION -- * v6.31: Moved AFTER ApplyPairOptimalSettings + // so AutoOpt dynamic values OVERRIDE static pair profile values. + // Previously AutoOpt ran first -> pair profile overwrote everything. + // =============================================================== + if(AutoOpt_Enabled) + { + UpdateAutoOptimization(); + if(AutoOpt_ShowPanel) + DrawAutoOptPanel(); + } + // CRT (Candle Range Theory) + if(CRT_Enabled) + { + DetectCRT(time, open, high, low, close, detectionLimit); + } + // TBS (Turtle Soup) + if(TBS_Enabled) + { + DetectTBS(time, high, low, close, detectionLimit); + } + // * v7.8 Trend Continuation + if(g_tcEnabled) + { + UpdateTrendContAge(); + DetectTrendCont(time, high, low, close, open, detectionLimit); + } + // AMD Phase + if(AMD_Enabled) + { + DetectAMDPhase(time, open, high, low, close); + } + // Judas Swing + if(Judas_Enabled) + { + // Calculate previous day high/low and Asian session levels + double pdh = 0, pdl = 0, asianHigh = 0, asianLow = 0; + // Simple calculation: use last 24-48 bars for PDH/PDL + int pdBars = MathMin(detectionLimit, 48); + if(pdBars > 0) + { + pdh = high[ArrayMaximum(high, 0, pdBars)]; + pdl = low[ArrayMinimum(low, 0, pdBars)]; + } + // Asian session: typically last 8-12 bars (depending on timeframe) + int asianBars = MathMin(detectionLimit, 12); + if(asianBars > 0) + { + asianHigh = high[ArrayMaximum(high, 0, asianBars)]; + asianLow = low[ArrayMinimum(low, 0, asianBars)]; + } + DetectJudasSwing(time, open, high, low, close, pdh, pdl, asianHigh, asianLow); + } + // Divergences + if(Divergence_Enabled) + { + DetectDivergences(time, high, low, close, detectionLimit); + } + // Trendlines + if(Trendline_Enabled) + { + DetectTrendlines(time, high, low, detectionLimit); + CheckTrendlineBreaks(high, low); + } + // News Filter + if(News_FilterEnabled) + { + UpdateNewsFilter(); + } + // VSA Detection + if(VSA_Enabled) + { + DetectVSAPatterns(detectionLimit); + } + // =============================================================== + // [NEW] CHART PATTERNS DETECTION (v6.41 - WITH DIAMONDS!) + // =============================================================== + if(ChartPatterns_Enabled) + { + // * v9.16 FIX#35: Only run pattern detection ONCE per new bar + // Problem: DetectHeadAndShoulders() ran every tick, re-detecting same stale patterns + // 30-40 times per bar -> 17,208 SKIPPED logs, massive CPU waste. + // Fix: Guard with bar timestamp. Patterns only change on new bars, not ticks. + bool newBarForPatterns = (time[0] != g_lastPatternDetectionBar); + if(newBarForPatterns) + { + g_lastPatternDetectionBar = time[0]; + // Find swing points for pattern detection (used by all patterns) + FindSwingPointsForPatterns(time, high, low, detectionLimit, 5); + // Head & Shoulders + if(HS_Enabled) + DetectHeadAndShoulders(time, high, low, close, detectionLimit); + // Double/Triple Top/Bottom + if(DTB_Enabled) + DetectDoubleTripleTopBottom(time, high, low, close, detectionLimit); + // Triangles (Ascending, Descending, Symmetrical) + if(Triangle_Enabled) + DetectTriangles(time, high, low, close, detectionLimit); + } + // Flags & Pennants + if(FlagPennant_Enabled) + DetectFlagsAndPennants(time, high, low, close, detectionLimit); + // Wedges (Rising, Falling) + if(Wedge_Enabled) + DetectWedges(time, high, low, close, detectionLimit); + // Diamonds (Top, Bottom) - NEW v6.41! + if(Diamond_Enabled) + DetectDiamonds(time, high, low, close, detectionLimit); + // V-Patterns (V-Top, V-Bottom) + if(VPattern_Enabled) + DetectVPatterns(time, high, low, close, detectionLimit); + // Update master pattern container + UpdateMasterPatternContainer(); + } + // =============================================================== + // [NEW] CANDLESTICK PATTERNS DETECTION + // =============================================================== + if(CandlePatterns_Enabled) + { + CandlePatternStructExtended candlePattern = DetectAllCandlePatterns(0); + if(candlePattern.patternStrength >= CandlePatterns_MinStrength) + { + DrawCandlePatternArrow(candlePattern); + // Store for reference + if(g_extendedCandleCount < ArraySize(g_extendedCandlePatterns)) + { + g_extendedCandlePatterns[g_extendedCandleCount] = candlePattern; + g_extendedCandleCount++; + } + // Limit array size + if(g_extendedCandleCount > 100) + { + for(int i = 0; i < 50; i++) + g_extendedCandlePatterns[i] = g_extendedCandlePatterns[i + 50]; + g_extendedCandleCount = 50; + } + } + } + // Silver Bullet Detection + if(KZ_EnableSilverBullet) + { + CheckSilverBulletWindows(time, high, low, open, close, detectionLimit); + } + // =============================================================== + // NEW FEATURES DASHBOARD + // =============================================================== + { + int newFeatureY = 300; + int newFeatureX = 10; + DrawNewFeaturesDashboard(newFeatureY, newFeatureX); + } + // =============================================================== + // PERIODIC DEEP CLEANUP (Every 100 bars) + // =============================================================== + static int cleanupCounter = 0; + cleanupCounter++; + if(cleanupCounter >= 100) + { + LimitObjectsByCount(50, 30, 20); + cleanupCounter = 0; + if(g_debugMode) + Print("[CLEAN] Deep cleanup performed. Memory optimized."); + } + // =============================================================== + // DEBUG OUTPUT + // =============================================================== + static int last_print_time = 0; + if(last_print_time != (int)time[0]) + { + last_print_time = (int)time[0]; + string debug_msg = StringFormat( + "[CHART] Indicators Updated: FVGs=%d (Active=%d), OBs=%d (Active=%d)", + ArraySize(FVG_Array), + g_fvgCount, + ArraySize(OB_Array), + g_obCount + ); + Print(debug_msg); + if(g_fvgCount > 0) + Print(" [OK] ", g_fvgCount, " active FVGs available for trading"); + if(g_obCount > 0) + Print(" [OK] ", g_obCount, " active Order Blocks available for trading"); + if(g_fvgCount == 0 && g_obCount == 0) + Print(" [WARN] No active FVGs or Order Blocks found"); + // Pattern detection summary + if(ChartPatterns_Enabled && g_allPatterns.hasActivePattern) + { + Print(" [CHART] Active Pattern: ", g_allPatterns.strongestPattern, + " [Score: ", g_allPatterns.strongestScore, "]"); + } + if(CandlePatterns_Enabled && g_lastExtendedCandlePattern.patternStrength >= CandlePatterns_MinStrength) + { + Print(" [CANDLE] Candle Pattern: ", g_lastExtendedCandlePattern.patternName, + " [Strength: ", g_lastExtendedCandlePattern.patternStrength, "]"); + } + } + + // ── FIX#505: Ensure D1 CHoCH bias is always fresh ─────────────────────────── + // ROOT: UpdateD1CHoChBias() was only called when g_workingD1CHoCHGate=1 (H1: OFF). + // FIX#505 reads g_d1CHoCH_Bull/Bear/Valid in ComputeMarketContext — needs fresh data. + // This call is cheap (only runs on new D1 bar), safe to always run. + UpdateD1CHoChBias(); + + // ── FIX#506: Stale D1 CHoCH Expiry ───────────────────────────────────── + // ROOT: g_d1CHoCH_Bull from Jan rally persisted while MTF=STRONG_BEAR + // for weeks (Apr 2024) → every system reading D1 saw "BULL" while + // market was in -300p Markdown. + // + // FIX: count consecutive H1 bars where raw MTF contradicts D1 CHoCH. + // After 12 bars (12 hours): CHoCH is stale → invalidate it. + // Result: D1=neutral → no D1 gate fires → MTF leads direction. + // + // Reset: counter resets to 0 when D1 and MTF agree OR when a new + // D1 CHoCH forms (UpdateD1CHoChBias sets Valid=true). + if(g_d1CHoCH_Valid) + { + ENUM_MTF_DIRECTION _rawMTF = g_mtfAnalysis.overallDirection; + bool _mtfBull = (_rawMTF == MTF_BULLISH || _rawMTF == MTF_STRONG_BULLISH); + bool _mtfBear = (_rawMTF == MTF_BEARISH || _rawMTF == MTF_STRONG_BEARISH); + bool _conflict = (g_d1CHoCH_Bull && _mtfBear) || (g_d1CHoCH_Bear && _mtfBull); + if(_conflict) + { + g_d1CHoCH_MtfConflictBars++; + // TF-aware expiry: 12 hours in bars. + // A D1 CHoCH is structural — it should persist 12h before giving up. + // M5: 12h = 144 bars, M15: 48 bars, H1: 12 bars (target TF). + int _bph = (int)MathRound(3600.0 / MathMax(1, PeriodSeconds())); + int _expiryBars = MathMax(12, _bph * 12); // 12h in bars, min 12 + if(g_d1CHoCH_MtfConflictBars >= _expiryBars) + { + if(g_verboseLog) + PrintFormat("[FIX#506] D1 CHoCH STALE: %d bars (%dh) | CHoCH=%s MTF=%s → invalidated", + g_d1CHoCH_MtfConflictBars, _expiryBars / MathMax(1,_bph), + g_d1CHoCH_Bull ? "BULL" : "BEAR", + EnumToString(_rawMTF)); + g_d1CHoCH_Valid = false; + g_d1CHoCH_MtfConflictBars = 0; + } + } + else + { + g_d1CHoCH_MtfConflictBars = 0; // D1 and MTF agree — reset counter + } + } + + // ── FIX#503: Trend Exhaustion Score — computed BEFORE ComputeMarketContext ── + // g_exhaustionScore is read by DeriveScenarioProfile (inside ComputeMarketContext + // call chain). Must be fresh before g_mktCtx is computed. + // UpdateRegimeAnalysis() already ran above and populated g_regimeData, + // g_cachedADX, g_cachedATR — so all inputs are ready. + // g_exhaustionScore is set inside UpdateRegimeAnalysis() at the end of the + // FIX#501 block (same function, same call). No extra call needed here — + // just verify it is non-stale (UpdateRegimeAnalysis runs every bar). + + // ── FIX#502: Section 10+17 merge ───────────────────────────────── + // ComputeMarketContext + DeriveScenarioProfile run HERE, at the end + // of RunSharedAnalysis, AFTER all detectors have populated their globals. + // This ensures g_mktCtx and g_scenarioProfile are always fresh when + // EA_CheckSignals reads them — no lazy cache needed. + // Owner: RunSharedAnalysis (single write point per bar). + g_mktCtx = ComputeMarketContext(); + g_scenarioProfile = DeriveScenarioProfile(g_mktCtx); + g_mktCtxLastBar = TimeCurrent(); // Keep for backward compatibility +} +#ifdef COMPILE_AS_EA // OnNewBar + EA functions are EA-only +void OnNewBar() +{ + g_tickStart = GetTickCount(); // [v6.42] Processing time measurement + g_intraBarChecked = false; // * v9.38 FIX#166: Reset intra-bar zone detection flag each bar + // =============================================================== + // DAILY LIMITS RESET + // =============================================================== + // * v8.0 FIX TIMEZONE: Use TimeGMT() so daily reset is always at 00:00 GMT, not broker local time + MqlDateTime dt; + datetime gmtNow2 = TimeGMT(); + TimeToStruct(gmtNow2, dt); + datetime today = StringToTime(StringFormat("%04d.%02d.%02d", dt.year, dt.mon, dt.day)); + if(g_ea_stats.date != today) + { + g_ea_stats.date = today; + g_ea_stats.trades = 0; + g_ea_stats.risk = 0; + // * v7.9 FIX BUG#3: Sync g_tradesThisDay (indicator counter) with g_ea_stats.trades (EA counter) + // Previously two independent counters fell out of sync -- daily limit could be bypassed or double-counted + g_tradesThisDay = 0; + // * FIX#424: Clear re-entry slot on daily reset + g_seReEntry.active = false; + g_seReEntryReady = false; + // * v9.34 FIX#132: Also reset per-regime counter on new day + g_tradesThisRegimePeriod = 0; + g_lastRegimeForThrottle = -1; + // * FIX#405: Reset consecutive loss streak HERE in OnNewBar daily block. + // BUG in FIX#404: streak reset was in PerformMaintenanceTasks() which runs in OnTick() + // AFTER OnNewBar() returns. Execution order every new bar: + // OnTick → OnNewBar → EA_CheckSignals → FIX#398 sees old streak → BLOCKS → return + // → back in OnTick → PerformMaintenanceTasks → FIX#404 resets streak (too late) + // Result: FIRST BAR of every new day still blocked by FIX#398 even after fixing #404. + // Fix: reset g_currentStreak and g_lossStreakForMTF here, before EA_CheckSignals runs. + // Also fixes BUG#2: FIX#404 used TimeCurrent() (broker time) but this block uses + // TimeGMT() — two different day boundaries. Now both use the same GMT reference. + if(EA_MaxConsecLossHalt > 0 && g_currentStreak < 0) + { + PrintFormat("[FIX#405] OnNewBar streak reset: g_currentStreak %d → 0 | g_lossStreakForMTF %d → 0 | FIX#398 HALT cleared", + g_currentStreak, g_lossStreakForMTF); + g_currentStreak = 0; + g_lossStreakForMTF = 0; + } + } + // =============================================================== + // COPY BAR DATA (replaces OnCalculate's passed-in arrays) + // =============================================================== + datetime time[]; + double open[], high[], low[], close[]; + long tick_volume[], volume[]; + int spread[]; + ArraySetAsSeries(time, true); + ArraySetAsSeries(open, true); + ArraySetAsSeries(high, true); + ArraySetAsSeries(low, true); + ArraySetAsSeries(close, true); + ArraySetAsSeries(tick_volume, true); + ArraySetAsSeries(volume, true); + ArraySetAsSeries(spread, true); + // Copy enough bars for all analysis modules + int bars_available = iBars(_Symbol, _Period); + int bars_to_copy = MathMin(bars_available, MathMax(MaxBarsToCalculate, 500)); + bool data_ready = true; + data_ready = data_ready && (CopyTime(_Symbol, _Period, 0, bars_to_copy, time) > 0); + data_ready = data_ready && (CopyOpen(_Symbol, _Period, 0, bars_to_copy, open) > 0); + data_ready = data_ready && (CopyHigh(_Symbol, _Period, 0, bars_to_copy, high) > 0); + data_ready = data_ready && (CopyLow(_Symbol, _Period, 0, bars_to_copy, low) > 0); + data_ready = data_ready && (CopyClose(_Symbol, _Period, 0, bars_to_copy, close) > 0); + data_ready = data_ready && (CopyTickVolume(_Symbol, _Period, 0, bars_to_copy, tick_volume) > 0); + // Volume and spread are optional + CopyRealVolume(_Symbol, _Period, 0, bars_to_copy, volume); + CopySpread(_Symbol, _Period, 0, bars_to_copy, spread); + if(!data_ready) + { + Print("[WARN] Failed to copy bar data for analysis"); + return; + } + int rates_total = ArraySize(time); + g_totalRates = rates_total; + // Detection limit for analysis functions + // * FIX#DETECT-LIMIT: was hardcoded MathMin(MaxBarsToCalculate,200) -- ignored g_workingFVG_MaxAge. + // On H4 AutoOpt sets g_workingFVG_MaxAge=720. Capping at 200 means bars 201-720 never scanned + // → new FVGs on those bars never detected → FVG=0 on almost every bar despite valid price action. + // Fix: use MathMax(g_workingFVG_MaxAge, 200) so the scanner always looks back far enough. + int detectionLimit = MathMin(rates_total - 1, MathMin(MaxBarsToCalculate, MathMax(g_workingFVG_MaxAge, 200))); + // [UNIFIED v6.42] Shared analysis - used by both EA (OnNewBar) and Indicator (OnCalculate) + RunSharedAnalysis(time, open, high, low, close, tick_volume, rates_total, detectionLimit); + // =============================================================== + // SIGNAL GENERATION (Indicator Signals) + // =============================================================== + if(EnableSignals) + { + UpdateActiveSignals(time, high, low, close); + GenerateSignals(time, open, high, low, close, tick_volume); + } + // [UNIFIED v6.42] All analysis features now in RunSharedAnalysis() + // =============================================================== + // EA TRADING LOGIC - * v7.4: GLOBAL daily limit (no pair override) + // =============================================================== + if(g_ea_stats.trades >= EA_MaxDailyTrades) return; + // * v6.33 FIX: Only check for NEW signals on NEW bars + // EA was calling EA_CheckSignals on EVERY TICK -> could open trades + // on tick noise within same candle + immediate re-entry after stops + static datetime g_ea_lastSignalBar = 0; + datetime currentBarTime = iTime(_Symbol, _Period, 0); + if(currentBarTime == g_ea_lastSignalBar) + { + // Already checked this bar - only manage existing positions + return; + } + // * v7.8 FIX: Count open positions AND track directions + int open_count = 0; + int open_buy_count = 0; + int open_sell_count = 0; + double total_open_risk = 0; // * v9.03: Track total exposed risk + g_ea_has_open_buy = false; + g_ea_has_open_sell = false; + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + if(g_ea_position.SelectByIndex(i) && + g_ea_position.Symbol() == _Symbol && + g_ea_position.Magic() == EA_MagicNumber) + { + open_count++; + if(g_ea_position.PositionType() == POSITION_TYPE_BUY) { g_ea_has_open_buy = true; open_buy_count++; } + if(g_ea_position.PositionType() == POSITION_TYPE_SELL) { g_ea_has_open_sell = true; open_sell_count++; } + // * v9.03: Calculate risk exposure of this position (distance to SL as % of balance) + double posSL = g_ea_position.StopLoss(); + double posOpen = g_ea_position.PriceOpen(); + if(posSL > 0 && posOpen > 0) + { + double posLoss = 0; + ENUM_ORDER_TYPE posOrdType = (g_ea_position.PositionType() == POSITION_TYPE_BUY) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL; + if(OrderCalcProfit(posOrdType, _Symbol, g_ea_position.Volume(), posOpen, posSL, posLoss)) + total_open_risk += MathAbs(posLoss); + } + } + } + // * v9.03 FIX: Smart Position Limit (replaces v7.8 hardcoded lock at 1) + // Rules: + // 1. Max open trades = EA_MaxOpenTrades (user input, respected) + // 2. HEDGE MODE: NO_HEDGE=block opposite, CLOSE_REVERSE=flip, ALLOW=both open + // 3. RISK CAP: Total open risk must not exceed daily DD limit + // 4. Same-direction stacking allowed within limits + // * v9.03: MaxOpenTrades check depends on hedge mode + // ALLOW_HEDGE: count per-direction (can have MaxOpen BUY + MaxOpen SELL) + // Others: count total + int effectiveMaxOpen = EA_MaxOpenTrades; + bool maxOpenReached = false; + if(EA_HedgeMode == HEDGE_ALLOW) + { + // In hedge mode, limit is per-direction -- checked later when direction is known + // But still enforce absolute cap (2x MaxOpen = max total positions) + if(open_count >= EA_MaxOpenTrades * 2) + { + if(g_verboseLog) + Print("* v9.03 MAX OPEN [HEDGE]: ", open_count, "/", EA_MaxOpenTrades * 2, " total positions -> no new entries"); + return; + } + } + else if(EA_HedgeMode == HEDGE_CLOSE_AND_REVERSE) + { + // Close&Reverse: max check on same-direction only (opposite will be closed) + // Don't block here -- let it through, closing happens in EA_CheckSignals + // But block if SAME direction is already at max + int sameMax = MathMax(open_buy_count, open_sell_count); + if(sameMax >= EA_MaxOpenTrades && !g_ea_has_open_buy && !g_ea_has_open_sell) + { + // Flat and at max? Shouldn't happen, but safety + maxOpenReached = true; + } + } + else // NO_HEDGE (default) + { + // * v9.12 FIX: Use GetActiveMultiTPCount() (logical trades) instead of open_count (broker legs). + // BUG: open_count = PositionsTotal() counts 3 legs per trade. After TP1 partial close + // of trade N, only 2 positions remain -> open_count drops to 2 (or 0 after SL hit). + // On Jan 20 this caused 6 simultaneous LONGs despite EA_MaxOpenTrades=1. + // Fix: count active MultiTPEntry records -- each = 1 logical trade. + int logicalOpenCount = GetActiveMultiTPCount(); + if(logicalOpenCount >= EA_MaxOpenTrades) + { + if(g_verboseLog) + Print("* v9.12 MAX OPEN: ", logicalOpenCount, "/", EA_MaxOpenTrades, + " logical trades active (", open_count, " broker positions) -> no new entries"); + return; + } + } + // * v9.03: Set anti-hedge flags based on mode + // These are checked in EA_CheckSignals AFTER signal direction is known + switch(EA_HedgeMode) + { + case HEDGE_NO_HEDGE: + g_antiHedge_blockBuy = g_ea_has_open_sell; // SELL open -> block BUY + g_antiHedge_blockSell = g_ea_has_open_buy; // BUY open -> block SELL + break; + case HEDGE_CLOSE_AND_REVERSE: + g_antiHedge_blockBuy = false; // Don't block -- will close SELL first + g_antiHedge_blockSell = false; // Don't block -- will close BUY first + break; + case HEDGE_ALLOW: + g_antiHedge_blockBuy = false; // Allow both directions + g_antiHedge_blockSell = false; + break; + } + // Risk cap: if total open risk already near daily DD limit, block new entries + double balance = AccountInfoDouble(ACCOUNT_BALANCE); + double maxDDAmount = balance * EA_MaxDailyDrawdownPercent / 100.0; + double riskHeadroom = maxDDAmount - total_open_risk; + double minNewTradeRisk = balance * EA_RiskPercent / 100.0 * 0.5; // At least half a normal trade must fit + if(riskHeadroom < minNewTradeRisk && open_count > 0) + { + if(g_verboseLog) + Print("* v9.03 RISK CAP: OpenRisk=$", DoubleToString(total_open_risk, 2), + " | DDLimit=$", DoubleToString(maxDDAmount, 2), + " | Headroom=$", DoubleToString(riskHeadroom, 2), + " -> not enough room for new trade"); + return; + } + // * v7.5c: Streak pause REMOVED -- Daily DD limit (2%) handles protection + // The streak pause used g_currentStreak which caused permanent lockout bug + // * v7.5c FIX #4: Regime-Aware Trade Throttle + // * v7.6b: Raised caps -- quality filter (score>=70) handles frequency naturally + // No longer need to hard-limit to 2-3. If the signal passes score>=70 in a CHOPPY regime, + // it's a rare high-quality setup and should be taken. + { + int maxTradesForRegime = g_autoOptParams.max_daily_trades; + if(g_regimeData.regime == REGIME_CHOPPY) + maxTradesForRegime = MathMin(3, maxTradesForRegime); // * v9.34 FIX#132: 2->3 (per-regime counter means this is now 3 CHOPPY trades, not 3 total) + else if(g_regimeData.regime == REGIME_RANGING || g_regimeData.regime == REGIME_RANGING_TIGHT || + g_regimeData.regime == REGIME_RANGING_WIDE) + maxTradesForRegime = MathMin(10, maxTradesForRegime); // * v7.6b: 3->10 + else if(g_regimeData.regime == REGIME_VOLATILE) + maxTradesForRegime = MathMin(8, maxTradesForRegime); // * v7.6b: 3->8 + // * v9.34 FIX#132: Use per-regime counter instead of daily total. + // Old bug: 2 trades in TREND regime → regime shifts to CHOPPY → g_ea_stats.trades=2 ≥ maxChoppy=2 + // → ZERO CHOPPY trades allowed even though none were taken in CHOPPY. + // Fix: reset g_tradesThisRegimePeriod whenever regime changes; compare throttle against it. + int currentRegimeInt = (int)g_regimeData.regime; + if(currentRegimeInt != g_lastRegimeForThrottle) + { + g_tradesThisRegimePeriod = 0; + g_lastRegimeForThrottle = currentRegimeInt; + } + if(g_tradesThisRegimePeriod >= maxTradesForRegime) + { + static datetime lastRegimeLog = 0; + if(time[0] != lastRegimeLog) + { + lastRegimeLog = time[0]; + Print("* v7.5c REGIME THROTTLE: ", EnumToString(g_regimeData.regime), + " -> max ", maxTradesForRegime, " trades/this-regime (used: ", g_tradesThisRegimePeriod, ")"); + } + return; + } + } + // * v7.8 UNIVERSAL SL COOLDOWN (all regimes, direction-aware) + // Scans history for recent SL hit and updates g_lastSLTime/Direction + { + // * v9.49 FIX#202: TF-aware cooldown (was flat 6 bars = 24h on H4, 6h on H1). + // ICT principle: after a SL hit, the next strong OB/FVG is often a high-quality reversal. + // H4: 2 bars = 8h cooldown (enough to see if price recovers a new swing). + // H1: 3 bars = 3h cooldown (enough for a session to establish new direction). + // M15/M5: 6 bars (unchanged — short TF needs more protection vs noise). + int cooldownBars; + if(_Period >= PERIOD_H4) cooldownBars = 2; // H4/D1: 8h cooldown + else if(_Period >= PERIOD_H1) cooldownBars = 3; // H1: 3h cooldown + else cooldownBars = 6; // M5/M15/M30: unchanged + datetime cooldownStart = currentBarTime - (datetime)(cooldownBars * PeriodSeconds(_Period)); + if(HistorySelect(cooldownStart, TimeCurrent())) + { + int hTotal = HistoryDealsTotal(); + for(int hd = hTotal - 1; hd >= 0; hd--) + { + ulong hdTicket = HistoryDealGetTicket(hd); + if(HistoryDealGetInteger(hdTicket, DEAL_MAGIC) != EA_MagicNumber) continue; + if(HistoryDealGetString(hdTicket, DEAL_SYMBOL) != _Symbol) continue; + if(HistoryDealGetInteger(hdTicket, DEAL_ENTRY) != DEAL_ENTRY_OUT) continue; + if(HistoryDealGetInteger(hdTicket, DEAL_REASON) != DEAL_REASON_SL) continue; + datetime slTime = (datetime)HistoryDealGetInteger(hdTicket, DEAL_TIME); + long dealType = HistoryDealGetInteger(hdTicket, DEAL_TYPE); + int closedDir = (dealType == DEAL_TYPE_BUY) ? 1 : -1; + if(slTime > g_lastSLTime) + { + g_lastSLTime = slTime; + g_lastSLDirection = closedDir; + if(g_verboseLog) + Print("* v7.8 COOLDOWN: SL hit detected, dir=", closedDir == 1 ? "SELL" : "BUY", + " at ", TimeToString(slTime)); + } + break; + } + } + if(g_lastSLTime > 0 && currentBarTime <= g_lastSLTime + (datetime)(cooldownBars * PeriodSeconds(_Period))) + { + g_slCooldownActive = true; + g_slCooldownDirection = g_lastSLDirection; + } + else + { + g_slCooldownActive = false; + } + } + // * vFIX: Choppy/Volatile cooldown + if(g_regimeData.regime == REGIME_CHOPPY || g_regimeData.regime == REGIME_VOLATILE) + { + if(g_slCooldownActive) + { + if(g_verboseLog) + Print("* vFIX COOLDOWN: SL in last 6 bars + ", + EnumToString(g_regimeData.regime), " -- skipping entry this bar"); + g_ea_lastSignalBar = currentBarTime; + return; + } + } + // Check for trading signals + // * FIX#445c: D1 entry delay — skip first 60 min of D1 bar. + // D1 bar opens at 00:00-00:05 UTC with spread 30-52p (midnight rollover). + // Spread normalises by 01:00-01:30 UTC. Opening at 00:05 means paying 30-50p spread + // on a 100-200p SL trade — that's 15-25% of SL wasted at entry. + // Fix: for D1, skip EA_CheckSignals if less than 60 min have elapsed since bar open. + // After 01:05 UTC spread is normal (3-5p) and the structural D1 signal is still valid. + // Does NOT apply when bar already has a strong sweep confirmation (g_dolDirection valid). + if(_Period >= PERIOD_D1) + { + datetime _d1BarOpen = iTime(_Symbol, PERIOD_D1, 0); + int _elapsedMin = (int)(TimeCurrent() - _d1BarOpen) / 60; + if(_elapsedMin < 60) + { + if(g_verboseLog) + PrintFormat("[FIX#445c] D1 entry delay: %d min < 60 min since bar open (spread normalization)", + _elapsedMin); + g_ea_lastSignalBar = currentBarTime; + return; + } + } + // * FIX#446: Set lastSignalBar BEFORE EA_CheckSignals — not after. + // BUG (bt11 Feb 6): EA_CheckSignals runs, SmartEntry rejects candidate A (divergence veto), + // returns without opening. g_ea_lastSignalBar NOT yet set → next tick (3ms later) re-runs + // EA_CheckSignals with candidate B (slightly different price/candidates) → passes SmartEntry + // → trade opens. The divergence veto fired correctly but was bypassed by the next tick. + // FIX: mark bar as checked before running → same-bar re-entry impossible. + // If signal is found AND valid → trade opens this tick. If rejected → bar is done. + g_ea_lastSignalBar = currentBarTime; + EA_CheckSignals(); + if(g_ea_signal.isValid) + { + // * v9.03: Safety net -- only block hedging in NO_HEDGE mode + if(EA_HedgeMode == HEDGE_NO_HEDGE) + { + bool wouldHedge = (g_ea_signal.isBullish && g_ea_has_open_sell) || + (!g_ea_signal.isBullish && g_ea_has_open_buy); + if(wouldHedge) + { + if(g_verboseLog) + Print("* v9.03 SAFETY NET: Anti-hedge block (NO_HEDGE mode) -- ", + g_ea_signal.isBullish ? "BUY" : "SELL", " blocked, opposite position open"); + g_ea_signal.isValid = false; + } + } + // ── FIX#502: Adjust SL/TP based on scenario geometry ───────── + // Only runs when scenario is valid and confidence is sufficient. + // Fallback: if scenario is UNKNOWN or adjustment fails, existing + // technique-derived SL/TP are preserved unchanged. + if(g_ea_signal.isValid && g_scenarioProfile.isValid) + AdjustSLTPForScenario(); + } + if(g_ea_signal.isValid) + { + // * v9.36 FIX#141: SL MAX WIDTH CHECK FOR SCALP/SHORT TIMEFRAMES + // Problem: FVG/structure-based SL can be 2-3x the ATR on M5 (e.g. 15.2p when ATR=4.9p). + // This makes ALL downstream thresholds (trail, BE, SmartExit) unreachable. + // Fix: On M5/M15/M30 (scalp/swing TF), if SL > max_sl_atr_mult × ATR → REJECT. + // H1+ is more lenient because structure-based SLs are naturally wider there. + // The ATR clamp (FIX#15h) handles TP side; this handles SL side. + // * FIX#SL141_INDEX: Index pairs (US500/US100) place SL at structural swing lows/highs + // which are naturally 5-9×ATR away. The 2.8×ATR M15 limit rejects ALL TC/FVG trades. + // Index uses a much wider limit: 10×ATR on M15 (observed SL/ATR ratios: 6-8×). + if(g_cachedATR > 0) + { + double _sl_width = MathAbs(g_ea_signal.entryPrice - g_ea_signal.stopLoss); + double _sl_pips = _sl_width / g_pipValue; + double _max_sl_mult = 0; // 0 = no check for this TF + // * FIX#465: Metal pairs (XAUUSD/XAGUSD) use same wide limit as Index. + // ROOT CAUSE: FIX#141's 2.5×ATR M5 limit was calibrated for EURUSD (ATR≈$4, + // structural SL≈$8 → ratio=2.0 → fine). For XAUUSD, ATR(14) M5 ≈ $1-3 (price + // action units), but structural SL from FVG/OB/BREAKER zones = $5-15 → ratio=4-9× + // → ALL XAUUSD M5 trades silently rejected by FIX#141. + // ICT: Gold structural SL MUST be placed beyond the zone — $5-15 IS valid for M5. + // Fix: treat Metal the same as Index (genuine wide-SL instruments). + bool _isWiderSL = (g_autoOptParams.pair_category == "Index" || + g_autoOptParams.pair_category == "Metal"); + if(_Period <= PERIOD_M5) _max_sl_mult = _isWiderSL ? 12.0 : 2.5; + else if(_Period <= PERIOD_M15) _max_sl_mult = _isWiderSL ? 10.0 : 3.5; // FIX#302: 2.8→3.5 + else if(_Period <= PERIOD_M30) _max_sl_mult = _isWiderSL ? 10.0 : 3.0; + else if(_Period <= PERIOD_H1) _max_sl_mult = _isWiderSL ? 12.0 : 3.5; + if(_max_sl_mult > 0 && _sl_width > _max_sl_mult * g_cachedATR) + { + PrintFormat("[FIX#141/FIX#465] TRADE REJECTED: SL too wide | SL=%.1fp ATR=%.1fp ratio=%.2f > max=%.1f | Trail/BE would be unreachable", + _sl_pips, g_cachedATR / g_pipValue, + _sl_width / g_cachedATR, _max_sl_mult); + g_ea_signal.isValid = false; + } + } + // =============================================================== + // * v9.09 FIX#15h: CENTRALIZED HTF TP CLAMP (ALL techniques!) + // BuildCandidateSLTP only covers JUDAS, OTE, BOS_RETEST. + // FVG, OB, TBS, CRT, LIQ_SWEEP, SB, BREAKER, TC all set + // their own TPs and BYPASS BuildCandidateSLTP -> unreachable TPs! + // This clamp covers EVERYTHING -- last gate before execution. + // + // Applied to ALL timeframes as safety net: + // M1-M5: Sharp moves, TP can be wider (3.5-4x ATR) + // M15-M30: Moderate (2.5-3x ATR) + // H1: Tighter (2.0-3x ATR) + // H4: Tight (1.5-2.5x ATR) + // D1+: Very tight (1.2-2x ATR) + // =============================================================== + { + double _atr = g_cachedATR; + double maxTP1x = 3.5, maxTP2x = 4.5, maxTP3x = 5.5; // M1-M5 defaults (widest) + // * FIX#CEIL_INDEX: Index pairs (US500/US100) place SL at structure levels 5-8xATR away. + // Forex 3xATR ceiling clamped TP1 below SL → post-clamp R:R < 1.0 → FIX#140 REJECT = 0 trades. + // Solution: Index pairs get 3x wider ceiling on all TFs. + bool _isIndexPair = (g_autoOptParams.pair_category == "Index"); + if(_Period >= PERIOD_D1) + { + // * v9.36 FIX#153b: D1 1.2->6.0 (pair profile TP1=4-5xATR -- 1.2 blocked everything) + // * FIX#434: D1 ceiling 6.0→8.0 — FIX#433 set tp1[4]=6.50×ATR which exceeded 6.0 clamp. + // After clamp: tp1=6.0, sl=2.50 → RR=2.40. Correct but wastes margin. + // 8.0 gives room for future calibration + vol-expanded SL without re-hitting ceiling. + maxTP1x = 8.0; maxTP2x = 10.0; maxTP3x = 13.0; + if(_isIndexPair) { maxTP1x = 12.0; maxTP2x = 16.0; maxTP3x = 22.0; } + } + else if(_Period >= PERIOD_H4) + { + // * v9.36 FIX#153b: H4 1.5->4.5 (GBPUSD pair profile TP1=3.2xATR, TP3=7.2xATR) + // OLD 1.5xATR clamp: SL=2.2xATR → clamped TP=1.5xATR → R:R=0.68 → FIX#140 REJECT = 0 trades + // NEW 4.5xATR clamp: allows TP1=3.2xATR, TP2=5.2, TP3=7.2 from pair profile to pass through + maxTP1x = 4.5; maxTP2x = 6.0; maxTP3x = 8.0; + if(_isIndexPair) { maxTP1x = 10.0; maxTP2x = 13.0; maxTP3x = 17.0; } + } + else if(_Period >= PERIOD_H1) + { + // * FIX#356: H1 clamp 3.5/4.5/5.5 → 6.5/8.0/10.0. + // Pair table: tp1=5.50 tp2=7.00 tp3=9.00 — old clamp 3.5 blocked them. + maxTP1x = 6.5; maxTP2x = 8.0; maxTP3x = 10.0; + if(_isIndexPair) { maxTP1x = 8.0; maxTP2x = 10.0; maxTP3x = 13.0; } + } + else if(_Period >= PERIOD_M15) // M15, M30 + { + // * FIX#356: M15 clamp 3.0/4.0/5.5 → 7.0/9.0/11.0. + // Pair table: tp1=6.00 tp2=7.50 tp3=9.00 — old clamp 3.0 blocked them ALL. + // (Same root cause as M5: pair table TP > hardcoded clamp → RR < mrr) + maxTP1x = 7.0; maxTP2x = 9.0; maxTP3x = 11.0; + // * FIX#CEIL_INDEX: US500/US100 M15 SL=4700-7200p, ATR=800-1000p → SL=5-8xATR. + // Old 3.0xATR ceiling → TP1≈2400p < SL=5000p → FIX#140 REJECT. + if(_isIndexPair) { maxTP1x = 10.0; maxTP2x = 13.0; maxTP3x = 16.0; } + } + else if(_Period >= PERIOD_M5) // M5 + { + // * FIX#356: 3.0/3.3/3.7 → 6.0/7.5/9.0. + // ROOT CAUSE of M5 zero trades: pair table tp1=5.00 but clamp=3.0 → TP capped at 3.0×ATR. + // SL=1.80×1.30(vol)=2.34, TP=3.0 → RR=1.28 < mrr=1.80 → ALL candidates rejected. + // FIX#19 confirms: "achievable=1.71 from TP=3.99/SL=2.34" → clamped value, not pair table. + // New clamp: 6.0/7.5/9.0 — above pair table values (5.00/6.50/8.00), allows them through. + maxTP1x = 6.0; maxTP2x = 7.5; maxTP3x = 9.0; + if(_isIndexPair) { maxTP1x = 8.0; maxTP2x = 10.0; maxTP3x = 12.0; } + } + // else M1: 3.5 / 4.5 / 5.5 (defaults above) + double _tp1d = MathAbs(g_ea_signal.tp1 - g_ea_signal.entryPrice); + double _tp2d = MathAbs(g_ea_signal.tp2 - g_ea_signal.entryPrice); + double _tp3d = MathAbs(g_ea_signal.tp3 - g_ea_signal.entryPrice); + bool _clamped = false; + if(_atr > 0) + { + if(_tp1d > maxTP1x * _atr) + { + _tp1d = maxTP1x * _atr; + g_ea_signal.tp1 = g_ea_signal.isBullish ? g_ea_signal.entryPrice + _tp1d : g_ea_signal.entryPrice - _tp1d; + _clamped = true; + } + if(_tp2d > maxTP2x * _atr) + { + _tp2d = maxTP2x * _atr; + g_ea_signal.tp2 = g_ea_signal.isBullish ? g_ea_signal.entryPrice + _tp2d : g_ea_signal.entryPrice - _tp2d; + _clamped = true; + } + if(_tp3d > maxTP3x * _atr) + { + _tp3d = maxTP3x * _atr; + g_ea_signal.tp3 = g_ea_signal.isBullish ? g_ea_signal.entryPrice + _tp3d : g_ea_signal.entryPrice - _tp3d; + _clamped = true; + } + // * v9.12 FIX: Enforce minimum TP2/TP3 separation from TP1 + // Old: 1.05/1.15 factor -- too tight, TP2 only 10-15% beyond TP1 + // After SL ladders to TP1, TP2 must be at least 1.0xATR beyond TP1 + // so normal M15 volatility doesn't immediately stop out the runner. + _tp2d = MathAbs(g_ea_signal.tp2 - g_ea_signal.entryPrice); + _tp3d = MathAbs(g_ea_signal.tp3 - g_ea_signal.entryPrice); + double minTP2sep = _tp1d + _atr * 1.0; // TP2 must be at least 1xATR beyond TP1 + double minTP3sep = _tp2d + _atr * 1.0; // TP3 must be at least 1xATR beyond TP2 + if(_tp2d < minTP2sep) + { + _tp2d = minTP2sep; + g_ea_signal.tp2 = g_ea_signal.isBullish ? g_ea_signal.entryPrice + _tp2d : g_ea_signal.entryPrice - _tp2d; + _clamped = true; + } + _tp3d = MathAbs(g_ea_signal.tp3 - g_ea_signal.entryPrice); + if(_tp3d < minTP3sep) + { + _tp3d = minTP3sep; + g_ea_signal.tp3 = g_ea_signal.isBullish ? g_ea_signal.entryPrice + _tp3d : g_ea_signal.entryPrice - _tp3d; + _clamped = true; + } + } + if(_clamped) + PrintFormat("* FIX#15h CENTRAL TP CLAMP [%s %s]: TP1=%.1fp(%.1fxATR) TP2=%.1fp(%.1fxATR) TP3=%.1fp(%.1fxATR)", + g_ea_signal.type, g_ea_signal.isBullish ? "BUY" : "SELL", + _tp1d / g_pipValue, _atr > 0 ? _tp1d / _atr : 0, + _tp2d / g_pipValue, _atr > 0 ? _tp2d / _atr : 0, + _tp3d / g_pipValue, _atr > 0 ? _tp3d / _atr : 0); + } + // * v9.09 FIX#16e: Log MTF direction with trade for debugging + PrintFormat("* MTF Direction: %s (Conf=%.0f%%) | Bull TFs=%d Bear TFs=%d | %s", + g_mtfAnalysis.alignment, g_mtfAnalysis.overallConfidence, + g_mtfAnalysis.bullishTFs, g_mtfAnalysis.bearishTFs, + g_mtfAnalysis.details); + Print("* VALID SIGNAL! Type: ", g_ea_signal.type, + " | Dir: ", g_ea_signal.isBullish ? "BUY" : "SELL", + " | Score: ", g_ea_signal.score, + " | Entry: ", DoubleToString(g_ea_signal.entryPrice, _Digits), + " | SL: ", DoubleToString(g_ea_signal.stopLoss, _Digits), + " | TP1: ", DoubleToString(g_ea_signal.tp1, _Digits), + " | Candidates: ", g_candidateCount); + // * v9.36 FIX#140: POST-CLAMP ACTUAL R:R VALIDATION + // Problem: R:R is checked BEFORE FIX#15h clamp. Clamp can shrink TP1 below SL dist + // → actual R:R < 1.0 → losing trade by design (e.g. ID=7: TP=14.9p, SL=15.2p → R:R=0.98). + // Fix A: After clamp, recompute actual R:R from final TP1 and SL values. + // Fix B: If actual R:R < 1.0, try to expand TP1 to achieve at least 1.0R. + // But only if expansion doesn't exceed the ATR clamp ceiling. + // If SL is so wide that even 1.0R TP exceeds the ceiling → REJECT the trade. + // Fix C: If actual R:R is between 1.0 and EA_MinRR (e.g. 1.0–1.3), allow (A+ exception + // already handled this) but log a warning. + { + double _sl_dist_pc = MathAbs(g_ea_signal.entryPrice - g_ea_signal.stopLoss); + double _tp1_dist_pc = MathAbs(g_ea_signal.tp1 - g_ea_signal.entryPrice); + double _actual_rr = (_sl_dist_pc > 0) ? (_tp1_dist_pc / _sl_dist_pc) : 0; + if(_actual_rr < 1.0 && _sl_dist_pc > 0) + { + // TP was clamped below SL distance -- try to fix by expanding TP1 + double _needed_tp1_dist = _sl_dist_pc * 1.05; // 1.05R minimum (5% above 1:1) + // * v9.36 FIX#153: ATR ceiling corrected per TF + // OLD values: H4=1.5 (M15-level!), D1=1.2 -- way too low for swing TFs + // SL=2.2×ATR on H4 → minimum TP=2.2×ATR -- needs ceiling >= 3.5×ATR at least + // NEW: ceiling = max TP3 multiplier for each TF (from pair profile table) + // This check is a LAST RESORT guard -- normal signals have TP1>>SL already. + double _atr_ceil = g_cachedATR; + double _max_tp1x = 4.0; // M5 default + if(_Period >= PERIOD_D1) _max_tp1x = 10.0; // D1: TP3=7-9×ATR possible + else if(_Period >= PERIOD_H4) _max_tp1x = 9.0; // H4: TP3=5-8×ATR (GBPUSD=7.2) + else if(_Period >= PERIOD_H1) _max_tp1x = 6.0; // H1: TP3=4-5×ATR + else if(_Period >= PERIOD_M15) _max_tp1x = 5.0; // M15: TP3=3-4.5×ATR + else if(_Period >= PERIOD_M5) _max_tp1x = 4.0; // M5: TP3=2.5-3×ATR + // * FIX#CEIL_INDEX: Index pairs (US500/US100) SL can be 5-9×ATR (structure-based). + // Post-clamp ceiling must accommodate 1.05R on these wide SLs. + // US500 M15: SL=7200p, ATR=1000p → 1.05R=7560p. Need ceiling >= 7.6×ATR. + // Using 15×ATR gives ceiling=15000p — still protective vs runaway SLs. + if(g_autoOptParams.pair_category == "Index") + { + if(_Period >= PERIOD_D1) _max_tp1x = 20.0; + else if(_Period >= PERIOD_H4) _max_tp1x = 18.0; + else if(_Period >= PERIOD_H1) _max_tp1x = 15.0; + else if(_Period >= PERIOD_M15) _max_tp1x = 15.0; // US500 M15: SL up to 8×ATR + else _max_tp1x = 12.0; + } + if(_atr_ceil > 0 && _needed_tp1_dist <= _max_tp1x * _atr_ceil) + { + // Expand TP1 to 1.05R -- still within ATR ceiling + g_ea_signal.tp1 = g_ea_signal.isBullish + ? g_ea_signal.entryPrice + _needed_tp1_dist + : g_ea_signal.entryPrice - _needed_tp1_dist; + PrintFormat("[FIX#140] Post-clamp R:R=%.2f < 1.0 → TP1 expanded to 1.05R (%.1fp). SL=%.1fp ATR=%.1fp", + _actual_rr, _needed_tp1_dist / g_pipValue, + _sl_dist_pc / g_pipValue, _atr_ceil / g_pipValue); + } + else + { + // SL too wide vs ATR clamp ceiling -- cannot achieve 1.0R → REJECT + PrintFormat("[FIX#140] TRADE REJECTED: post-clamp R:R=%.2f < 1.0 | SL=%.1fp ATR=%.1fp | 1.05R=%.1fp > ceiling=%.1fp", + _actual_rr, _sl_dist_pc / g_pipValue, + _atr_ceil / g_pipValue, + _needed_tp1_dist / g_pipValue, + _max_tp1x * _atr_ceil / g_pipValue); + g_ea_signal.isValid = false; + } + } + else if(_actual_rr < EA_MinRR && _actual_rr >= 1.0) + { + PrintFormat("[FIX#140] WARN: post-clamp R:R=%.2f below EA_MinRR=%.2f (but >=1.0). A+ exception applies.", + _actual_rr, (double)EA_MinRR); + } + } + if(g_ea_signal.isValid) + EA_ExecuteTrade(); + } + else + { + // Debug why signal was rejected + static datetime last_rejection_log = 0; + if(time[0] != last_rejection_log) + { + last_rejection_log = time[0]; + string mtfDir = "N/A"; + if(MTF_Enabled) + { + if(g_mtfAnalysis.overallDirection == MTF_BULLISH) mtfDir = "BULL"; + else if(g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH) mtfDir = "STRONG_BULL"; + else if(g_mtfAnalysis.overallDirection == MTF_BEARISH) mtfDir = "BEAR"; + else if(g_mtfAnalysis.overallDirection == MTF_STRONG_BEARISH) mtfDir = "STRONG_BEAR"; + else mtfDir = "NEUTRAL"; + } + double currentSpread = (double)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * g_point / g_pipValue; + Print("Bar State: FVG=", g_fvgCount, " OB=", g_obCount, + " TBS=", g_tbsCount, " CRT=", g_crtCount, + " | KZ=", EA_IsInKillzone() ? "YES" : "NO", + " | MTF=", mtfDir, + " | Structure=", g_isBullishStructure ? "BULL" : "BEAR", + " | Regime=", GetRegimeString(g_regimeData.regime), + " | Spread=", DoubleToString(currentSpread, 1), "p", + " | ATR=", DoubleToString(g_cachedATR / g_pipValue, 1), "p", + " | Candidates=", g_candidateCount); + if(g_fvgCount == 0 && g_obCount == 0 && g_tbsCount == 0 && g_crtCount == 0) + Print(" No active signal sources"); + } + } +} +#endif // COMPILE_AS_EA - OnNewBar +//+------------------------------------------------------------------+ +//| Determine Timeframe Strategy Category | +//+------------------------------------------------------------------+ +ENUM_TF_STRATEGY GetTimeframeStrategy() +{ + ENUM_TIMEFRAMES tf = _Period; + if(tf <= PERIOD_M5) return TF_SCALPING; + if(tf <= PERIOD_M30) return TF_INTRADAY; + if(tf <= PERIOD_H4) return TF_SWING; + return TF_POSITION; +} +//+------------------------------------------------------------------+ +//| * Build Technique Priority List Based on Regime+TF+Pair | +//+------------------------------------------------------------------+ +void BuildTechniquePriorities() +{ + ENUM_MARKET_REGIME regime = g_regimeData.regime; + ENUM_TF_STRATEGY tfStrat = GetTimeframeStrategy(); + g_techState.tfStrategy = tfStrat; + g_techState.activeRegime = regime; + // Initialize all techniques + for(int i = 0; i < (int)TECH_TOTAL; i++) + { + g_techState.priorities[i].technique = (ENUM_ENTRY_TECHNIQUE)i; + g_techState.priorities[i].priority = 5; + g_techState.priorities[i].confidenceBoost = 0; + g_techState.priorities[i].minQuality = 0.6; + g_techState.priorities[i].enabled = true; + } + // Set names + g_techState.priorities[TECH_FVG].name = "FVG"; + g_techState.priorities[TECH_OB].name = "OB"; + g_techState.priorities[TECH_TBS].name = "TBS"; + g_techState.priorities[TECH_CRT].name = "CRT"; + g_techState.priorities[TECH_LIQ_SWEEP].name = "LIQ_SWEEP"; + g_techState.priorities[TECH_JUDAS].name = "JUDAS"; + g_techState.priorities[TECH_SILVER_BULLET].name = "SB"; + g_techState.priorities[TECH_BREAKER].name = "BREAKER"; + g_techState.priorities[TECH_OTE].name = "OTE"; + g_techState.priorities[TECH_BOS_RETEST].name = "BOS_RETEST"; + g_techState.priorities[TECH_TREND_CONT].name = "TREND_CONT"; // * v7.8 + g_techState.priorities[TECH_MEAN_REV].name = "MEAN_REV"; // * FIX#373 + // Check module enabled flags + // * FIX#460: STRATEGY_* inputs now AND-gate with Enable* inputs for real trades. + // BEFORE: STRATEGY_FVG_Entry only gated the legacy SIGNAL_Array visual tracker. + // Real trades via EA_CheckSignals → TECH_FVG used only EnableFVG. + // User could set STRATEGY_FVG_Entry=false thinking FVG trades stopped — they didn't. + // FIX: techState.enabled = Enable* AND STRATEGY_* (both must be true). + // If user disables either the Enable* OR the STRATEGY_* input → technique disabled for real trades. + // STRATEGY_MM_Model/ML_Signal/Killzone have no TECH_* equivalent — gate legacy path only (unchanged). + g_techState.priorities[TECH_FVG].enabled = EnableFVG && STRATEGY_FVG_Entry; + g_techState.priorities[TECH_OB].enabled = EnableOB && STRATEGY_OB_Entry; + g_techState.priorities[TECH_LIQ_SWEEP].enabled = EnableLiquidity && STRATEGY_LIQ_Grab; + g_techState.priorities[TECH_OTE].enabled = EnableOTE && STRATEGY_OTE_Entry; + g_techState.priorities[TECH_BREAKER].enabled = EnableBreakerBlocks && STRATEGY_BB_Entry; + g_techState.priorities[TECH_BOS_RETEST].enabled= EnableStructure && STRATEGY_BOS_Retest; + g_techState.priorities[TECH_TBS].enabled = TBS_Enabled; + g_techState.priorities[TECH_CRT].enabled = CRT_Enabled; + g_techState.priorities[TECH_JUDAS].enabled = Judas_Enabled; + g_techState.priorities[TECH_SILVER_BULLET].enabled = KZ_EnableSilverBullet; + g_techState.priorities[TECH_TREND_CONT].enabled = g_tcEnabled; // * v7.8 / FIX#453 + // * FIX#373: MR enabled = Regime_UseMeanReversion global OR pair table force-on + g_techState.priorities[TECH_MEAN_REV].enabled = Regime_UseMeanReversion; + // =============================================================== + // * REGIME-BASED PRIORITY ADJUSTMENT + // =============================================================== + switch(regime) + { + case REGIME_TRENDING: + case REGIME_TREND_UP: + case REGIME_TREND_DOWN: + case REGIME_STRONG_TREND_UP: + case REGIME_STRONG_TREND_DOWN: + case REGIME_WEAK_TREND_UP: // * v7.8 FIX: WEAK trends were falling to default (Regime:0 bonus) + case REGIME_WEAK_TREND_DOWN: + // * v7.8: Trending -> TREND_CONT is king (EMA pullback + momentum) + g_techState.priorities[TECH_TREND_CONT].priority = 1; + g_techState.priorities[TECH_TREND_CONT].confidenceBoost = 18; + g_techState.priorities[TECH_BOS_RETEST].priority = 2; + g_techState.priorities[TECH_BOS_RETEST].confidenceBoost = 15; + g_techState.priorities[TECH_OB].priority = 3; + g_techState.priorities[TECH_OB].confidenceBoost = 12; + g_techState.priorities[TECH_OTE].priority = 4; + g_techState.priorities[TECH_OTE].confidenceBoost = 10; + g_techState.priorities[TECH_FVG].priority = 5; + g_techState.priorities[TECH_FVG].confidenceBoost = 8; + g_techState.priorities[TECH_CRT].priority = 6; + g_techState.priorities[TECH_CRT].confidenceBoost = 5; + // Reduce counter-trend techniques + g_techState.priorities[TECH_TBS].priority = 9; + g_techState.priorities[TECH_TBS].confidenceBoost = -5; + g_techState.priorities[TECH_LIQ_SWEEP].priority = 8; + g_techState.priorities[TECH_LIQ_SWEEP].confidenceBoost = -3; + break; + case REGIME_RANGING: + case REGIME_RANGING_TIGHT: + case REGIME_RANGING_WIDE: + // * FIX#373: RANGING: MEAN_REV is priority 1 alongside OTE (both exploit range boundaries) + // Ranging is the ideal MR environment — confirmed range, no momentum + g_techState.priorities[TECH_MEAN_REV].priority = 1; + g_techState.priorities[TECH_MEAN_REV].confidenceBoost = 18; + // Ranging: FVG + OTE + Liquidity sweeps — shift down + g_techState.priorities[TECH_OTE].priority = 2; + g_techState.priorities[TECH_OTE].confidenceBoost = 12; + g_techState.priorities[TECH_FVG].priority = 3; + g_techState.priorities[TECH_FVG].confidenceBoost = 10; + g_techState.priorities[TECH_LIQ_SWEEP].priority = 4; + g_techState.priorities[TECH_LIQ_SWEEP].confidenceBoost = 10; + g_techState.priorities[TECH_OB].priority = 5; + g_techState.priorities[TECH_OB].confidenceBoost = 8; + g_techState.priorities[TECH_TBS].priority = 6; + g_techState.priorities[TECH_TBS].confidenceBoost = 5; + // BOS retest unreliable in range; TREND_CONT disabled (no trend to continue) + g_techState.priorities[TECH_BOS_RETEST].priority = 9; + g_techState.priorities[TECH_BOS_RETEST].confidenceBoost = -10; + g_techState.priorities[TECH_TREND_CONT].priority = 10; + g_techState.priorities[TECH_TREND_CONT].confidenceBoost = -15; + g_techState.priorities[TECH_TREND_CONT].enabled = false; // * v7.8: No trend = no continuation + break; + case REGIME_VOLATILE: + case REGIME_CHOPPY: + // * FIX#373: CHOPPY: MEAN_REV is priority 1 — range boundary entries with RSI extreme. + // Range-bound markets don't trend → TBS/Judas still valid but MR is the primary edge. + // MR takes priority slot 1; TBS/Judas/SB shift down by 1. + g_techState.priorities[TECH_MEAN_REV].priority = 1; + g_techState.priorities[TECH_MEAN_REV].confidenceBoost = 20; // +20 vs other techniques + // Volatile/Choppy: TBS + Judas + Silver Bullet (sweep-and-reverse) — shift down + g_techState.priorities[TECH_TBS].priority = 2; + g_techState.priorities[TECH_TBS].confidenceBoost = 15; + g_techState.priorities[TECH_JUDAS].priority = 3; + g_techState.priorities[TECH_JUDAS].confidenceBoost = 12; + g_techState.priorities[TECH_SILVER_BULLET].priority = 4; + g_techState.priorities[TECH_SILVER_BULLET].confidenceBoost = 10; + g_techState.priorities[TECH_LIQ_SWEEP].priority = 5; + g_techState.priorities[TECH_LIQ_SWEEP].confidenceBoost = 8; + // Raise quality bar for everything + for(int i = 0; i < (int)TECH_TOTAL; i++) + g_techState.priorities[i].minQuality = 0.80; + // * v9.02 FIX: CHOPPY/VOLATILE -- raise quality bar instead of disabling everything. + // Old behaviour: FVG/OB/OTE/BOS/BREAKER/CRT all disabled -> 0 candidates -> 0 trades. + // New behaviour: keep all enabled but require higher quality (0.85) and score penalties. + // Only TREND_CONT disabled (genuinely useless with no trend). + g_techState.priorities[TECH_TREND_CONT].enabled = false; // No trend = no continuation + // FVG/OB/OTE/BOS/BREAKER/CRT: KEEP ENABLED, just deprioritise + g_techState.priorities[TECH_FVG].confidenceBoost -= 8; + g_techState.priorities[TECH_OB].confidenceBoost -= 8; + g_techState.priorities[TECH_BREAKER].confidenceBoost -= 5; + g_techState.priorities[TECH_OTE].confidenceBoost -= 5; + g_techState.priorities[TECH_BOS_RETEST].confidenceBoost -= 10; + g_techState.priorities[TECH_CRT].confidenceBoost -= 5; + // minQuality already set to 0.80 above for all -- now stricter + for(int i = 0; i < (int)TECH_TOTAL; i++) + g_techState.priorities[i].minQuality = 0.85; // Extra quality gate + if(g_verboseLog) + Print("* v9.02 CHOPPY/VOLATILE: FVG/OB/OTE active with -score penalty. Only TC disabled. MR = priority 1."); + break; + case REGIME_BREAKOUT: + // Breakout: CRT + TBS + Breaker + BOS; TREND_CONT moderate (early trend) + g_techState.priorities[TECH_CRT].priority = 1; + g_techState.priorities[TECH_CRT].confidenceBoost = 15; + g_techState.priorities[TECH_TBS].priority = 2; + g_techState.priorities[TECH_TBS].confidenceBoost = 12; + g_techState.priorities[TECH_BREAKER].priority = 3; + g_techState.priorities[TECH_BREAKER].confidenceBoost = 10; + g_techState.priorities[TECH_BOS_RETEST].priority = 4; + g_techState.priorities[TECH_BOS_RETEST].confidenceBoost = 8; + g_techState.priorities[TECH_FVG].priority = 5; + g_techState.priorities[TECH_FVG].confidenceBoost = 5; + g_techState.priorities[TECH_TREND_CONT].priority = 6; // * v7.8: Post-breakout momentum + g_techState.priorities[TECH_TREND_CONT].confidenceBoost = 5; + break; + default: // UNKNOWN - balanced approach + g_techState.priorities[TECH_OB].priority = 2; + g_techState.priorities[TECH_FVG].priority = 3; + g_techState.priorities[TECH_TBS].priority = 3; + g_techState.priorities[TECH_CRT].priority = 4; + break; + } + // =============================================================== + // * TIMEFRAME-BASED ADJUSTMENTS + // =============================================================== + switch(tfStrat) + { + case TF_SCALPING: + // Scalping: Silver Bullet + FVG are best, disable swing setups + g_techState.priorities[TECH_SILVER_BULLET].priority = MathMin(g_techState.priorities[TECH_SILVER_BULLET].priority, 2); + g_techState.priorities[TECH_SILVER_BULLET].confidenceBoost += 8; + g_techState.priorities[TECH_FVG].confidenceBoost += 5; + // Disable slow setups + g_techState.priorities[TECH_BOS_RETEST].enabled = false; + g_techState.priorities[TECH_OTE].enabled = false; + break; + case TF_INTRADAY: + // Intraday: Killzone-based entries excel + g_techState.priorities[TECH_JUDAS].confidenceBoost += 8; + g_techState.priorities[TECH_SILVER_BULLET].confidenceBoost += 5; + g_techState.priorities[TECH_CRT].confidenceBoost += 3; + break; + case TF_SWING: + // Swing: Structure + OB + OTE + g_techState.priorities[TECH_OB].confidenceBoost += 8; + g_techState.priorities[TECH_OTE].confidenceBoost += 8; + g_techState.priorities[TECH_BOS_RETEST].confidenceBoost += 5; + g_techState.priorities[TECH_BREAKER].confidenceBoost += 5; + // Silver Bullet less useful on H1-H4 + g_techState.priorities[TECH_SILVER_BULLET].enabled = false; + break; + case TF_POSITION: + // Position: Only major structure entries + g_techState.priorities[TECH_OB].confidenceBoost += 10; + g_techState.priorities[TECH_BOS_RETEST].confidenceBoost += 10; + g_techState.priorities[TECH_BREAKER].confidenceBoost += 8; + // Disable fast setups + g_techState.priorities[TECH_SILVER_BULLET].enabled = false; + g_techState.priorities[TECH_CRT].enabled = false; + g_techState.priorities[TECH_JUDAS].enabled = false; + break; + } + // =============================================================== + // * PAIR-BASED ADJUSTMENTS + // =============================================================== + if(g_gates.computed) + { + if(tfStrat == TF_SCALPING && !g_autoOptParams.allow_scalping) + { + g_techState.priorities[TECH_SILVER_BULLET].enabled = false; + g_techState.priorities[TECH_FVG].priority += 3; + } + if(!g_autoOptParams.allow_liq_grab) + { + g_techState.priorities[TECH_LIQ_SWEEP].enabled = false; + g_techState.priorities[TECH_TBS].confidenceBoost -= 5; + } + if(!g_autoOptParams.allow_bos_retest) + g_techState.priorities[TECH_BOS_RETEST].enabled = false; + if(!g_autoOptParams.allow_ote) + g_techState.priorities[TECH_OTE].enabled = false; + } + g_techState.lastUpdate = TimeCurrent(); +} +//+------------------------------------------------------------------+ +//| * Multi-Confirmation Cascade | +//+------------------------------------------------------------------+ +ConfirmationCascade RunConfirmationCascade(bool isBullish, ENUM_ENTRY_TECHNIQUE sourceTech = TECH_FVG) // * v6.35: Added sourceTech to prevent self-confirmation +{ + ConfirmationCascade cascade; + ZeroMemory(cascade); + cascade.totalConfirmed = 0; + cascade.totalChecked = 0; + cascade.totalPoints = 0; + double strengthSum = 0; + double current_price = (SymbolInfoDouble(_Symbol, SYMBOL_ASK) + SymbolInfoDouble(_Symbol, SYMBOL_BID)) / 2.0; + // -- 1. STRUCTURE ALIGNMENT (0-18 pts) -- + if(EnableStructure) + { + cascade.confirmations[CONF_STRUCTURE].source = CONF_STRUCTURE; + cascade.totalChecked++; + bool aligned = (isBullish && g_isBullishStructure) || (!isBullish && !g_isBullishStructure); + cascade.confirmations[CONF_STRUCTURE].confirmed = aligned; + cascade.confirmations[CONF_STRUCTURE].strength = aligned ? 0.9 : 0.3; // * v7.4 FIX: was 0.1 -> counter-structure got no credit + cascade.confirmations[CONF_STRUCTURE].points = aligned ? 18 : 6; // * v7.4 FIX: was 0 -> 34pt penalty killed ALL shorts + cascade.confirmations[CONF_STRUCTURE].detail = aligned ? "Structure aligned" : "Counter-structure"; + if(aligned) { cascade.totalConfirmed++; cascade.totalPoints += 18; strengthSum += 0.9; } + else { cascade.totalPoints += 6; strengthSum += 0.3; } // * v7.4: Partial credit + } + // -- 2. FVG CONFLUENCE (0-14 pts) -- + // * v6.35 FIX #4: Skip when source IS an FVG (self-confirmation!) + // FVG candidate always finds itself as "nearby FVG" -> auto +14 pts + if(EnableFVG && g_fvgCount > 0 && sourceTech != TECH_FVG) + { + cascade.confirmations[CONF_FVG].source = CONF_FVG; + cascade.totalChecked++; + bool found = false; + double bestStr = 0; + int fvgSz = ArraySize(FVG_Array); // * v7.4: bounds safety + for(int i = 0; i < fvgSz && !found; i++) // * v9.02 FIX: was MathMin(g_fvgCount,fvgSz) -- missed active FVGs at high indices + { + if(g_fvgs[i].active && g_fvgs[i].isBullish == isBullish) + { + double dist = MathAbs(current_price - (g_fvgs[i].top + g_fvgs[i].bottom) / 2.0); + if(dist < g_cachedATR * 3.0) + { + found = true; + if(g_fvgs[i].quality == FVG_QUALITY_PREMIUM) bestStr = 1.0; + else if(g_fvgs[i].quality == FVG_QUALITY_HIGH) bestStr = 0.8; + else if(g_fvgs[i].quality == FVG_QUALITY_MEDIUM) bestStr = 0.5; + else bestStr = 0.3; + } + } + } + cascade.confirmations[CONF_FVG].confirmed = found; + cascade.confirmations[CONF_FVG].strength = bestStr; + cascade.confirmations[CONF_FVG].points = found ? (int)(14 * bestStr) : 0; + cascade.confirmations[CONF_FVG].detail = found ? "FVG confluence" : "No FVG nearby"; + if(found) { cascade.totalConfirmed++; cascade.totalPoints += cascade.confirmations[CONF_FVG].points; strengthSum += bestStr; } + } + // -- 3. ORDER BLOCK CONFLUENCE (0-14 pts) -- + // * v6.35 FIX #4: Skip when source IS an OB (self-confirmation!) + if(EnableOB && g_obCount > 0 && sourceTech != TECH_OB) + { + cascade.confirmations[CONF_OB].source = CONF_OB; + cascade.totalChecked++; + bool found = false; + double bestStr = 0; + int obSz = ArraySize(OB_Array); // * v7.4: bounds safety + for(int i = 0; i < MathMin(g_obCount, obSz) && !found; i++) + { + if(g_obs[i].active && !g_obs[i].mitigated && g_obs[i].isBullish == isBullish) + { + double dist = MathAbs(current_price - (g_obs[i].top + g_obs[i].bottom) / 2.0); + if(dist < g_cachedATR * 3.0) + { + found = true; + // * vFIX CRITICAL: OB.strength = vol/avgVol (unbounded, 1-50+) + // Was causing totalPoints 700+ (14*50) instead of max 14! + bestStr = MathMin(1.0, g_obs[i].strength / 3.0); + } + } + } + cascade.confirmations[CONF_OB].confirmed = found; + cascade.confirmations[CONF_OB].strength = bestStr; + cascade.confirmations[CONF_OB].points = found ? (int)(14 * bestStr) : 0; + cascade.confirmations[CONF_OB].detail = found ? "OB confluence" : "No OB nearby"; + if(found) { cascade.totalConfirmed++; cascade.totalPoints += cascade.confirmations[CONF_OB].points; strengthSum += bestStr; } + } + // -- 4. KILLZONE TIMING (0-15 pts) -- + { + cascade.confirmations[CONF_KILLZONE].source = CONF_KILLZONE; + cascade.totalChecked++; + bool inKz = EA_IsInKillzone(); + cascade.confirmations[CONF_KILLZONE].confirmed = inKz; + cascade.confirmations[CONF_KILLZONE].strength = inKz ? 0.85 : 0.0; + cascade.confirmations[CONF_KILLZONE].points = inKz ? 15 : 0; + cascade.confirmations[CONF_KILLZONE].detail = inKz ? "In Killzone" : "Outside Killzone"; + if(inKz) { cascade.totalConfirmed++; cascade.totalPoints += 15; strengthSum += 0.85; } + } + // -- 5. HTF TREND ALIGNMENT (0-16 pts) -- + if(MTF_Enabled || EnableHTFConfirmation) + { + cascade.confirmations[CONF_HTF_TREND].source = CONF_HTF_TREND; + cascade.totalChecked++; + ENUM_MTF_DIRECTION mtf = GetMTFDirection(); + bool aligned = false; + double str = 0; + if(isBullish) + { + if(mtf == MTF_STRONG_BULLISH) { aligned = true; str = 1.0; } + else if(mtf == MTF_BULLISH) { aligned = true; str = 0.7; } + } + else + { + if(mtf == MTF_STRONG_BEARISH) { aligned = true; str = 1.0; } + else if(mtf == MTF_BEARISH) { aligned = true; str = 0.7; } + } + cascade.confirmations[CONF_HTF_TREND].confirmed = aligned; + cascade.confirmations[CONF_HTF_TREND].strength = str; + cascade.confirmations[CONF_HTF_TREND].points = aligned ? (int)(16 * str) : 0; + cascade.confirmations[CONF_HTF_TREND].detail = aligned ? "HTF trend aligned" : "HTF disagrees"; + if(aligned) { cascade.totalConfirmed++; cascade.totalPoints += cascade.confirmations[CONF_HTF_TREND].points; strengthSum += str; } + } + // -- 6. PREMIUM/DISCOUNT ZONE (0-14 pts) -- + { + cascade.confirmations[CONF_PREMIUM_DISC].source = CONF_PREMIUM_DISC; + cascade.totalChecked++; + bool optimal = false; + double str = 0; + if(isBullish && g_currentPDZone == "DISCOUNT") { optimal = true; str = 1.0; } + else if(!isBullish && g_currentPDZone == "PREMIUM") { optimal = true; str = 1.0; } + else if(g_currentPDZone == "EQUILIBRIUM") { optimal = false; str = 0.3; } + cascade.confirmations[CONF_PREMIUM_DISC].confirmed = optimal; + cascade.confirmations[CONF_PREMIUM_DISC].strength = str; + cascade.confirmations[CONF_PREMIUM_DISC].points = optimal ? 14 : (g_currentPDZone == "EQUILIBRIUM" ? 3 : 0); + cascade.confirmations[CONF_PREMIUM_DISC].detail = optimal ? "Optimal zone" : "Not in optimal zone"; + if(optimal) { cascade.totalConfirmed++; cascade.totalPoints += 14; strengthSum += str; } + } + // -- 7. VSA CONFIRMATION (0-12 pts) -- + if(VSA_Enabled && g_vsaCount > 0) + { + cascade.confirmations[CONF_VSA].source = CONF_VSA; + cascade.totalChecked++; + bool found = false; + double str = 0; + for(int i = ArraySize(g_vsaPatterns) - 1; i >= MathMax(0, ArraySize(g_vsaPatterns) - 3); i--) + { + if((isBullish && g_vsaPatterns[i].isBullish) || (!isBullish && g_vsaPatterns[i].isBearish)) + { + found = true; + // * vFIX CRITICAL: VSA strength = 55-90 (percentage, NOT 0-1!) + // Was causing totalPoints = 12*80 = 960 instead of max 12! + str = MathMin(1.0, g_vsaPatterns[i].strength / 100.0); + break; + } + } + cascade.confirmations[CONF_VSA].confirmed = found; + cascade.confirmations[CONF_VSA].strength = str; + cascade.confirmations[CONF_VSA].points = found ? (int)(12 * str) : 0; + cascade.confirmations[CONF_VSA].detail = found ? "VSA confirms" : "No VSA support"; + if(found) { cascade.totalConfirmed++; cascade.totalPoints += cascade.confirmations[CONF_VSA].points; strengthSum += str; } + } + // -- 8. DIVERGENCE (0-12 pts) -- + if(Divergence_Enabled && g_divergenceCount > 0) + { + cascade.confirmations[CONF_DIVERGENCE].source = CONF_DIVERGENCE; + cascade.totalChecked++; + bool found = false; + for(int i = g_divergenceCount - 1; i >= 0; i--) + { + if(!g_divergences[i].active) continue; + bool divBull = (g_divergences[i].type == DIV_REGULAR_BULLISH || g_divergences[i].type == DIV_HIDDEN_BULLISH); + if(divBull == isBullish) { found = true; break; } + } + cascade.confirmations[CONF_DIVERGENCE].confirmed = found; + cascade.confirmations[CONF_DIVERGENCE].strength = found ? 0.8 : 0.0; + cascade.confirmations[CONF_DIVERGENCE].points = found ? 12 : 0; + cascade.confirmations[CONF_DIVERGENCE].detail = found ? "Divergence supports" : "No divergence"; + if(found) { cascade.totalConfirmed++; cascade.totalPoints += 12; strengthSum += 0.8; } + } + // -- 9. TRENDLINE (0-10 pts) -- + if(Trendline_Enabled) + { + cascade.confirmations[CONF_TRENDLINE].source = CONF_TRENDLINE; + cascade.totalChecked++; + bool support = false; + if(isBullish && (g_tlSupportActive || g_trendlineBreakBull)) support = true; + else if(!isBullish && (g_tlResistanceActive || g_trendlineBreakBear)) support = true; + cascade.confirmations[CONF_TRENDLINE].confirmed = support; + cascade.confirmations[CONF_TRENDLINE].strength = support ? 0.7 : 0.0; + cascade.confirmations[CONF_TRENDLINE].points = support ? 10 : 0; + cascade.confirmations[CONF_TRENDLINE].detail = support ? "Trendline supports" : "No trendline"; + if(support) { cascade.totalConfirmed++; cascade.totalPoints += 10; strengthSum += 0.7; } + } + // -- 10. AMD PHASE (0-10 pts) -- + // * v6.38 FIX BUG#3: AMD was only checking DISTRIBUTION regardless of direction! + // Bullish entries should confirm during ACCUMULATION (Smart Money buying) + // Bearish entries should confirm during DISTRIBUTION (Smart Money selling) + if(AMD_Enabled) + { + cascade.confirmations[CONF_AMD_PHASE].source = CONF_AMD_PHASE; + cascade.totalChecked++; + bool optimal = false; + if(isBullish && (g_amdData.phase == AMD_ACCUMULATION || g_amdData.phase == AMD_REACCUMULATION)) + optimal = true; + else if(!isBullish && (g_amdData.phase == AMD_DISTRIBUTION || g_amdData.phase == AMD_REDISTRIBUTION)) + optimal = true; + cascade.confirmations[CONF_AMD_PHASE].confirmed = optimal; + cascade.confirmations[CONF_AMD_PHASE].strength = optimal ? 0.8 : 0.0; + cascade.confirmations[CONF_AMD_PHASE].points = optimal ? 10 : 0; + cascade.confirmations[CONF_AMD_PHASE].detail = optimal ? "AMD phase confirms direction" : "AMD phase neutral/opposing"; + if(optimal) { cascade.totalConfirmed++; cascade.totalPoints += 10; strengthSum += 0.8; } + } + // -- 11. RSI CONFIRMATION (0-10 pts) -- + if(g_cachedRSI > 0) + { + cascade.confirmations[CONF_RSI].source = CONF_RSI; + cascade.totalChecked++; + bool confirms = false; + double str = 0; + if(isBullish && g_cachedRSI < 35) { confirms = true; str = 1.0; } + else if(isBullish && g_cachedRSI < 45) { confirms = true; str = 0.6; } + else if(!isBullish && g_cachedRSI > 65) { confirms = true; str = 1.0; } + else if(!isBullish && g_cachedRSI > 55) { confirms = true; str = 0.6; } + cascade.confirmations[CONF_RSI].confirmed = confirms; + cascade.confirmations[CONF_RSI].strength = str; + cascade.confirmations[CONF_RSI].points = confirms ? (int)(10 * str) : 0; + cascade.confirmations[CONF_RSI].detail = confirms ? StringFormat("RSI=%.1f confirms", g_cachedRSI) : "RSI neutral"; + if(confirms) { cascade.totalConfirmed++; cascade.totalPoints += cascade.confirmations[CONF_RSI].points; strengthSum += str; } + } + // -- 12. REGIME ALIGNMENT (0-10 pts) -- + // * v6.38: Always check (DetectMarketRegime now runs unconditionally) + { + cascade.confirmations[CONF_REGIME].source = CONF_REGIME; + cascade.totalChecked++; + bool favorable = (g_regimeData.regime == REGIME_TRENDING || + g_regimeData.regime == REGIME_TREND_UP || + g_regimeData.regime == REGIME_TREND_DOWN || + g_regimeData.regime == REGIME_BREAKOUT); + cascade.confirmations[CONF_REGIME].confirmed = favorable; + cascade.confirmations[CONF_REGIME].strength = favorable ? 0.75 : 0.0; + cascade.confirmations[CONF_REGIME].points = favorable ? 10 : 0; + cascade.confirmations[CONF_REGIME].detail = favorable ? "Favorable regime" : "Unfavorable regime"; + if(favorable) { cascade.totalConfirmed++; cascade.totalPoints += 10; strengthSum += 0.75; } + } + // -- ML confirmation: NN predicts TP1 win probability > 55% -- + // The neural network is trained on historical TP1 outcomes for this pair/TF. + // When it agrees with the setup direction AND signals >55% probability, it counts + // as an independent confirmation — the same weight as regime or RSI confirmation. + { + cascade.confirmations[CONF_ML].source = CONF_ML; + cascade.totalChecked++; + bool mlConfirms = g_nnReadyForUse && g_nnTP1WinProb > 0.55; + double mlStr = mlConfirms ? MathMin(1.0, (g_nnTP1WinProb - 0.50) * 4.0) : 0.0; + int mlPts = mlConfirms ? (int)(10.0 * mlStr) : 0; + cascade.confirmations[CONF_ML].confirmed = mlConfirms; + cascade.confirmations[CONF_ML].strength = mlStr; + cascade.confirmations[CONF_ML].points = mlPts; + cascade.confirmations[CONF_ML].detail = mlConfirms + ? StringFormat("ML WP=%.0f%%", g_nnTP1WinProb * 100) + : (!g_nnReadyForUse ? "ML not ready" : StringFormat("ML WP=%.0f%% < 55%%", g_nnTP1WinProb * 100)); + if(mlConfirms) { cascade.totalConfirmed++; cascade.totalPoints += mlPts; strengthSum += mlStr; } + } + cascade.avgStrength = (cascade.totalConfirmed > 0) ? (strengthSum / cascade.totalConfirmed) : 0; + // -- Determine minimum required confirmations -- + // minRequired: g_gates is single source (set by ComputeActiveGates from pair table) + int minRequired = g_gates.computed ? g_gates.minConfirmations : 2; + if(!g_gates.computed && (g_regimeData.regime == REGIME_VOLATILE || g_regimeData.regime == REGIME_CHOPPY)) + minRequired = 3; + // -- 13. LIQUIDITY SWEEP CONFIRMATION (0-15 pts) -- * FIX-A + // ICT: price sweeps liquidity before reversing. Swept SSL near BUY zone = institutional demand + // absorbed sell orders. Swept BSL near SELL zone = supply absorbed buy orders. + // * FIX#322b re-enabled FVG for H4 — LIQ sweep now adds on top of FVG as 4th confirmation. + // Recency: sweep within last 8 bars. Proximity: within 2×ATR of current price. + { + cascade.confirmations[CONF_LIQ_SWEEP].source = CONF_LIQ_SWEEP; + cascade.totalChecked++; + bool _lsFound = false; + double _lsBestStr = 0; + int _lsMaxSecs = 8 * PeriodSeconds(_Period); + double _lsProx = g_cachedATR * 2.0; + for(int _li = ArraySize(LIQ_Array) - 1; _li >= 0; _li--) + { + if(!LIQ_Array[_li].isValid || !LIQ_Array[_li].swept) continue; + bool _dirOK = isBullish ? (!LIQ_Array[_li].isBSL) : (LIQ_Array[_li].isBSL); + if(!_dirOK) continue; + if(MathAbs(LIQ_Array[_li].price - current_price) > _lsProx) continue; + if(LIQ_Array[_li].sweepTime <= 0) continue; + if((int)(TimeCurrent() - LIQ_Array[_li].sweepTime) > _lsMaxSecs) continue; + double _ageFrac = 1.0 - (double)(TimeCurrent() - LIQ_Array[_li].sweepTime) / _lsMaxSecs; + double _str = MathMax(0.5, _ageFrac) * MathMin(1.0, LIQ_Array[_li].touches * 0.3 + 0.3); + if(_str > _lsBestStr) { _lsBestStr = _str; _lsFound = true; } + } + int _lsPts = _lsFound ? (int)(15.0 * _lsBestStr) : 0; + cascade.confirmations[CONF_LIQ_SWEEP].confirmed = _lsFound; + cascade.confirmations[CONF_LIQ_SWEEP].strength = _lsBestStr; + cascade.confirmations[CONF_LIQ_SWEEP].points = _lsPts; + cascade.confirmations[CONF_LIQ_SWEEP].detail = _lsFound + ? StringFormat("LIQ sweep (str=%.2f pts=%d)", _lsBestStr, _lsPts) + : "No recent liquidity sweep nearby"; + if(_lsFound) { cascade.totalConfirmed++; cascade.totalPoints += _lsPts; strengthSum += _lsBestStr; } + } + cascade.requiredMin = minRequired; + cascade.passesMinimum = (cascade.totalConfirmed >= minRequired); + cascade.summary = StringFormat("%d/%d confirmed (%d pts, avg %.0f%%)", + cascade.totalConfirmed, cascade.totalChecked, + cascade.totalPoints, cascade.avgStrength * 100); + return cascade; +} +//+------------------------------------------------------------------+ +//| * Add a Candidate Signal to the ranking pool | +//+------------------------------------------------------------------+ +void AddCandidate(bool isBullish, string type, ENUM_ENTRY_TECHNIQUE tech, + int sourceIdx, double entryPrice, double slPrice, + double tp1Price, double tp2Price, double tp3Price, + int baseScore) +{ + if(g_candidateCount >= 20) return; + // * v7.8 FIX: SL COOLDOWN enforced for ALL techniques here + // Previously g_blockSell/BuyFromCooldown were set but never checked. + // Fix: check them at the entry point of AddCandidate so no technique bypasses cooldown. + if(!isBullish && g_blockSellFromCooldown) return; + if(isBullish && g_blockBuyFromCooldown) return; + // * v7.8 FIX: UNIVERSAL MTF ALIGNMENT -- block all techniques trading against strong HTF + // TC already has this check inline; now applying to FVG, BREAKER, OB, OTE, etc. + // Counter-trend is allowed ONLY if Regime_AllowCounterTrend = true + if(MTF_Enabled && !Regime_AllowCounterTrend) + { + // FIX#506: use raw g_mtfAnalysis.overallDirection (no more MTF override). + // D1 macro direction is handled by ctx.allowBuy/allowSell (FIX#506 hard gate). + // Here we add a D1 exception: when D1 confirms a direction, the MTF gate + // does NOT block trades in that direction even if H1/H4 MTF disagrees + // (= pullback scenario: D1=BEAR + MTF=BULL → SELL is allowed despite MTF=BULL). + bool mtfStrongBull = (g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH); + bool mtfBull = (g_mtfAnalysis.overallDirection == MTF_BULLISH || mtfStrongBull); + bool mtfStrongBear = (g_mtfAnalysis.overallDirection == MTF_STRONG_BEARISH); + bool mtfBear = (g_mtfAnalysis.overallDirection == MTF_BEARISH || mtfStrongBear); + bool mtfNeutral = (g_mtfAnalysis.overallDirection == MTF_NEUTRAL); // * v8.08 + // D1 exception flags — when D1 confirms a direction, exempt its trades from MTF gate + bool d1Bear = (g_d1CHoCH_Valid && g_d1CHoCH_Bear); // D1 downtrend confirmed + bool d1Bull = (g_d1CHoCH_Valid && g_d1CHoCH_Bull); // D1 uptrend confirmed + // * v9.04 FIX#6: CONSECUTIVE LOSS RELAXATION + // After 3+ consecutive losses, the MTF/Structure lock is likely WRONG about direction. + // Evidence: 13 consecutive SL hits (T20-T32) all BUY in bearish market because + // H4 MTF was still BULLISH + M15 Structure still BULLISH -> blocked all SELLs. + // Fix: After 3 consecutive losses, allow counter-trend with reduced score (not full block). + // After 5+ losses, disable MTF block entirely -- trust is lost. + // * v9.12 FIX#24 CLEANUP: Removed mtfRelaxed/mtfDisabled dead code. + // v9.11 set both to false permanently, making all else/else-if branches unreachable. + // Now simplified to the always-active path: MTF filter is ALWAYS enforced. + // After losses, only score penalty applies (see FIX#24 penalty below). + // MTF direction block -- ALWAYS enforced (no relaxation/disable) + // FIX#506 D1 exception: D1=BEAR exempts SELL from MTF=BULL block (pullback SELL) + // D1=BULL exempts BUY from MTF=BEAR block (pullback BUY) + if(!isBullish && mtfBull && !d1Bear) + { + if(g_verboseLog) + Print("* v9.12 MTF BLOCK [", type, "]: SELL rejected -- MTF=", + mtfStrongBull ? "STRONG_BULL" : "BULL"); + return; + } + if(isBullish && mtfBear && !d1Bull) + { + if(g_verboseLog) + Print("* v9.12 MTF BLOCK [", type, "]: BUY rejected -- MTF=", + mtfStrongBear ? "STRONG_BEAR" : "BEAR"); + return; + } + // * v9.37 FIX#147: MTF<->Structure COHERENCE CHECK — demoted from hard block to score penalty. + // BUG: On GBPUSD M5 MTF and Structure disagree frequently (different ATR periods) → 0 trades. + // FIX: Conflict now applies -30 score penalty (logged) instead of unconditional return. + // Hard block retained ONLY when MTF signal is strong (score contribution > 60) AND structure + // is a confirmed opposite HH/LL — i.e. not just a minor swing disagreement. + bool mtfStructConflict = (mtfBull && !g_isBullishStructure) || (mtfBear && g_isBullishStructure); + if(mtfStructConflict) + { + // Apply score penalty — caller (EvaluateSmartEntry) will handle threshold rejection + g_ea_signal.score -= 30; + if(g_verboseLog) + Print("* v9.37 FIX#147 MTF<->STRUCT CONFLICT [", type, + "]: score -30 (was hard block) | MTFBull=", mtfBull, + " Structure=", g_isBullishStructure ? "BULL" : "BEAR"); + } + // * v8.08 BUG#3 FIX: MTF=NEUTRAL conservative mode + if(mtfNeutral) + { + // Don't block, but mark that higher score is needed -- checked in EvaluateSmartEntry + } + } + + // ── FIX#502: SCENARIO GATE — only allow techniques matching current scenario ── + // g_scenarioProfile.allow* is set by DeriveScenarioProfile() each bar. + // This prevents e.g. TC entries in a ranging market, or OTE in a breakout. + // Gate only fires when scenario is valid (isValid=true, already checked above). + if(g_scenarioProfile.isValid) + { + bool allowed = true; + switch(tech) + { + case TECH_TREND_CONT: allowed = g_scenarioProfile.allowTC; break; + case TECH_FVG: allowed = g_scenarioProfile.allowFVG; break; + case TECH_OB: allowed = g_scenarioProfile.allowOB; break; + case TECH_BOS_RETEST: allowed = g_scenarioProfile.allowBOS; break; + case TECH_OTE: allowed = g_scenarioProfile.allowOTE; break; + case TECH_LIQ_SWEEP: allowed = g_scenarioProfile.allowLIQ; break; + case TECH_MEAN_REV: allowed = g_scenarioProfile.allowMEANREV; break; + // FIX#510: ICT-specific techniques mapped to scenario allow flags. + // Previously always allowed=true — bypassed scenario gate entirely. + case TECH_CRT: allowed = g_scenarioProfile.allowFVG || g_scenarioProfile.allowOB; break; // imbalance/zone concept + case TECH_JUDAS: allowed = g_scenarioProfile.allowLIQ; break; // liquidity sweep concept + case TECH_SILVER_BULLET: allowed = g_scenarioProfile.allowTC || g_scenarioProfile.allowFVG; break; // KZ+FVG + case TECH_BREAKER: allowed = g_scenarioProfile.allowOB; break; // failed order block + default: allowed = true; break; + } + if(!allowed) + { + if(g_verboseLog) + PrintFormat("[FIX#502] SCENARIO GATE [%s]: tech=%d blocked | scenario=%s", + type, (int)tech, g_scenarioProfile.description); + return; + } + } + CandidateSignal cand; + ZeroMemory(cand); + cand.valid = true; + cand.isBullish = isBullish; + cand.type = type; + cand.technique = tech; + cand.sourceIndex = sourceIdx; + cand.entryPrice = entryPrice; + cand.stopLoss = slPrice; + cand.tp1 = tp1Price; + cand.tp2 = tp2Price; + cand.tp3 = tp3Price; + cand.baseScore = baseScore; + // * v10.31 FIX#320/P3: REMOVE loss streak score penalty. + // OLD (FIX#24): streak≥3 → score-15, streak≥5 → score-25 → effective min_conf rises + // → EA stops entering for WEEKS after a CHOPPY period (Jan-Feb 2024 = 4 SLs → frozen) + // ROOT CAUSE: Score measures SETUP QUALITY (structure, OB, FVG, MTF). A losing streak + // means the MARKET was bad (choppy), not that the NEXT setup has lower quality. + // FIX: streak penalty applies ONLY to lot sizing (in CalculatePositionSize via g_workingLossStreakCut), + // NOT to score. A high-quality setup after 5 SLs still deserves to enter — just smaller lots. + // Note: g_workingLossStreakCut[3]=0.08 already handles the lot reduction for H4. + // * v7.5c FIX: DYNAMIC MINIMUM SL FLOOR -- replaces hardcoded per-instrument floors + // Problem: Hardcoded floors only covered Gold/Silver/JPY. Indices got minSL=0.003 = nothing! + // US500 M5: SL avg 8.3pts vs ATR=920pts -> SL = 0.9% of ATR -> stopped by noise (19% win rate) + // Solution: Dynamic minSL = MAX(ATR x 1.5, Spread x 5.0, hardcoded_floor) + // This auto-adapts to ANY instrument, ANY timeframe + { + double slDist = MathAbs(entryPrice - cand.stopLoss); + // Component 1: ATR-based minimum + // * v9.04 FIX#7: SL FLOOR was 2.0xATR for M15+ -> too aggressive for EURUSD/Majors + // * FIX#441: D1+ uses 0.35x ATR floor (zone noise floor, same as H4 FIX#173). + // D1 ATR=65p → ATR×1.3=84p floor was EXPANDING structural SL from 50-75p → 84p. + // Combined with Kelly 4.69% → $948 per SL hit. ATR×0.35=23p rarely fires on D1 + // (D1 structural SL from FVG zone is already 50-80p > 23p), so structure preserved. + double atrSLMult; + if(_Period >= PERIOD_D1) + atrSLMult = 0.35; // * FIX#441: D1+ zone noise floor (structural SL rarely needs expanding) + else if(_Period >= PERIOD_M15) + { + string sym_floor = _Symbol; + if(StringFind(sym_floor, "XAU") >= 0 || StringFind(sym_floor, "GOLD") >= 0 || + StringFind(sym_floor, "XAG") >= 0 || StringFind(sym_floor, "US500") >= 0 || + StringFind(sym_floor, "US100") >= 0 || StringFind(sym_floor, "NAS") >= 0 || + StringFind(sym_floor, "USTEC") >= 0 || StringFind(sym_floor, "US30") >= 0) + atrSLMult = 1.5; // Metals/Indices: volatile, keep 1.5x + else + atrSLMult = 1.3; // Forex pairs: structural SL is usually precise, 1.3x sufficient + } + else + atrSLMult = 1.2; // M5 scalping: tight floors + double atrMinSL = g_cachedATR * atrSLMult; + // Component 2: Spread-based minimum (5x spread to ensure SL > transaction cost) + double spreadPrice = (double)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * _Point; + double spreadMinSL = spreadPrice * 5.0; + // Component 3: Hardcoded instrument floors (absolute safety net) + double hardcodedMinSL = 0; + string sym = _Symbol; + // * v7.7 FIX: Timeframe-adaptive hardcoded floors (old fixed $35 blocked ALL M5 Gold trades!) + if(StringFind(sym, "XAU") >= 0 || StringFind(sym, "GOLD") >= 0) + { + if(_Period <= PERIOD_M5) hardcodedMinSL = 8.0; // M1-M5: $8 (~=2xATR on M5 Gold) + else if(_Period <= PERIOD_M15) hardcodedMinSL = 15.0; // M15: $15 + else if(_Period <= PERIOD_H1) hardcodedMinSL = 22.0; // H1: $22 + else if(_Period <= PERIOD_H4) hardcodedMinSL = 35.0; // H4: $35 + else hardcodedMinSL = 50.0; // D1+: $50 + } + else if(StringFind(sym, "XAG") >= 0 || StringFind(sym, "SILVER") >= 0) + { + if(_Period <= PERIOD_M5) hardcodedMinSL = 0.10; + else if(_Period <= PERIOD_H1) hardcodedMinSL = 0.30; + else hardcodedMinSL = 0.50; + } + else if(StringFind(sym, "JPY") >= 0) + hardcodedMinSL = 0.30; + else if(StringFind(sym, "US500") >= 0 || StringFind(sym, "SP500") >= 0) + { + if(_Period <= PERIOD_M5) hardcodedMinSL = 3.0; + else hardcodedMinSL = 5.0; + } + else if(StringFind(sym, "US100") >= 0 || StringFind(sym, "NAS") >= 0 || StringFind(sym, "USTEC") >= 0) + { + if(_Period <= PERIOD_M5) hardcodedMinSL = 12.0; // * v7.7: 20->12 for M5 (20pt = 1.7xATR, fine for H1 not M5) + else hardcodedMinSL = 20.0; + } + else if(StringFind(sym, "US30") >= 0) + hardcodedMinSL = 30.0; + else if(StringFind(sym, "DE40") >= 0 || StringFind(sym, "UK100") >= 0 || StringFind(sym, "JP225") >= 0) + hardcodedMinSL = 15.0; + else if(StringFind(sym, "OIL") >= 0 || StringFind(sym, "WTI") >= 0 || StringFind(sym, "BRENT") >= 0) + hardcodedMinSL = 0.30; + else + { + // * v9.02 FIX: Remove hardcoded 0.0030 (=30pips for EURUSD M15!). + // Old floor forced SL=30pips when ATRx2=16pips -> R:R destroyed. + // For standard forex pairs (EUR,GBP,AUD,CHF,CAD,NZD): use ATR-based only. + // hardcodedMinSL=0 means atrMinSL wins (= ATR x 2.0 = ~16pips for M15 EURUSD) + hardcodedMinSL = 0.0; + } + // Final: use the LARGEST of all three + double minSL_price = MathMax(atrMinSL, MathMax(spreadMinSL, hardcodedMinSL)); + // * v9.08 FIX#12: ABSOLUTE MINIMUM SL FLOOR for Forex pairs + // Problem: ATRx1.3 floor fails in low-vol sessions (Asian). ATR=1.8p -> floor=2.3p = noise! + // Evidence: 16/75 trades with SL < 6 pips (T19: 2.5p, T28: 3.4p). All stopped by noise. + // Fix: Absolute floor scaled by timeframe. + if(hardcodedMinSL == 0) // Standard Forex pairs (no hardcoded floor) + { + // * v9.09 FIX#18: Timeframe-aware absolute SL floor + // * v9.13 FIX#27: 7p -> 5p for M15. When ATR drops to 3.8-4.6p in quiet + // sessions, the 7p floor inflates SL from 5.2p to 7.0p. + // TP1=2.0x4.2p=8.4p -> R:R=8.4/7.0=1.20 < adjMinRR=1.38 -> ALL candidates + // blocked. Caused ZERO trades after Jan 20 in EURUSD backtest. + // 5p = 50x spread on EURUSD (still safe from noise). + // * v9.31 FIX#109: Extend absolute floor to M5. Previously _Period >= PERIOD_M15 + // excluded M5 entirely. In Asian session ATR=0.7p -> atrMinSL=0.84p -> floor=0.84p + // -> 1.6p SL accepted -> stopped immediately by spread noise. + // Fix: M5 gets 3p floor (below active-session ATR of 3-5p so no R:R damage). + double absMinSL = 0.00050; // M15 default: 5 pips (was 7 -- destroyed R:R) + if(_Period <= PERIOD_M5) absMinSL = 0.00030; // * v9.31 FIX#109: M5: 3p floor (was 0!) + else if(_Period >= PERIOD_H4) absMinSL = 0.00200; // H4: 20 pips minimum + else if(_Period >= PERIOD_H1) absMinSL = 0.00080; // H1: 8 pips minimum (was 12p, reduced for sl=1.20xATR calibration) + if(StringFind(_Symbol, "JPY") >= 0) + absMinSL *= 100.0; // JPY pairs: 3-digit pricing + if(minSL_price < absMinSL) + { + if(g_verboseLog) + Print("* v9.31 FIX#109 ABS SL FLOOR: ", type, " ATR floor=", DoubleToString(minSL_price*10000, 1), + "p -> absolute=", DoubleToString(absMinSL*10000, 1), "p (ATR too low in quiet session)"); + minSL_price = absMinSL; + } + } + // * v9.46 FIX#188: Guard against SL=0 before noise reject. + // ROOT CAUSE: Some FVG geometry (very tight gap, top≈bottom) returns SL=0 from structural calc. + // slDist=0 triggers "NOISE REJECT: FVG SL=$0.00 < noise threshold=$0.00" and skips valid trade. + // Fix: if SL ended up at/near entry (zero distance), fall back to ATR-based SL. + // Note: uses g_cachedATR (no atr param in AddCandidate scope). + { + double _slCheck = MathAbs(cand.stopLoss - entryPrice); + if(_slCheck < _Point * 5 || cand.stopLoss == 0) + { + double _atrSL = g_cachedATR * GetActiveSLMult(); + cand.stopLoss = isBullish ? entryPrice - _atrSL : entryPrice + _atrSL; + if(g_verboseLog) + PrintFormat("* FIX#188 SL=0 guard: %s ATR fallback SL=%.5f (g_cachedATR=%.5f)", type, cand.stopLoss, g_cachedATR); + } + } + // * v10.01 FIX#244: Recalculate slDist AFTER FIX#188 ATR fallback. + // ROOT CAUSE: slDist was computed before FIX#188 runs. If FIX#188 replaces SL=0 with + // ATR-based SL, slDist still holds 0 → NOISE REJECT fires on "SL=$0.00 < $0.00" and + // skips the trade — defeating FIX#188 entirely. 68 occurrences in backtest log. + // Fix: recompute slDist from the (possibly updated) cand.stopLoss before noise check. + slDist = MathAbs(cand.stopLoss - entryPrice); // * FIX#244: sync with FIX#188 fallback + // * LEVEL 3: NOISE REJECTION -- if structural SL < ATRx0.8, the structure is noise-level + // Don't widen SL (that changes trade thesis), just SKIP the trade entirely + double noiseThreshold = g_cachedATR * 0.8; + if(slDist < noiseThreshold && slDist > 0 && g_cachedATR > 0) + { + Print("* v7.5c NOISE REJECT: ", type, " SL=$", DoubleToString(slDist, 2), + " < noise threshold=$", DoubleToString(noiseThreshold, 2), + " (ATRx0.8) -> signal too tight, SKIPPED"); + cand.valid = false; + return; + } + if(slDist < minSL_price && minSL_price > 0) + { + double oldSL = slDist; + if(isBullish) + cand.stopLoss = entryPrice - minSL_price; + else + cand.stopLoss = entryPrice + minSL_price; + // * Scale TP proportionally to maintain R:R + double slExpansion = minSL_price / MathMax(oldSL, _Point); + if(slExpansion > 1.1) + { + double tp1Dist = MathAbs(cand.tp1 - entryPrice) * slExpansion; + double tp2Dist = MathAbs(cand.tp2 - entryPrice) * slExpansion; + double tp3Dist = MathAbs(cand.tp3 - entryPrice) * slExpansion; + if(isBullish) + { + cand.tp1 = entryPrice + tp1Dist; + cand.tp2 = entryPrice + tp2Dist; + cand.tp3 = entryPrice + tp3Dist; + } + else + { + cand.tp1 = entryPrice - tp1Dist; + cand.tp2 = entryPrice - tp2Dist; + cand.tp3 = entryPrice - tp3Dist; + } + } + // * v8.09 FIX: Use 5 decimals for forex (price-based SL values like 0.00300 showed as /bin/sh.00) + int slDecimals = (hardcodedMinSL < 1.0) ? 5 : 2; + Print("* v7.5c SL FLOOR: ", type, " SL=", DoubleToString(oldSL, slDecimals), + " -> ", DoubleToString(minSL_price, slDecimals), + " (ATRx", DoubleToString(atrSLMult,1), "=", DoubleToString(atrMinSL, slDecimals), + " | Sprdx5=", DoubleToString(spreadMinSL, slDecimals), + " | Floor=", DoubleToString(hardcodedMinSL, slDecimals), ")"); + } + } + // * v7.5c: DYNAMIC MINIMUM TP1 FLOOR (matches SL floor logic) + { + double tp1AbsDist = MathAbs(cand.tp1 - entryPrice); + double slDistFinal = MathAbs(cand.stopLoss - entryPrice); + // TP1 must be at least MinRR x SL distance + double minTP_byRR = slDistFinal * EA_MinRR; + // TP1 must be at least 2x ATR + double minTP_byATR = g_cachedATR * 2.0; + double minTP_price = MathMax(minTP_byRR, minTP_byATR); + if(tp1AbsDist < minTP_price && minTP_price > 0) + { + if(isBullish) + { + cand.tp1 = entryPrice + minTP_price; + cand.tp2 = entryPrice + minTP_price * 1.25; + cand.tp3 = entryPrice + minTP_price * 1.55; + } + else + { + cand.tp1 = entryPrice - minTP_price; + cand.tp2 = entryPrice - minTP_price * 1.25; + cand.tp3 = entryPrice - minTP_price * 1.55; + } + } + } + // R:R calculation -- * v7.5c: Use CORRECTED SL/TP (was using original pre-floor values!) + double spreadCost = (double)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * _Point; + double risk = MathAbs(entryPrice - cand.stopLoss); + double reward = MathMax(0, MathAbs(cand.tp1 - entryPrice) - spreadCost); + cand.rr = (risk > 0) ? reward / risk : 0; + // Get technique priority + cand.priority = g_techState.priorities[tech].priority; + // Regime bonus from technique selector + cand.regimeBonus = (int)g_techState.priorities[tech].confidenceBoost; + // * v8.08 BUG#6 FIX: Regime stability requirement + // On M15, regime changes every few bars -> TC gets regimeBonus=18 on a freshly-changed regime. + // Evidence: Trade #4 (CHOPPY->WEAK_DOWNTREND) and Trade #11 (CHOPPY) both lost with full boost. + // Fix: If regime changed within last 3 bars, halve the regimeBonus. If within last 6, reduce by 30%. + // This prevents inflated confidence on unstable regimes without blocking trades entirely. + if(cand.regimeBonus > 0 && g_regimeData.lastChange > 0) + { + int barsSinceRegimeChange = (int)((TimeCurrent() - g_regimeData.lastChange) / PeriodSeconds()); + if(barsSinceRegimeChange < 3) + { + cand.regimeBonus = (int)(cand.regimeBonus * 0.40); // 60% reduction for very fresh regime + if(g_verboseLog) + Print("* v8.08 REGIME UNSTABLE: ", barsSinceRegimeChange, " bars since change -> regimeBonus reduced to ", cand.regimeBonus); + } + else if(barsSinceRegimeChange < 6) + { + cand.regimeBonus = (int)(cand.regimeBonus * 0.70); // 30% reduction for semi-fresh + } + } + // * v7.2: Complete pair-technique affinity map + cand.pairBonus = 0; + if(g_gates.computed) + { + string cat = g_autoOptParams.pair_category; + // Metal (Gold/Silver): sweep reversals + candle patterns dominate + if(cat == "Metal" && + (tech == TECH_TBS || tech == TECH_LIQ_SWEEP || tech == TECH_JUDAS)) + cand.pairBonus = 8; + else if(cat == "Metal" && tech == TECH_CRT) + cand.pairBonus = 6; + // Major (EUR/USD, GBP/USD etc): clean FVG/OB + precise fibs + else if(cat == "Major" && (tech == TECH_FVG || tech == TECH_OB)) + cand.pairBonus = 6; + else if(cat == "Major" && tech == TECH_OTE) + cand.pairBonus = 5; + else if(cat == "Major" && tech == TECH_BOS_RETEST) + cand.pairBonus = 4; + // Cross (EUR/GBP, AUD/NZD etc): structural + breaker plays + else if(cat == "Cross" && (tech == TECH_BREAKER || tech == TECH_BOS_RETEST)) + cand.pairBonus = 6; + else if(cat == "Cross" && tech == TECH_OB) + cand.pairBonus = 4; + // VolatileCross (GBP/JPY etc): sweep momentum + Judas + else if(cat == "VolatileCross" && + (tech == TECH_TBS || tech == TECH_LIQ_SWEEP)) + cand.pairBonus = 7; + else if(cat == "VolatileCross" && tech == TECH_JUDAS) + cand.pairBonus = 6; + // Index (US30, NAS100 etc): session-based entries + else if(cat == "Index" && + (tech == TECH_FVG || tech == TECH_SILVER_BULLET || tech == TECH_JUDAS)) + cand.pairBonus = 5; + else if(cat == "Index" && tech == TECH_LIQ_SWEEP) + cand.pairBonus = 4; + // * v7.8 Trend Continuation: best on trending instruments + // * v8.08 BUG#1 FIX: Index pairBonus 8->3. TC dominated 68% of trades with 23% WR + // while FVG (50% WR) was systematically excluded by inflated TC scores. + // TC base=25 + structure=5 + killzone=5 + regime=18 + pairBonus=8 = 61 before confirmations + // FVG base=15-23 + regime=8 + pairBonus=5 = 28-36 -> TC ALWAYS won. Now balanced. + if(tech == TECH_TREND_CONT) + { + if(cat == "Index") cand.pairBonus += 3; // * v8.08: was 8, reduced -- TC over-dominated + else if(cat == "Metal") cand.pairBonus += 5; // * v8.08: was 7, slight reduction + else if(cat == "Major") cand.pairBonus += 4; // * v8.08: was 5 + else if(cat == "Cross" || cat == "VolatileCross") cand.pairBonus += 3; + } + } + // Run confirmation cascade + // * v6.35: Pass source technique to prevent self-confirmation + cand.cascade = RunConfirmationCascade(isBullish, tech); + cand.confirmationScore = cand.cascade.totalPoints; + // * FIX#362: Eliminate double regime weighting for H4+ TRENDING. + // ROOT CAUSE: cand.regimeBonus (from g_techState.priorities[tech].confidenceBoost = 18-20 pts for + // TC in TRENDING) + CONF_REGIME in cascade (+10 pts for REGIME_TRENDING) = SAME signal counted TWICE. + // DATA PROOF: 5-conf + Regime=TRENDING → WR=21%, -$713 (39 trades). TRENDING regime means price + // already moved → lagging entry. Double reward for this = systematically entering exhausted moves. + // FIX: On H4+, if CONF_REGIME is confirmed (= TRENDING regime is driving regimeBonus), cap + // regimeBonus at max(0, regimeBonus - cascade.confirmations[CONF_REGIME].points). + // This keeps regime bonus from technique priority (signal strength) but removes the cascade double-count. + // Lower TFs (M15/H1): regime changes faster, double-counting is less persistent — keep unchanged. + if(_Period >= PERIOD_H4 && cand.regimeBonus > 0 && + cand.cascade.confirmations[CONF_REGIME].confirmed) + { + int regimeCascadePts = cand.cascade.confirmations[CONF_REGIME].points; + int correctedBonus = MathMax(0, cand.regimeBonus - regimeCascadePts); + if(g_verboseLog) + PrintFormat("[FIX#362] H4 double-regime dedup: regimeBonus %d→%d (cascade CONF_REGIME=%d pts removed)", + cand.regimeBonus, correctedBonus, regimeCascadePts); + cand.regimeBonus = correctedBonus; + } + // Total score = base + confirmations + regime + pair + cand.totalScore = cand.baseScore + cand.confirmationScore + cand.regimeBonus + cand.pairBonus; + // * v9.04 FIX#8: TP R:R CAP -- Prevent unreachable TPs + // Evidence: FVG/OB with structural SL=9p but ATR-based TP1=93p -> R:R=10.2 -> NEVER hits + // T18 (FVG R:R=5.1), T26 (OB R:R=10.2), T32 (TC R:R=5.8) -> all SL hits + // Fix: Cap TP distances relative to SL distance. Max R:R = 3.5 for TP1, 5.0 for TP2, 6.5 for TP3 + { + double slDist = MathAbs(cand.entryPrice - cand.stopLoss); + if(slDist > 0) + { + double tp1Dist = MathAbs(cand.tp1 - cand.entryPrice); + double tp2Dist = MathAbs(cand.tp2 - cand.entryPrice); + double tp3Dist = MathAbs(cand.tp3 - cand.entryPrice); + double maxTP1 = slDist * 3.5; // Max 3.5R for TP1 + double maxTP2 = slDist * 5.0; // Max 5.0R for TP2 + double maxTP3 = slDist * 6.5; // Max 6.5R for TP3 + if(tp1Dist > maxTP1) + { + if(g_verboseLog) + Print("* v9.04 TP1 R:R CAP [", cand.type, "]: R:R=", DoubleToString(tp1Dist/slDist, 1), " -> capped to 3.5R"); + cand.tp1 = cand.isBullish ? cand.entryPrice + maxTP1 : cand.entryPrice - maxTP1; + } + if(tp2Dist > maxTP2) + { + cand.tp2 = cand.isBullish ? cand.entryPrice + maxTP2 : cand.entryPrice - maxTP2; + } + if(tp3Dist > maxTP3) + { + cand.tp3 = cand.isBullish ? cand.entryPrice + maxTP3 : cand.entryPrice - maxTP3; + } + // Ensure TP ordering: TP1 < TP2 < TP3 + double d1 = MathAbs(cand.tp1 - cand.entryPrice); + double d2 = MathAbs(cand.tp2 - cand.entryPrice); + double d3 = MathAbs(cand.tp3 - cand.entryPrice); + if(d2 <= d1) cand.tp2 = cand.isBullish ? cand.entryPrice + d1 * 1.3 : cand.entryPrice - d1 * 1.3; + if(d3 <= d2) cand.tp3 = cand.isBullish ? cand.entryPrice + d2 * 1.3 : cand.entryPrice - d2 * 1.3; + } + } + // * v9.03 FIX#5: Override TP3 with technique-specific structural target + if(g_cachedATR > 0) + cand.tp3 = FindStructuralTP3(cand, g_cachedATR); + // * v9.44 FIX#179: Override TP1 and TP2 with real market structure targets. + // Previously ALL techniques left TP1/TP2 as fixed ATR multiples. + // Now TP1 targets nearest BSL/SSL/swing/OB edge, TP2 targets next beyond TP1. + // Fallback: if no structural target found, ATR-based TP is kept unchanged. + if(g_cachedATR > 0) + { + double slDist = MathAbs(cand.entryPrice - cand.stopLoss); + if(slDist > 0) + { + double minTP1Dist = slDist * MathMax(0.75, GetActiveMinRR() * 0.75); + double structTP1 = FindNearestStructuralTP(cand, g_cachedATR, minTP1Dist); + if(structTP1 > 0) + { + double structTP1Dist = MathAbs(structTP1 - cand.entryPrice); + double currentTP1Dist = MathAbs(cand.tp1 - cand.entryPrice); + if(MathAbs(structTP1Dist - currentTP1Dist) > g_cachedATR * 0.10) + { + cand.tp1 = structTP1; + double minTP2Dist = structTP1Dist * 1.20; + double structTP2 = FindNearestStructuralTP(cand, g_cachedATR, minTP2Dist); + if(structTP2 > 0) + { + double structTP2Dist = MathAbs(structTP2 - cand.entryPrice); + double currentTP2Dist = MathAbs(cand.tp2 - cand.entryPrice); + if(MathAbs(structTP2Dist - currentTP2Dist) > g_cachedATR * 0.10) + cand.tp2 = structTP2; + } + } + } + // R:R safety floor: TP1 must be at least minRR x SL from entry + double tp1Dist = MathAbs(cand.tp1 - cand.entryPrice); + double minTP1Floor = slDist * GetActiveMinRR(); + if(tp1Dist < minTP1Floor) + cand.tp1 = cand.isBullish ? cand.entryPrice + minTP1Floor + : cand.entryPrice - minTP1Floor; + // Ensure TP2 > TP1 + double newTP1Dist = MathAbs(cand.tp1 - cand.entryPrice); + double newTP2Dist = MathAbs(cand.tp2 - cand.entryPrice); + if(newTP2Dist <= newTP1Dist * 1.10) + cand.tp2 = cand.isBullish ? cand.entryPrice + newTP1Dist * 1.35 + : cand.entryPrice - newTP1Dist * 1.35; + } + } + // * v9.41 FIX#177: Copy pending zone data into candidate (was self-assignment bug — fixed) + // Each technique sets g_pendingZone* before AddCandidate(). Copy into cand now. + cand.zoneTop = g_pendingZoneTop; + cand.zoneBottom = g_pendingZoneBottom; + cand.zoneType = g_pendingZoneType; + // Reset pending zone so next candidate starts clean + g_pendingZoneTop = 0; + g_pendingZoneBottom = 0; + g_pendingZoneType = ""; + // ── FIX#502: Stamp scenario profile onto candidate ──────────────── + // Candidate carries scenario context so OpenTrade can compute + // scenario-aware SL/TP without re-running detection. + cand.scenario = g_scenarioProfile.scenario; + cand.entryStyle = g_scenarioProfile.entryStyle; + cand.slMethod = g_scenarioProfile.slMethod; + cand.tpMethod = g_scenarioProfile.tpMethod; + g_candidates[g_candidateCount] = cand; + g_candidateCount++; +} +//+------------------------------------------------------------------+ +//| * v9.44 FIX#179: FindNearestStructuralTP | +//| Finds nearest real market target (LIQ pool, swing H/L, OB edge, | +//| FVG CE) at least minDist from entry. | +//| Used for TP1 (minDist = slDist x minRR) and TP2 (x1.20 beyond). | +//| Returns 0 if no valid target -> caller keeps ATR-based TP. | +//+------------------------------------------------------------------+ +double FindNearestStructuralTP(const CandidateSignal &cand, double atr, double minDist) +{ + double entry = cand.entryPrice; + bool isBull = cand.isBullish; + double best = 0; + double bestD = DBL_MAX; + + // 1. Unswept Liquidity Pools (BSL for BUY, SSL for SELL) + for(int i = 0; i < ArraySize(LIQ_Array); i++) + { + if(!LIQ_Array[i].isValid || LIQ_Array[i].swept) continue; + double lp = LIQ_Array[i].price; + double d = isBull ? (lp - entry) : (entry - lp); + if(d < minDist) continue; + bool inDir = isBull ? LIQ_Array[i].isBSL : !LIQ_Array[i].isBSL; + if(inDir && d < bestD) { bestD = d; best = lp; } + } + + // 2. Unbroken Swing Highs/Lows (recent 80 bars) + int sz = ArraySize(STRUCT_Array); + for(int i = sz - 1; i >= MathMax(0, sz - 80); i--) + { + if(!STRUCT_Array[i].isValid || STRUCT_Array[i].broken) continue; + if(STRUCT_Array[i].age > 100) continue; + double sp = STRUCT_Array[i].price; + double d = isBull ? (sp - entry) : (entry - sp); + if(d < minDist) continue; + bool inDir = isBull ? STRUCT_Array[i].isHigh : !STRUCT_Array[i].isHigh; + if(inDir && d < bestD) { bestD = d; best = sp; } + } + + // 3. Opposing Order Block Edge (strong OBs, strength >= 0.70) + for(int i = 0; i < ArraySize(OB_Array); i++) + { + if(!OB_Array[i].active || OB_Array[i].mitigated) continue; + if(OB_Array[i].strength < 0.70) continue; + double obEdge = 0; + if( isBull && !OB_Array[i].isBullish) obEdge = OB_Array[i].bottom - atr * 0.10; + if(!isBull && OB_Array[i].isBullish) obEdge = OB_Array[i].top + atr * 0.10; + if(obEdge == 0) continue; + double d = isBull ? (obEdge - entry) : (entry - obEdge); + if(d < minDist) continue; + if(d < bestD) { bestD = d; best = obEdge; } + } + + // 4. Active Opposing FVG Consequent Encroachment (CE), HIGH quality only + for(int i = 0; i < ArraySize(FVG_Array); i++) + { + if(FVG_Array[i].status != FVG_STATUS_ACTIVE) continue; + if(FVG_Array[i].quality < FVG_QUALITY_HIGH) continue; + double ce = (FVG_Array[i].top + FVG_Array[i].bottom) / 2.0; + bool oppDir = isBull ? !FVG_Array[i].isBullish : FVG_Array[i].isBullish; + if(!oppDir) continue; + double d = isBull ? (ce - entry) : (entry - ce); + if(d < minDist) continue; + if(d < bestD) { bestD = d; best = ce; } + } + + return best; // 0 = no structural target found +} +//+------------------------------------------------------------------+ +//| * v9.03 FIX#5: STRUCTURAL TP3 -- Technique-specific runner target | +//| Each ICT technique has a natural structural target for TP3: | +//| FVG/OB/BREAKER -> next unswept liquidity pool | +//| OTE -> Fib extension 1.618 of the swing | +//| BOS -> next swing high/low beyond broken structure | +//| CRT -> CRT extension projection (already calculated) | +//| TBS -> opposing liquidity pool after sweep | +//| Falls back to ATR-based TP3 if no valid structural target found. | +//+------------------------------------------------------------------+ +double FindStructuralTP3(const CandidateSignal &cand, double atr) +{ + double entry = cand.entryPrice; + bool isBull = cand.isBullish; + double atrTP3 = cand.tp3; // Current ATR-based TP3 (fallback) + double tp2Level = cand.tp2; // TP3 must be beyond TP2 + double bestTarget = 0; + string targetSource = ""; + // -- 1. LIQUIDITY POOLS (universal target -- works for ALL techniques) -- + // Unswept liquidity is the natural magnet for price movement + double nearestLiq = 0; + double nearestLiqDist = 99999; + for(int i = 0; i < ArraySize(LIQ_Array); i++) + { + if(!LIQ_Array[i].isValid || LIQ_Array[i].swept) continue; + double liqPrice = LIQ_Array[i].price; + // For BUY: BSL (buy-side liquidity) above entry = target + if(isBull && LIQ_Array[i].isBSL && liqPrice > tp2Level) + { + double dist = liqPrice - entry; + if(dist < nearestLiqDist && dist > 0) + { + nearestLiqDist = dist; + nearestLiq = liqPrice; + } + } + // For SELL: SSL (sell-side liquidity) below entry = target + else if(!isBull && !LIQ_Array[i].isBSL && liqPrice < tp2Level) + { + double dist = entry - liqPrice; + if(dist < nearestLiqDist && dist > 0) + { + nearestLiqDist = dist; + nearestLiq = liqPrice; + } + } + } + if(nearestLiq > 0) + { + bestTarget = nearestLiq; + targetSource = "LIQ_POOL"; + } + // -- 2. TECHNIQUE-SPECIFIC TARGETS -- + switch(cand.technique) + { + case TECH_OTE: + { + // OTE natural target = Fib extension 1.618 of the swing + if(cand.sourceIndex >= 0 && cand.sourceIndex < ArraySize(OTE_Array)) + { + double swingH = OTE_Array[cand.sourceIndex].swingHigh; + double swingL = OTE_Array[cand.sourceIndex].swingLow; + double swingRange = swingH - swingL; + if(swingRange > 0) + { + double fibTarget = 0; + if(isBull) + fibTarget = swingH + swingRange * 0.618; // 1.618 extension + else + fibTarget = swingL - swingRange * 0.618; // 1.618 extension + // Use fib target if it's beyond TP2 and within reason + bool fibValid = isBull ? (fibTarget > tp2Level) : (fibTarget < tp2Level); + double fibDist = MathAbs(fibTarget - entry); + if(fibValid && fibDist < atr * 6.0) + { + // Prefer fib over liquidity if fib is closer (more precise) + if(bestTarget == 0 || fibDist < MathAbs(bestTarget - entry)) + { + bestTarget = fibTarget; + targetSource = "FIB_1.618"; + } + } + } + } + break; + } + case TECH_CRT: + { + // CRT already calculates extension projections + if(cand.sourceIndex >= 0 && cand.sourceIndex < ArraySize(g_crtSetups)) + { + double crtExt = isBull ? g_crtSetups[cand.sourceIndex].extensionUp + : g_crtSetups[cand.sourceIndex].extensionDown; + bool crtValid = isBull ? (crtExt > tp2Level) : (crtExt < tp2Level); + if(crtExt > 0 && crtValid) + { + bestTarget = crtExt; + targetSource = "CRT_EXT"; + } + } + break; + } + case TECH_BOS_RETEST: + { + // BOS target = next swing high (bull) or swing low (bear) beyond broken level + double swingTarget = 0; + if(isBull && g_swingHighCount > 0) + { + // Find next swing high above TP2 + for(int s = 0; s < MathMin(g_swingHighCount, ArraySize(g_swingHighs)); s++) + { + if(g_swingHighs[s] > tp2Level && (swingTarget == 0 || g_swingHighs[s] < swingTarget)) + swingTarget = g_swingHighs[s]; + } + } + else if(!isBull && g_swingLowCount > 0) + { + for(int s = 0; s < MathMin(g_swingLowCount, ArraySize(g_swingLows)); s++) + { + if(g_swingLows[s] < tp2Level && (swingTarget == 0 || g_swingLows[s] > swingTarget)) + swingTarget = g_swingLows[s]; + } + } + if(swingTarget > 0) + { + double swDist = MathAbs(swingTarget - entry); + if(swDist < atr * 6.0) // Within reasonable range + { + if(bestTarget == 0 || swDist < MathAbs(bestTarget - entry)) + { + bestTarget = swingTarget; + targetSource = "SWING_LEVEL"; + } + } + } + break; + } + case TECH_FVG: + { + // FVG target = next opposing FVG CE (centre equilibrium) as secondary magnet + double oppFVG = 0; + for(int f = 0; f < ArraySize(FVG_Array); f++) + { + if(FVG_Array[f].status != FVG_STATUS_ACTIVE) continue; + if(FVG_Array[f].isBullish == isBull) continue; // Want OPPOSING FVG + double fvgCE = FVG_Array[f].ce; + bool valid = isBull ? (fvgCE > tp2Level) : (fvgCE < tp2Level); + if(valid) + { + double dist = MathAbs(fvgCE - entry); + if(dist < atr * 6.0 && (oppFVG == 0 || dist < MathAbs(oppFVG - entry))) + oppFVG = fvgCE; + } + } + // Use opposing FVG only if no closer liquidity pool + if(oppFVG > 0 && bestTarget == 0) + { + bestTarget = oppFVG; + targetSource = "OPP_FVG_CE"; + } + break; + } + case TECH_OB: + case TECH_BREAKER: + { + // OB/Breaker target = opposing OB edge or next liquidity + double oppOB = 0; + for(int ob = 0; ob < ArraySize(OB_Array); ob++) + { + if(!OB_Array[ob].active || OB_Array[ob].mitigated) continue; + if(OB_Array[ob].isBullish == isBull) continue; // Opposing + if(OB_Array[ob].strength < 0.6) continue; + double obEdge = isBull ? OB_Array[ob].bottom : OB_Array[ob].top; + bool valid = isBull ? (obEdge > tp2Level) : (obEdge < tp2Level); + if(valid) + { + double dist = MathAbs(obEdge - entry); + if(dist < atr * 6.0 && (oppOB == 0 || dist < MathAbs(oppOB - entry))) + oppOB = obEdge; + } + } + if(oppOB > 0 && bestTarget == 0) + { + bestTarget = oppOB; + targetSource = "OPP_OB"; + } + break; + } + case TECH_TBS: + { + // TBS target = opposing liquidity pool (after sweep, price targets the other side) + // Liquidity scan already handled above -- TBS prioritizes it + break; + } + default: + break; + } + // -- 3. VALIDATION -- + if(bestTarget > 0) + { + double targetDist = MathAbs(bestTarget - entry); + double tp2Dist = MathAbs(tp2Level - entry); + double slDist = MathAbs(entry - cand.stopLoss); + // TP3 must be: beyond TP2, at least 2.5R from entry, max 6xATR + bool beyondTP2 = isBull ? (bestTarget > tp2Level + g_pipValue) + : (bestTarget < tp2Level - g_pipValue); + bool minRR = (slDist > 0 && targetDist / slDist >= 2.5); + bool maxDist = (targetDist <= atr * 6.0); + if(beyondTP2 && minRR && maxDist) + { + if(g_verboseLog) + Print("* v9.03 STRUCTURAL TP3 [", cand.type, "]: ", targetSource, + " | ATR TP3=", DoubleToString(atrTP3, _Digits), + " -> Struct TP3=", DoubleToString(bestTarget, _Digits), + " | R:R=", DoubleToString(targetDist / slDist, 1)); + return bestTarget; + } + } + // No valid structural target -> keep ATR-based TP3 + return atrTP3; +} +//+------------------------------------------------------------------+ +//| * Build SL/TP for a candidate using pair-adapted values | +//+------------------------------------------------------------------+ +void BuildCandidateSLTP(CandidateSignal &cand, double atr) +{ + double slMult = EA_StopLossATR; + double tp1Mult = EA_TP1_ATR; + double tp2Mult = EA_TP2_ATR; + double tp3Mult = EA_TP3_ATR; + // FIX#164: Profile overrides ONLY when AutoOpt active; manual mode uses EA inputs + if(AutoOpt_Enabled && g_gates.computed) + { + slMult = g_autoOptParams.sl_atr_mult; + tp1Mult = g_autoOptParams.tp_atr_mult; + double _tp2R = 1.35, _tp3R = 1.75; + GetTP2TP3Ratios(g_autoOptParams.tf_category, g_autoOptParams.pair_category, _tp2R, _tp3R); + tp2Mult = g_autoOptParams.tp_atr_mult * _tp2R; + tp3Mult = g_autoOptParams.tp_atr_mult * _tp3R; + } + // * FIX v7.2: Apply TIMEFRAME adaptation (was disconnected!) + // * v9.09 FIX#14: SKIP when g_gates.computed -- AutoOpt Step2 already + // applied TF scaling to g_autoOptParams.tp_atr_mult/sl_atr_mult. + // Applying tfTPScale again caused DOUBLE TF SCALING: + // EURUSD H4: TP1=5.06 x 1.40 = 7.08, TP3=8.86 x 1.40 = 12.40 -> unreachable! + // M5 was invisible (x1.00) but H4 (x1.40) and D1 (x1.60) exploded. + double tfSLScale = 1.0; + double tfTPScale = 1.0; + if(!AutoOpt_Enabled || !g_gates.computed) // FIX#164: manual mode always gets TF scaling + { + switch(_Period) + { + case PERIOD_M1: tfSLScale = 0.70; tfTPScale = 0.65; break; + case PERIOD_M5: tfSLScale = 1.00; tfTPScale = 1.00; break; + case PERIOD_M15: tfSLScale = 1.00; tfTPScale = 1.00; break; + case PERIOD_M30: tfSLScale = 1.10; tfTPScale = 1.10; break; + case PERIOD_H1: tfSLScale = 1.20; tfTPScale = 1.25; break; + case PERIOD_H4: tfSLScale = 1.35; tfTPScale = 1.40; break; + case PERIOD_D1: tfSLScale = 1.50; tfTPScale = 1.60; break; + case PERIOD_W1: tfSLScale = 2.00; tfTPScale = 2.20; break; + default: tfSLScale = 1.00; tfTPScale = 1.00; break; + } + slMult *= tfSLScale; + tp1Mult *= tfTPScale; + tp2Mult *= tfTPScale; + tp3Mult *= tfTPScale; + } + // * v7.5b FIX: REMOVED old "M5 Gold FIX" that boosted TP1 +30%, TP2 +40%, TP3 +50% + // This was PUSHING TP2/TP3 further away (opposite of what we want). + // Backtest: 0/8 TP2/TP3 ever hit. Auto-opt already sets appropriate TP multipliers. + // Now TP2/TP3 are close to TP1 (x1.08, x1.15) per the bridge code. + // Session SL adjustment + UpdateSessionInfo(); + // * FIX: Regime-based SL AND TP adjustment + // v7.2 BUG: Only TP was reduced in ranging -> R:R collapsed below minRR + // Both SL AND TP must scale together to preserve R:R ratio + double regimeSLMult = 1.0; + double regimeTPMult = 1.0; + switch(g_regimeData.regime) + { + case REGIME_STRONG_TREND_UP: + case REGIME_STRONG_TREND_DOWN: + regimeSLMult = 1.10; regimeTPMult = 1.25; break; // Wide SL + extended TP + case REGIME_TRENDING: + case REGIME_TREND_UP: + case REGIME_TREND_DOWN: + regimeSLMult = 1.05; regimeTPMult = 1.15; break; // Slightly wider + case REGIME_WEAK_TREND_UP: + case REGIME_WEAK_TREND_DOWN: + regimeSLMult = 0.95; regimeTPMult = 0.95; break; // Both slightly tighter + case REGIME_RANGING: + case REGIME_RANGING_TIGHT: + // * M5 FIX: Gentle reduction for lower timeframes + if(_Period <= PERIOD_M5) + { + regimeSLMult = 0.95; regimeTPMult = 0.90; // M5: gentle (-5%/-10%) + } + else + { + regimeSLMult = 0.80; regimeTPMult = 0.80; // HTF: original (-20%) + } + break; + case REGIME_RANGING_WIDE: + regimeSLMult = 0.90; regimeTPMult = 0.85; break; // Wider range: less aggressive + case REGIME_VOLATILE: + case REGIME_CHOPPY: + regimeSLMult = 1.05; regimeTPMult = 0.95; break; // * FIX v7.3: Was 1.10/0.85 = R:R collapse. Now balanced. + case REGIME_BREAKOUT: + regimeSLMult = 0.90; regimeTPMult = 1.30; break; // Tight SL + extended TP for breakouts + default: // REGIME_UNKNOWN + regimeSLMult = 1.0; regimeTPMult = 1.0; break; + } + // * FIX #5: Session-based TP adjustment (not just SL) + double sessionTPMult = 1.0; + if(SessionSL_Enabled) + { + if(g_sessionSLMultiplier < 0.9) sessionTPMult = 0.85; // Asian: tighter TPs (was 0.75 = too aggressive) + else if(g_sessionSLMultiplier > 1.15) sessionTPMult = 1.10; // Overlap: wider TPs + } + double sl_dist = slMult * atr * g_sessionSLMultiplier * regimeSLMult; + // * v7.5c FIX: DYNAMIC MINIMUM SL FLOOR (same logic as AddCandidate) + // * v9.04 FIX#7: Pair-adaptive SL floor (matched to AddCandidate) + double atrFloorMult_b = 1.3; + if(StringFind(_Symbol, "XAU") >= 0 || StringFind(_Symbol, "GOLD") >= 0 || + StringFind(_Symbol, "XAG") >= 0 || StringFind(_Symbol, "US500") >= 0 || + StringFind(_Symbol, "US100") >= 0 || StringFind(_Symbol, "NAS") >= 0) + atrFloorMult_b = 1.5; + double atrMinSL_b = atr * atrFloorMult_b; + double spreadPrice_b = (double)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * _Point; + double spreadMinSL_b = spreadPrice_b * 5.0; // 5x spread minimum + double hardcodedMinSL_b = 0; + string sym_b = _Symbol; + // * v7.7 FIX: Timeframe-adaptive hardcoded floors (mirror of AddCandidate fix) + if(StringFind(sym_b, "XAU") >= 0 || StringFind(sym_b, "GOLD") >= 0) + { + if(_Period <= PERIOD_M5) hardcodedMinSL_b = 8.0; + else if(_Period <= PERIOD_M15) hardcodedMinSL_b = 15.0; + else if(_Period <= PERIOD_H1) hardcodedMinSL_b = 22.0; + else if(_Period <= PERIOD_H4) hardcodedMinSL_b = 35.0; + else hardcodedMinSL_b = 50.0; + } + else if(StringFind(sym_b, "XAG") >= 0 || StringFind(sym_b, "SILVER") >= 0) + { + if(_Period <= PERIOD_M5) hardcodedMinSL_b = 0.10; + else if(_Period <= PERIOD_H1) hardcodedMinSL_b = 0.30; + else hardcodedMinSL_b = 0.50; + } + else if(StringFind(sym_b, "JPY") >= 0) + hardcodedMinSL_b = 0.30; + else if(StringFind(sym_b, "US500") >= 0 || StringFind(sym_b, "SP500") >= 0) + { + if(_Period <= PERIOD_M5) hardcodedMinSL_b = 3.0; + else hardcodedMinSL_b = 5.0; + } + else if(StringFind(sym_b, "US100") >= 0 || StringFind(sym_b, "NAS") >= 0 || StringFind(sym_b, "USTEC") >= 0) + { + if(_Period <= PERIOD_M5) hardcodedMinSL_b = 12.0; + else hardcodedMinSL_b = 20.0; + } + else if(StringFind(sym_b, "US30") >= 0) + hardcodedMinSL_b = 30.0; + else if(StringFind(sym_b, "DE40") >= 0 || StringFind(sym_b, "UK100") >= 0 || StringFind(sym_b, "JP225") >= 0) + hardcodedMinSL_b = 15.0; + else if(StringFind(sym_b, "OIL") >= 0 || StringFind(sym_b, "WTI") >= 0 || StringFind(sym_b, "BRENT") >= 0) + hardcodedMinSL_b = 0.30; + else + hardcodedMinSL_b = 0.0; // * v9.02 FIX: was 0.0030 (=30pips!) -- forex uses ATR-based floor only (same as AddCandidate fix) + // * v9.31 FIX#102: Also apply pair+TF profile SL minimum (from settings table) + double profileMinSL_b = (g_workingSL_MinPips > 0) ? g_workingSL_MinPips * _Point * (StringFind(_Symbol,"JPY")>=0 ? 100.0 : 1.0) : 0.0; + // For indices/metals, sl_min_pips is already in "points" not pips -- detect and correct + if(g_workingSL_MinPips >= 10.0 && (StringFind(_Symbol,"XAU")>=0 || StringFind(_Symbol,"US")>=0 || StringFind(_Symbol,"BTC")>=0)) + profileMinSL_b = g_workingSL_MinPips * _Point; + double minSL_price_b = MathMax(atrMinSL_b, MathMax(spreadMinSL_b, MathMax(hardcodedMinSL_b, profileMinSL_b))); + // * v9.09 FIX#18: TF-aware ABSOLUTE MINIMUM SL FLOOR (matched to AddCandidate) + if(hardcodedMinSL_b == 0 && _Period >= PERIOD_M15) + { + double absMinSL_b = 0.00070; + if(_Period >= PERIOD_H4) absMinSL_b = 0.00200; + else if(_Period >= PERIOD_H1) absMinSL_b = 0.00120; + if(StringFind(_Symbol, "JPY") >= 0) absMinSL_b *= 100.0; + if(minSL_price_b < absMinSL_b) minSL_price_b = absMinSL_b; + } + if(sl_dist < minSL_price_b && minSL_price_b > 0) + { + if(g_verboseLog) + Print("* v7.5c BuildSLTP SL FLOOR: ATR-based=$", DoubleToString(sl_dist, 2), + " -> Min=$", DoubleToString(minSL_price_b, 2)); + sl_dist = minSL_price_b; + } + tp1Mult *= regimeTPMult * sessionTPMult; + tp2Mult *= regimeTPMult * sessionTPMult; + tp3Mult *= regimeTPMult * sessionTPMult; + // =============================================================== + // * v9.09 FIX#15d: HTF TP MULTIPLIER CLAMP (manual mode only) + // IMPORTANT: When g_gates.computed=true, tp1Mult was already + // set to g_autoOptParams.tp_atr_mult (from pair table via ApplyPairTFProfile) + // per pair AND per TF (e.g. GBPUSD H4=3.20, XAUUSD H4=3.50). + // Clamping it here would OVERWRITE correct pair data with wrong generic + // values -> REJECT all H4 candidates (root cause confirmed: 0 H4 trades). + // FIX#162: Only apply this clamp in MANUAL mode (no pair profile), + // as a safety net against unreachable ATR-multiples on HTF. + // =============================================================== + // FIX#164: HTF clamp applies in manual mode (AutoOpt=false) + if(!AutoOpt_Enabled || !g_gates.computed) + { + if(_Period >= PERIOD_D1) + { + tp1Mult = MathMin(tp1Mult, 6.0); + tp2Mult = MathMin(tp2Mult, 8.0); + tp3Mult = MathMin(tp3Mult, 11.0); + if(g_verboseLog) + PrintFormat("* FIX#162 [manual] D1 TP CLAMP: tp1=%.2f tp2=%.2f tp3=%.2f", tp1Mult, tp2Mult, tp3Mult); + } + else if(_Period >= PERIOD_H4) + { + tp1Mult = MathMin(tp1Mult, 4.5); + tp2Mult = MathMin(tp2Mult, 6.0); + tp3Mult = MathMin(tp3Mult, 8.0); + if(g_verboseLog) + PrintFormat("* FIX#162 [manual] H4 TP CLAMP: tp1=%.2f tp2=%.2f tp3=%.2f", tp1Mult, tp2Mult, tp3Mult); + } + else if(_Period >= PERIOD_H1) + { + tp1Mult = MathMin(tp1Mult, 2.0); + tp2Mult = MathMin(tp2Mult, 2.5); + tp3Mult = MathMin(tp3Mult, 3.0); + } + } + // When pair profile IS loaded: tp1Mult already = correct pair+TF value. + // No clamp needed -- pair profile is the authority. + cand.entryPrice = cand.isBullish ? + SymbolInfoDouble(_Symbol, SYMBOL_ASK) : + SymbolInfoDouble(_Symbol, SYMBOL_BID); + // * v9.36 FIX#142: TP PROPORTIONAL TO ACTUAL SL DISTANCE + // Problem: TP = entry ± tp1Mult × ATR, but SL = entry ± sl_dist (structure-based). + // If sl_dist > tp1Mult×ATR (e.g. SL=15.2p, TP=14.9p) → R:R < 1.0 → losing by design. + // Fix: ensure TP1 >= entry ± sl_dist × EA_MinRR (R:R-proportional floor). + // The ATR-based TP is still used as starting point (for structure), but we enforce + // the minimum R:R relative to the ACTUAL SL -- not relative to ATR. + double _rr_based_tp1 = sl_dist * MathMax(1.0, (double)EA_MinRR); // min 1.0R, prefer EA_MinRR + double _rr_based_tp2 = sl_dist * MathMax(1.5, (double)EA_MinRR * 1.3); + double _rr_based_tp3 = sl_dist * MathMax(2.0, (double)EA_MinRR * 1.8); + if(cand.isBullish) + { + cand.stopLoss = cand.entryPrice - sl_dist; + cand.tp1 = cand.entryPrice + MathMax(tp1Mult * atr, _rr_based_tp1); + cand.tp2 = cand.entryPrice + MathMax(tp2Mult * atr, _rr_based_tp2); + cand.tp3 = cand.entryPrice + MathMax(tp3Mult * atr, _rr_based_tp3); + } + else + { + cand.stopLoss = cand.entryPrice + sl_dist; + cand.tp1 = cand.entryPrice - MathMax(tp1Mult * atr, _rr_based_tp1); + cand.tp2 = cand.entryPrice - MathMax(tp2Mult * atr, _rr_based_tp2); + cand.tp3 = cand.entryPrice - MathMax(tp3Mult * atr, _rr_based_tp3); + } + // =============================================================== + // * v9.03 STRUCTURE-FIRST TP TARGETING + // ICT principle: TPs should target REAL structure levels where + // price is likely to react, not arbitrary ATR distances. + // ATR TPs above are FALLBACK -- structure overrides when available. + // =============================================================== + { + double slDist = MathAbs(cand.entryPrice - cand.stopLoss); + double minTP1Dist = slDist * EA_MinRR; // Minimum distance for TP1 + // * v9.09 FIX#15e: HTF -- cap minTP1Dist at ATR-based maximum + // On H4+, SL is wide (1.5x ATR) -> SL x minRR = 2.3x ATR -> too far! + // Price rarely moves >2x ATR directionally on HTF. + // Cap minTP1Dist at reasonable ATR multiple so structure-first + // can find CLOSER targets that the market actually reaches. + if(_Period >= PERIOD_D1) + minTP1Dist = MathMin(minTP1Dist, atr * 0.8); // D1: find targets from 0.8x ATR + else if(_Period >= PERIOD_H4) + minTP1Dist = MathMin(minTP1Dist, atr * 1.0); // H4: find targets from 1.0x ATR + else if(_Period >= PERIOD_H1) + minTP1Dist = MathMin(minTP1Dist, atr * 1.3); // H1: find targets from 1.3x ATR + // * v9.03 FIX#10: Pair category for structure TP adjustments + string _cat = g_autoOptParams.pair_category; + // Collect ALL structure targets in trade direction + double targets[]; // Price levels + int targetType[]; // 0=swing, 1=liquidity, 2=HTF_swing, 3=opposing_OB_edge + ArrayResize(targets, 0, 40); + ArrayResize(targetType, 0, 40); + int tgtCount = 0; + // -- 1. SWING HIGHS/LOWS (primary targets -- price gravitates to these) -- + int structSize = ArraySize(STRUCT_Array); + for(int i = structSize - 1; i >= MathMax(0, structSize - 100); i--) + { + if(!STRUCT_Array[i].isValid) continue; + if(STRUCT_Array[i].age > 120) continue; + double swPrice = STRUCT_Array[i].price; + double dist = MathAbs(swPrice - cand.entryPrice); + if(cand.isBullish && STRUCT_Array[i].isHigh && swPrice > cand.entryPrice + minTP1Dist * 0.8) + { + int ns = ArraySize(targets) + 1; + ArrayResize(targets, ns, 40); ArrayResize(targetType, ns, 40); + targets[ns-1] = swPrice; + targetType[ns-1] = 0; // swing + tgtCount++; + } + else if(!cand.isBullish && !STRUCT_Array[i].isHigh && swPrice < cand.entryPrice - minTP1Dist * 0.8) + { + int ns = ArraySize(targets) + 1; + ArrayResize(targets, ns, 40); ArrayResize(targetType, ns, 40); + targets[ns-1] = swPrice; + targetType[ns-1] = 0; // swing + tgtCount++; + } + } + // -- 2. UNSWEPT LIQUIDITY POOLS (magnets -- price drawn to sweep these) -- + for(int i = 0; i < ArraySize(LIQ_Array); i++) + { + if(!LIQ_Array[i].isValid || LIQ_Array[i].swept) continue; + double liqLevel = LIQ_Array[i].price; + if(cand.isBullish && LIQ_Array[i].isBSL && liqLevel > cand.entryPrice + minTP1Dist * 0.8) + { + int ns = ArraySize(targets) + 1; + ArrayResize(targets, ns, 40); ArrayResize(targetType, ns, 40); + targets[ns-1] = liqLevel; + targetType[ns-1] = 1; // liquidity + tgtCount++; + } + else if(!cand.isBullish && !LIQ_Array[i].isBSL && liqLevel < cand.entryPrice - minTP1Dist * 0.8) + { + int ns = ArraySize(targets) + 1; + ArrayResize(targets, ns, 40); ArrayResize(targetType, ns, 40); + targets[ns-1] = liqLevel; + targetType[ns-1] = 1; // liquidity + tgtCount++; + } + } + // -- 3. OPPOSING OB EDGES (partial TP before strong resistance/support) -- + for(int i = 0; i < ArraySize(OB_Array); i++) + { + if(!OB_Array[i].active || OB_Array[i].mitigated) continue; + if(OB_Array[i].strength < 0.75) continue; // Only strong OBs as targets + double obEdge = 0; + if(cand.isBullish && !OB_Array[i].isBullish) // Bearish OB above = resistance + obEdge = OB_Array[i].bottom; + else if(!cand.isBullish && OB_Array[i].isBullish) // Bullish OB below = support + obEdge = OB_Array[i].top; + if(obEdge > 0) + { + double dist = MathAbs(obEdge - cand.entryPrice); + if(dist >= minTP1Dist * 0.8) + { + int ns = ArraySize(targets) + 1; + ArrayResize(targets, ns, 40); ArrayResize(targetType, ns, 40); + // * v9.03 FIX#10: Pair-aware OB buffer -- noisy pairs need bigger buffer + double obBuf = atr * 0.15; + if(_cat == "Index" || _cat == "Metal" || _cat == "Energy") obBuf = atr * 0.25; + else if(_cat == "VolatileCross") obBuf = atr * 0.20; + targets[ns-1] = cand.isBullish ? (obEdge - obBuf) : (obEdge + obBuf); // Buffer before OB + targetType[ns-1] = 3; // OB edge + tgtCount++; + } + } + } + // -- 4. FVG CONSEQUENT ENCROACHMENT (CE) as intermediate targets -- + for(int i = 0; i < ArraySize(FVG_Array); i++) + { + if(FVG_Array[i].status != FVG_STATUS_ACTIVE) continue; + if(FVG_Array[i].quality < FVG_QUALITY_HIGH) continue; + // ALIGNED FVGs (same direction) = price targets for rebalancing + double fvgCE = (FVG_Array[i].top + FVG_Array[i].bottom) / 2.0; // Consequent Encroachment + bool isAligned = (cand.isBullish && !FVG_Array[i].isBullish && fvgCE > cand.entryPrice + minTP1Dist * 0.8) || + (!cand.isBullish && FVG_Array[i].isBullish && fvgCE < cand.entryPrice - minTP1Dist * 0.8); + if(isAligned) + { + int ns = ArraySize(targets) + 1; + ArrayResize(targets, ns, 40); ArrayResize(targetType, ns, 40); + targets[ns-1] = fvgCE; + targetType[ns-1] = 3; // FVG CE + tgtCount++; + } + } + // -- SORT targets by distance from entry -- + if(tgtCount >= 1) + { + // Sort ascending by distance + for(int i = 0; i < tgtCount - 1; i++) + { + for(int j = i + 1; j < tgtCount; j++) + { + double distI = MathAbs(targets[i] - cand.entryPrice); + double distJ = MathAbs(targets[j] - cand.entryPrice); + if(distJ < distI) + { + double tmpT = targets[i]; targets[i] = targets[j]; targets[j] = tmpT; + int tmpTy = targetType[i]; targetType[i] = targetType[j]; targetType[j] = tmpTy; + } + } + } + // -- ASSIGN TPs from structure targets -- + // * v9.03 FIX#10: Pair-category aware spacing -- noisy pairs need wider gaps + double spacingMult = 0.20; // Default for Forex Majors + if(_cat == "Index") spacingMult = 0.35; // Indices: high noise, wider spacing + else if(_cat == "Metal") spacingMult = 0.30; // Metals: moderate noise + else if(_cat == "Energy") spacingMult = 0.30; // Energy: moderate noise + else if(_cat == "VolatileCross") spacingMult = 0.25; // Volatile crosses + // else Major/Minor/Exotic: 0.20 (clean structure) + double minSpacing = atr * spacingMult; + // TP1 = NEAREST target >= minTP1Dist + for(int i = 0; i < tgtCount; i++) + { + double dist = MathAbs(targets[i] - cand.entryPrice); + if(dist >= minTP1Dist) + { + cand.tp1 = targets[i]; + // TP2 = NEXT target beyond TP1 + spacing + for(int j = i + 1; j < tgtCount; j++) + { + if(MathAbs(targets[j] - cand.tp1) >= minSpacing) + { + cand.tp2 = targets[j]; + // TP3 = FURTHEST target beyond TP2 + spacing + for(int k = j + 1; k < tgtCount; k++) + { + if(MathAbs(targets[k] - cand.tp2) >= minSpacing) + { + cand.tp3 = targets[k]; + break; + } + } + break; + } + } + break; + } + } + // -- FALLBACK: If not enough structure targets, keep ATR-based TPs -- + // ATR TPs were already set above, so only override when structure found + double newTP1Dist = MathAbs(cand.tp1 - cand.entryPrice); + double newTP2Dist = MathAbs(cand.tp2 - cand.entryPrice); + double newTP3Dist = MathAbs(cand.tp3 - cand.entryPrice); + // Ensure TP2 is further than TP1 (if structure placed them wrong) + if(newTP2Dist <= newTP1Dist * 1.10) + { + double atrTP2 = tp2Mult * atr; // Fall back to ATR for TP2 + if(atrTP2 > newTP1Dist * 1.10) + cand.tp2 = cand.isBullish ? cand.entryPrice + atrTP2 : cand.entryPrice - atrTP2; + } + // Ensure TP3 is further than TP2 + newTP2Dist = MathAbs(cand.tp2 - cand.entryPrice); + if(newTP3Dist <= newTP2Dist * 1.10) + { + double atrTP3 = tp3Mult * atr; // Fall back to ATR for TP3 + if(atrTP3 > newTP2Dist * 1.10) + cand.tp3 = cand.isBullish ? cand.entryPrice + atrTP3 : cand.entryPrice - atrTP3; + } + if(g_verboseLog) + { + string tpSrc1 = "ATR", tpSrc2 = "ATR", tpSrc3 = "ATR"; + if(MathAbs(cand.tp1 - (cand.entryPrice + (cand.isBullish ? 1 : -1) * tp1Mult * atr)) > atr * 0.05) tpSrc1 = "STRUCT"; + if(MathAbs(cand.tp2 - (cand.entryPrice + (cand.isBullish ? 1 : -1) * tp2Mult * atr)) > atr * 0.05) tpSrc2 = "STRUCT"; + if(MathAbs(cand.tp3 - (cand.entryPrice + (cand.isBullish ? 1 : -1) * tp3Mult * atr)) > atr * 0.05) tpSrc3 = "STRUCT"; + Print("* v9.03 STRUCTURE TPs: Found ", tgtCount, " targets | ", + "TP1[", tpSrc1, "]=", DoubleToString(MathAbs(cand.tp1 - cand.entryPrice) / atr, 2), "xATR | ", + "TP2[", tpSrc2, "]=", DoubleToString(MathAbs(cand.tp2 - cand.entryPrice) / atr, 2), "xATR | ", + "TP3[", tpSrc3, "]=", DoubleToString(MathAbs(cand.tp3 - cand.entryPrice) / atr, 2), "xATR"); + } + } + // else: tgtCount == 0 -> keep ATR-based TPs (already set above) + } + // =============================================================== + // * FIX #1 & #2: SMART TP ADJUSTMENT - Obstacle avoidance + // Pull TPs CLOSER when opposing structure blocks the path + // =============================================================== + // * v7.5b FIX 22: Save original TPs BEFORE SmartAdjustTPs + double origTP1c = cand.tp1, origTP2c = cand.tp2, origTP3c = cand.tp3; + SmartAdjustTPs(cand.entryPrice, cand.stopLoss, cand.tp1, cand.tp2, cand.tp3, + cand.isBullish, atr); + // * v7.5b FIX 22: Limit compression -- TP never below 60% of original + { + double oD1 = MathAbs(origTP1c - cand.entryPrice), nD1 = MathAbs(cand.tp1 - cand.entryPrice); + if(oD1 > 0 && nD1 < oD1 * 0.60) + { double r = oD1 * 0.60; cand.tp1 = cand.isBullish ? cand.entryPrice + r : cand.entryPrice - r; } + double oD2 = MathAbs(origTP2c - cand.entryPrice), nD2 = MathAbs(cand.tp2 - cand.entryPrice); + if(oD2 > 0 && nD2 < oD2 * 0.60) + { double r2 = oD2 * 0.60; cand.tp2 = cand.isBullish ? cand.entryPrice + r2 : cand.entryPrice - r2; } + double oD3 = MathAbs(origTP3c - cand.entryPrice), nD3 = MathAbs(cand.tp3 - cand.entryPrice); + if(oD3 > 0 && nD3 < oD3 * 0.60) + { double r3 = oD3 * 0.60; cand.tp3 = cand.isBullish ? cand.entryPrice + r3 : cand.entryPrice - r3; } + } + // * v6.3: R:R SAFETY FLOOR after SmartAdjustTPs + // Ensure TP1 was not pulled so close that R:R drops below MinRR + // * v9.09 FIX#15f: HTF-aware -- on H4+, use lower effective minRR + // because achievable moves are smaller relative to SL distance. + // H4 example: SL=21.6p, best move=22p -> R:R~=1.0 is realistic. + // Enforcing minRR=1.5 pushes TP to 32.4p -> never hits -> all losses! + double effectiveMinRR = EA_MinRR; + // * FIX#390a: Use pair table mrr as the floor, not hardcoded 0.8R. + // BUG: H4 floor = 0.8R was root cause of negative-EV trades passing. + // EURUSD H4 pair mrr=1.60 means breakeven requires RR≥1.60 at 46% WR. + // Old floor 0.8R allowed RR=0.9 through → guaranteed -EV → losses. + // FIX: Use g_autoOptParams.min_rr (= pair table mrr, set in ApplyPairTFProfile). + // Falls back to EA_MinRR if AutoOpt not initialised yet. + // D1 keeps 0.7R floor (achievable moves are smaller relative to SL on daily). + // * FIX#392a: effectiveMinRR floor = pair table mrr directly (NOT g_autoOptParams.min_rr) + // BUG: FIX#391 read g_autoOptParams.min_rr which FIX#19 had already clamped. + // FIX#19 runs when tp_atr_mult is session-reduced (×0.90): + // achievableRR = (3.20×0.90)/1.60 = 1.80 → maxMinRR = 1.80×0.85 = 1.53 + // → g_autoOptParams.min_rr clamped: 1.60 → 1.53 + // → FIX#391 floor = 1.20 (hardcoded, ignored the clamped value anyway) + // BUT FIX#37 exception reads adjustedMinRR which IS based on g_autoOptParams.min_rr=1.53 + // → fix37MinRR = MathMax(1.0, 1.53×0.96-0.3) = 1.17 (too loose) + // + // FIX: Read mrr directly from pair table — immune to FIX#19 clamping. + // GetPairTFMinRR() calls GetPairTFConfig().mrr[tfIdx] — always the canonical value. + // effectiveMinRR floor = pair mrr × 0.80 (80% = allow 20% buffer for spread RR adj) + // H4 EURUSD: 1.60 × 0.80 = 1.28 → better than hardcoded 1.20 AND pair-aware. + { + double _pairMRR392 = GetPairTFMinRR(_Symbol, _Period); + if(_Period >= PERIOD_D1) + effectiveMinRR = MathMin(effectiveMinRR, 0.7); + else if(_Period >= PERIOD_H4) + effectiveMinRR = MathMax(effectiveMinRR, _pairMRR392 * 0.80); // * FIX#392a: 1.60×0.80=1.28 + else if(_Period >= PERIOD_H1) + effectiveMinRR = MathMin(effectiveMinRR, 1.0); + } + double postAdjustRisk = MathAbs(cand.entryPrice - cand.stopLoss); + double postAdjustReward = MathAbs(cand.tp1 - cand.entryPrice); + if(postAdjustRisk > 0 && postAdjustReward / postAdjustRisk < effectiveMinRR) + { + // Restore TP1 to minimum viable distance + double minTP1Dist = postAdjustRisk * effectiveMinRR; + if(cand.isBullish) + cand.tp1 = cand.entryPrice + minTP1Dist; + else + cand.tp1 = cand.entryPrice - minTP1Dist; + if(g_verboseLog) + Print("* v6.3: TP1 restored to MinRR floor after SmartAdjustTPs | R:R=", + DoubleToString(effectiveMinRR, 2)); + } + // * v7.5b FIX 21: GUARANTEE TP ORDERING (TP1 < TP2 < TP3 for BUY, opposite for SELL) + // Problem: When SL floor activates (ATR=$6, SL floored to $35), TP1 gets pushed to $45 + // but TP2/TP3 are still ATR-based ($15/$19) -> TP2 < TP1 = inverted ordering! + // Solution: Ensure each TP is at least 15% further than the previous one + double tp1Dist = MathAbs(cand.tp1 - cand.entryPrice); + double tp2Dist = MathAbs(cand.tp2 - cand.entryPrice); + double tp3Dist = MathAbs(cand.tp3 - cand.entryPrice); + if(tp2Dist <= tp1Dist * 1.10) // TP2 must be at least 10% beyond TP1 + tp2Dist = tp1Dist * 1.20; + if(tp3Dist <= tp2Dist * 1.10) // TP3 must be at least 10% beyond TP2 + tp3Dist = tp2Dist * 1.20; + if(cand.isBullish) + { + cand.tp2 = cand.entryPrice + tp2Dist; + cand.tp3 = cand.entryPrice + tp3Dist; + } + else + { + cand.tp2 = cand.entryPrice - tp2Dist; + cand.tp3 = cand.entryPrice - tp3Dist; + } + // =============================================================== + // * v9.09 FIX#15g: FINAL TP HARD CLAMP (ALL timeframes) + // After ALL calculations (ATR, structure-first, SmartAdjust, R:R floor, + // ordering), clamp TPs to physically achievable distances. + // This catches techniques that call BuildCandidateSLTP. + // FIX#15h (centralized) catches ALL techniques including bypasses. + // =============================================================== + { + // * v9.36 FIX#159: Sync FIX#15g clamp values with FIX#153 (FIX#15h centralized). + // BUG: FIX#153 updated ONLY FIX#15h (post-SelectBestCandidate clamp on g_ea_signal). + // FIX#15g (pre-SelectBestCandidate clamp in BuildCandidateSLTP) kept OLD tight values: + // H4: maxTP1=1.5xATR → cand.rr = 1.5/2.0 = 0.75 < adjMinRR=1.80 → ALL H4 rejected! + // D1: maxTP1=1.2xATR → cand.rr = 1.2/3.0 = 0.40 → impossible to pass R:R gate + // ROOT CAUSE of 0-1 trades in 35-day H4 backtest. + // Fix: Mirror FIX#153 values here so cand.rr is computed from correct (wider) TP distances. + double maxTP1xATR = 3.5, maxTP2xATR = 4.5, maxTP3xATR = 5.5; // M1 defaults + if(_Period >= PERIOD_D1) + { + maxTP1xATR = 6.0; maxTP2xATR = 8.0; maxTP3xATR = 11.0; // * FIX#159: was 1.2/1.4/1.7 + } + else if(_Period >= PERIOD_H4) + { + maxTP1xATR = 4.5; maxTP2xATR = 6.0; maxTP3xATR = 8.0; // * FIX#159: was 1.5/1.8/2.1 (CRITICAL: caused RR=0.75 -> 0 H4 trades) + } + else if(_Period >= PERIOD_H1) + { + maxTP1xATR = 3.5; maxTP2xATR = 4.5; maxTP3xATR = 5.5; // * FIX#159: was 2.0/2.3/2.7 + } + else if(_Period >= PERIOD_M15) + { + maxTP1xATR = 3.0; maxTP2xATR = 4.0; maxTP3xATR = 5.5; // * FIX#159: was 2.5/2.8/3.2 + } + else if(_Period >= PERIOD_M5) + { + maxTP1xATR = 3.0; maxTP2xATR = 3.3; maxTP3xATR = 3.7; // unchanged + } + // else M1: 3.5/4.5/5.5 (defaults above) + double finalTP1Dist = MathAbs(cand.tp1 - cand.entryPrice); + double finalTP2Dist = MathAbs(cand.tp2 - cand.entryPrice); + double finalTP3Dist = MathAbs(cand.tp3 - cand.entryPrice); + bool clamped = false; + if(finalTP1Dist > maxTP1xATR * atr) + { + finalTP1Dist = maxTP1xATR * atr; + cand.tp1 = cand.isBullish ? cand.entryPrice + finalTP1Dist : cand.entryPrice - finalTP1Dist; + clamped = true; + } + if(finalTP2Dist > maxTP2xATR * atr) + { + finalTP2Dist = maxTP2xATR * atr; + cand.tp2 = cand.isBullish ? cand.entryPrice + finalTP2Dist : cand.entryPrice - finalTP2Dist; + clamped = true; + } + if(finalTP3Dist > maxTP3xATR * atr) + { + finalTP3Dist = maxTP3xATR * atr; + cand.tp3 = cand.isBullish ? cand.entryPrice + finalTP3Dist : cand.entryPrice - finalTP3Dist; + clamped = true; + } + // Re-enforce ordering after clamp + finalTP2Dist = MathAbs(cand.tp2 - cand.entryPrice); + finalTP3Dist = MathAbs(cand.tp3 - cand.entryPrice); + if(finalTP2Dist <= finalTP1Dist * 1.05) + { + finalTP2Dist = finalTP1Dist * 1.15; + cand.tp2 = cand.isBullish ? cand.entryPrice + finalTP2Dist : cand.entryPrice - finalTP2Dist; + } + if(finalTP3Dist <= finalTP2Dist * 1.05) + { + finalTP3Dist = finalTP2Dist * 1.15; + cand.tp3 = cand.isBullish ? cand.entryPrice + finalTP3Dist : cand.entryPrice - finalTP3Dist; + } + if(clamped && EnableDebugMode) + PrintFormat("* FIX#15g HTF TP CLAMP: TP1=%.1fp(%.1fxATR) TP2=%.1fp(%.1fxATR) TP3=%.1fp(%.1fxATR)", + finalTP1Dist / g_pipValue, finalTP1Dist / atr, + finalTP2Dist / g_pipValue, finalTP2Dist / atr, + finalTP3Dist / g_pipValue, finalTP3Dist / atr); + } +} +//+------------------------------------------------------------------+ +//| * v9.03: SmartAdjustTPs -- OBSTACLE AVOIDANCE ONLY | +//| Structure-first targeting (v9.03) handles TP placement. | +//| This function ONLY pulls TPs closer when opposing structure | +//| (strong OBs, FVGs, Breakers) blocks the path to target. | +//+------------------------------------------------------------------+ +void SmartAdjustTPs(double entry, double sl, double &tp1, double &tp2, double &tp3, + bool isBullish, double atr) +{ + double minTP1Dist = MathAbs(entry - sl) * EA_MinRR; + // -- Collect OPPOSING structures that could BLOCK price -- + double obstacles[]; + ArrayResize(obstacles, 0, 30); + int obsCount = 0; + // 1. Opposing Order Blocks (strong resistance/support) + for(int i = 0; i < ArraySize(OB_Array); i++) + { + if(!OB_Array[i].active || OB_Array[i].mitigated) continue; + if(OB_Array[i].strength < 0.90) continue; // Only strong OBs are real obstacles + double obLevel = 0; + bool isOpposing = false; + if(isBullish && !OB_Array[i].isBullish) // Bearish OB above = resistance + { + obLevel = OB_Array[i].bottom; + isOpposing = (obLevel > entry && obLevel < tp3); + } + else if(!isBullish && OB_Array[i].isBullish) // Bullish OB below = support + { + obLevel = OB_Array[i].top; + isOpposing = (obLevel < entry && obLevel > tp3); + } + if(isOpposing && obLevel > 0) + { + int ns = ArraySize(obstacles) + 1; + ArrayResize(obstacles, ns, 30); + obstacles[ns - 1] = obLevel; + obsCount++; + } + } + // 2. Opposing FVGs (unfilled, HIGH+ quality only) + for(int i = 0; i < ArraySize(FVG_Array); i++) + { + if(FVG_Array[i].status != FVG_STATUS_ACTIVE) continue; + if(FVG_Array[i].quality < FVG_QUALITY_HIGH) continue; + double fvgEdge = 0; + bool isOpposing = false; + if(isBullish && !FVG_Array[i].isBullish) + { + fvgEdge = FVG_Array[i].bottom; + isOpposing = (fvgEdge > entry && fvgEdge < tp3); + } + else if(!isBullish && FVG_Array[i].isBullish) + { + fvgEdge = FVG_Array[i].top; + isOpposing = (fvgEdge < entry && fvgEdge > tp3); + } + if(isOpposing && fvgEdge > 0) + { + int ns = ArraySize(obstacles) + 1; + ArrayResize(obstacles, ns, 30); + obstacles[ns - 1] = fvgEdge; + obsCount++; + } + } + // 3. Breaker Blocks (strong only) + for(int i = 0; i < ArraySize(BREAKER_Array); i++) + { + if(!BREAKER_Array[i].active || BREAKER_Array[i].mitigated) continue; + if(BREAKER_Array[i].strength < 0.85) continue; + double bbLevel = 0; + bool isOpposing = false; + if(isBullish && !BREAKER_Array[i].isBullish) + { + bbLevel = BREAKER_Array[i].bottom; + isOpposing = (bbLevel > entry && bbLevel < tp3); + } + else if(!isBullish && BREAKER_Array[i].isBullish) + { + bbLevel = BREAKER_Array[i].top; + isOpposing = (bbLevel < entry && bbLevel > tp3); + } + if(isOpposing && bbLevel > 0) + { + int ns = ArraySize(obstacles) + 1; + ArrayResize(obstacles, ns, 30); + obstacles[ns - 1] = bbLevel; + obsCount++; + } + } + // -- PULL TPs closer when obstacles block the path -- + if(obsCount > 0) + { + // Sort obstacles by distance from entry + if(isBullish) + ArraySort(obstacles); // Ascending (nearest first) + else + { + // Descending for shorts (nearest below entry first) + double tempArr[]; + int sz = ArraySize(obstacles); + ArrayResize(tempArr, sz); + ArraySort(obstacles); + for(int j = 0; j < sz; j++) + tempArr[j] = obstacles[sz - 1 - j]; + ArrayCopy(obstacles, tempArr); + } + // * v9.03 FIX#10: Pair-category aware obstacle buffer + double obsBufMult = 0.30; // Default for Forex + string _obsCat = g_autoOptParams.pair_category; + if(_obsCat == "Index") obsBufMult = 0.45; // Indices: wider buffer (noise) + else if(_obsCat == "Metal") obsBufMult = 0.40; // Metals: wider + else if(_obsCat == "Energy") obsBufMult = 0.40; // Energy: wider + else if(_obsCat == "VolatileCross") obsBufMult = 0.35; // Volatile crosses + double buffer = atr * obsBufMult; + // TP1: Pull to just before FIRST obstacle (if closer than current TP1) + if(obsCount >= 1) + { + double obs1 = obstacles[0]; + double adjTP1 = isBullish ? (obs1 - buffer) : (obs1 + buffer); + double distAdj = MathAbs(adjTP1 - entry); + if(isBullish && adjTP1 < tp1 && distAdj >= minTP1Dist) + tp1 = adjTP1; + else if(!isBullish && adjTP1 > tp1 && distAdj >= minTP1Dist) + tp1 = adjTP1; + } + // TP2: Pull to before SECOND obstacle + if(obsCount >= 2) + { + double obs2 = obstacles[1]; + double adjTP2 = isBullish ? (obs2 - buffer) : (obs2 + buffer); + if(isBullish && adjTP2 < tp2 && adjTP2 > tp1) + tp2 = adjTP2; + else if(!isBullish && adjTP2 > tp2 && adjTP2 < tp1) + tp2 = adjTP2; + } + // TP3: Pull to before THIRD obstacle + if(obsCount >= 3) + { + double obs3 = obstacles[2]; + double adjTP3 = isBullish ? (obs3 - buffer) : (obs3 + buffer); + if(isBullish && adjTP3 < tp3 && adjTP3 > tp2) + tp3 = adjTP3; + else if(!isBullish && adjTP3 > tp3 && adjTP3 < tp2) + tp3 = adjTP3; + } + } + // -- SAFETY: Ensure TP ordering -- + double tp1Dist = MathAbs(tp1 - entry); + if(tp1Dist < minTP1Dist) tp1Dist = minTP1Dist; + if(isBullish) + { + tp1 = MathMax(tp1, entry + minTP1Dist); + double minTP2 = entry + tp1Dist * 1.3; + double minTP3 = entry + tp1Dist * 1.6; + tp2 = MathMax(tp2, minTP2); + tp3 = MathMax(tp3, minTP3); + } + else + { + tp1 = MathMin(tp1, entry - minTP1Dist); + double minTP2 = entry - tp1Dist * 1.3; + double minTP3 = entry - tp1Dist * 1.6; + tp2 = MathMin(tp2, minTP2); + tp3 = MathMin(tp3, minTP3); + } + if(g_verboseLog) + { + double safeATR = (atr > 0) ? atr : 1.0; + Print("* SmartTP [ObstacleOnly]: Obstacles=", obsCount, + " | TP1=", DoubleToString(MathAbs(tp1 - entry) / safeATR, 2), "xATR", + " TP2=", DoubleToString(MathAbs(tp2 - entry) / safeATR, 2), "xATR", + " TP3=", DoubleToString(MathAbs(tp3 - entry) / safeATR, 2), "xATR"); + } +} +//+------------------------------------------------------------------+ +//| * Select Best Candidate Signal | +//+------------------------------------------------------------------+ +// ========================================================================= +// ENTRY PIPELINE — SelectBestCandidate + EA_CheckSignals +// +// All per-pair/TF config from PairTFConfig (ApplyPairTFProfile sets working vars). +// Two responsibilities: +// EA_CheckSignals — pre-scan guards (time/spread/DD/direction) +// SelectBestCandidate — per-candidate quality gates, returns best index +// +// Guard philosophy: block as early as possible, as rarely as necessary. +// Quality philosophy: score + confluence decide, not hardcoded regime rules. +// ========================================================================= + +// ───────────────────────────────────────────────────────────────────────── +// SelectBestCandidate — called from EA_CheckSignals after candidates built +// Returns index of best candidate or -1 if none qualify +// ───────────────────────────────────────────────────────────────────────── +int SelectBestCandidate() +{ + if(g_candidateCount == 0) return -1; + + // --- Thresholds from gates (set by ComputeActiveGates / pair table) --- + double minScore = g_gates.computed ? (double)g_gates.minScore : EA_MinEntryScore; + int minConf = g_gates.computed ? g_gates.minConfirmations : 2; + double minRR = (AutoOpt_Enabled && g_workingMinRiskReward > 0) ? g_workingMinRiskReward : EA_MinRR; + + // Regime-adjusted minRR — market geometry changes with regime + switch(g_regimeData.regime) + { + case REGIME_RANGING: case REGIME_RANGING_TIGHT: case REGIME_RANGING_WIDE: + minRR *= (_Period <= PERIOD_M5) ? 0.70 : 0.80; break; + case REGIME_CHOPPY: case REGIME_VOLATILE: + minRR *= 0.90; break; + case REGIME_WEAK_TREND_UP: case REGIME_WEAK_TREND_DOWN: + minRR *= (_Period >= PERIOD_H4) ? 0.85 : 0.92; break; + case REGIME_BREAKOUT: + minRR *= 1.10; break; + case REGIME_STRONG_TREND_UP: case REGIME_STRONG_TREND_DOWN: + minRR *= 1.05; break; + case REGIME_UNKNOWN: + minRR *= 0.85; break; + default: break; + } + + // Spread-adjusted minRR: R:R is net of spread + double spreadCost = (double)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * _Point; + double avgRisk = (g_cachedATR > 0) ? GetActiveSLMult() * g_cachedATR : 1.0; + double adjMinRR = MathMax(1.0, minRR - (avgRisk > 0 ? spreadCost / avgRisk : 0)); + + // --- Time blocks (D1+ exempt — position TF, session irrelevant) --- + if(_Period < PERIOD_D1) + { + MqlDateTime dt; + TimeToStruct(TimeCurrent(), dt); + if(Time_AvoidMondayAM && dt.day_of_week == 1 && dt.hour < 3) return -1; + if(Time_AvoidWorstHours && + (dt.hour >= Time_AvoidHoursStart || dt.hour < Time_AvoidHoursEnd)) return -1; + if(Time_AvoidWeekends && (dt.day_of_week == 0 || dt.day_of_week == 6)) return -1; + // Low-liquidity dates + if((dt.mon == 1 && dt.day <= 3) || + (dt.mon == 12 && dt.day >= 24 && dt.day <= 26)) return -1; + } + + // --- Direction conflict: both sides have close scores → market undecided --- + int bullCnt = 0, bearCnt = 0; + double maxBull = 0, maxBear = 0; + for(int i = 0; i < g_candidateCount; i++) + { + if(!g_candidates[i].valid) continue; + if(g_candidates[i].isBullish) { bullCnt++; maxBull = MathMax(maxBull, g_candidates[i].totalScore); } + else { bearCnt++; maxBear = MathMax(maxBear, g_candidates[i].totalScore); } + } + bool conflict = (bullCnt > 0 && bearCnt > 0); + if(conflict && MathAbs(maxBull - maxBear) < 10) return -1; + + // --- Per-candidate evaluation --- + int bestIdx = -1; + double bestComp = -9999; + + // Diagnostic counters + int rejTech=0, rejDir=0, rejCT=0, rejScore=0, rejRR=0, rejConf=0, rejKZ=0, rejDiv=0; + + for(int i = 0; i < g_candidateCount; i++) + { + if(!g_candidates[i].valid) continue; + bool isBull = g_candidates[i].isBullish; + double score = g_candidates[i].totalScore; + double rr = g_candidates[i].rr; + ENUM_ENTRY_TECHNIQUE tech = g_candidates[i].technique; + + // ── Gate 1: Technique enabled (pair table via ApplyPairTFProfile) ── + // Convention: -1=force-off, 0=use global Enable* input, 1=force-on + if(tech == TECH_FVG) + { + bool off = (g_workingAllowFVG == -1) || (g_workingAllowFVG == 0 && !EnableFVG); + if(off) { rejTech++; continue; } + } + if(tech == TECH_OB) + { + bool off = (g_workingAllowOB == -1) || (g_workingAllowOB == 0 && !EnableOB); + if(off) { rejTech++; continue; } + } + if(tech == TECH_BOS_RETEST) + { + bool off = (g_workingAllowBOSRetest == -1) || (g_workingAllowBOSRetest == 0 && !STRATEGY_BOS_Retest); + if(off) { rejTech++; continue; } + } + if(tech == TECH_OTE) + { + bool off = (g_workingAllowOTE == -1) || (g_workingAllowOTE == 0 && !EnableOTE); + if(off) { rejTech++; continue; } + } + if(tech == TECH_BREAKER) + { + bool off = (g_workingAllowBreaker == -1) || (g_workingAllowBreaker == 0 && !EnableBreakerBlocks); + if(off) { rejTech++; continue; } + } + if(tech == TECH_TREND_CONT) + { + bool off = (g_workingAllowTC == -1) || (g_workingAllowTC == 0 && !EnableTrendCont); + if(off) { rejTech++; continue; } + } + // KILLZONE_ENTRY uses TECH_SILVER_BULLET — gated by allow_liq from pair table. + // allow_liq=-1 → force-off (e.g. EURUSD H1: 8T 62%WR -$550, payoff ratio broken). + // 0 = use global KZ_EnableSilverBullet input. 1 = force-on. + if(tech == TECH_SILVER_BULLET) + { + bool off = (g_workingAllowLIQ == -1) || (g_workingAllowLIQ == 0 && !KZ_EnableSilverBullet); + if(off) { rejTech++; continue; } + } + // LIQ_SWEEP uses TECH_LIQ_SWEEP — same allow_liq flag. + if(tech == TECH_LIQ_SWEEP) + { + bool off = (g_workingAllowLIQ == -1) || (g_workingAllowLIQ == 0 && !EnableLiquidity); + if(off) { rejTech++; continue; } + } + + // ── Gate 2: D1 CHoCH direction ────────────────────────────────────── + if(g_workingD1CHoCHGate && g_d1CHoCH_Valid) + { + // D1 direction is stale when H4 structure AND MTF both contradict it + bool mtfBull = (g_mtfAnalysis.overallDirection == MTF_BULLISH || + g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH); + bool mtfBear = (g_mtfAnalysis.overallDirection == MTF_BEARISH || + g_mtfAnalysis.overallDirection == MTF_STRONG_BEARISH); + bool d1Stale = (g_d1CHoCH_Bear && g_isBullishStructure && mtfBull) || + (g_d1CHoCH_Bull && !g_isBullishStructure && mtfBear); + if(!d1Stale) + { + if(g_d1CHoCH_Bear && isBull) { rejDir++; continue; } + if(g_d1CHoCH_Bull && !isBull) { rejDir++; continue; } + } + } + + // ── Gate 3: Counter-trend in trending regime ───────────────────────── + // ICT principle: only counter-trend when HTF delivery has reversed + { + bool regTrendBull = (g_regimeData.regime == REGIME_STRONG_TREND_UP || + g_regimeData.regime == REGIME_TREND_UP || + g_regimeData.regime == REGIME_WEAK_TREND_UP); + bool regTrendBear = (g_regimeData.regime == REGIME_STRONG_TREND_DOWN || + g_regimeData.regime == REGIME_TREND_DOWN || + g_regimeData.regime == REGIME_WEAK_TREND_DOWN); + bool isCT = (regTrendBull && !isBull) || (regTrendBear && isBull); + + if(isCT) + { + // MTF must support the counter-trend direction (HTF has already turned) + // On H1+, counter-trend entries require confirmed multi-TF alignment (STRONG). + // Plain BULLISH/BEARISH only reflects short-term momentum, not HTF delivery. + bool _needStrong = (_Period >= PERIOD_H1); + bool mtfSupports = (_needStrong) + ? (( isBull && g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH) || + (!isBull && g_mtfAnalysis.overallDirection == MTF_STRONG_BEARISH)) + : (( isBull && (g_mtfAnalysis.overallDirection == MTF_BULLISH || + g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH)) || + (!isBull && (g_mtfAnalysis.overallDirection == MTF_BEARISH || + g_mtfAnalysis.overallDirection == MTF_STRONG_BEARISH))); + + // H4+: structure must not oppose (unless D1 CHoCH confirms the shift) + if(_Period >= PERIOD_H4 && mtfSupports) + { + bool structOpposes = ( isBull && !g_isBullishStructure) || + (!isBull && g_isBullishStructure); + bool chochConfirms = ( isBull && g_d1CHoCH_Bull && g_d1CHoCH_Valid) || + (!isBull && g_d1CHoCH_Bear && g_d1CHoCH_Valid); + if(structOpposes && !chochConfirms) mtfSupports = false; + } + + if(!mtfSupports) { rejCT++; continue; } + // MTF supports the CT trade → mark exempt so EvaluateSmartEntry allows it + g_fix176CTExempt = true; + } + } + + // ── Gate 4b: H1 structure-direction alignment ─────────────────────── + // Counter-structure entries (e.g. SELL when H1 structure=BULLISH) need MTF support. + // H4+: STRONG MTF required (large moves; plain BEARISH = short-term noise on H4). + // H1/M15: plain BEARISH/BULLISH sufficient — intraday delivery can oppose H1 structure + // when the higher timeframe delivery has already turned (e.g. MTF 57% bearish). + // Without ANY MTF support the trade is pure speculation — still blocked. + if(_Period <= PERIOD_H1 && _Period >= PERIOD_M15 && EnableStructure) + { + bool structOpposes = ( isBull && !g_isBullishStructure) || + (!isBull && g_isBullishStructure); + if(structOpposes) + { + // Accept plain OR strong MTF alignment for H1/M15 (intraday delivery) + bool mtfSupports = ( isBull && (g_mtfAnalysis.overallDirection == MTF_BULLISH || + g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH)) || + (!isBull && (g_mtfAnalysis.overallDirection == MTF_BEARISH || + g_mtfAnalysis.overallDirection == MTF_STRONG_BEARISH)); + if(!mtfSupports) { rejDir++; continue; } + } + } + + // ── Gate 4: Market context direction (if confident) ────────────────── + if(g_mktCtx.valid && g_mktCtx.contextConfidence >= 65) + { + if( isBull && !g_mktCtx.allowBuy) { rejDir++; continue; } + if(!isBull && !g_mktCtx.allowSell) { rejDir++; continue; } + } + + // ── Gate 5: Minimum score ──────────────────────────────────────────── + { + double effScore = minScore; + bool weakOrChoppy = (g_regimeData.regime == REGIME_CHOPPY || + g_regimeData.regime == REGIME_VOLATILE || + g_regimeData.regime == REGIME_RANGING || + g_regimeData.regime == REGIME_RANGING_TIGHT || + g_regimeData.regime == REGIME_RANGING_WIDE || + g_regimeData.regime == REGIME_WEAK_TREND_UP || + g_regimeData.regime == REGIME_WEAK_TREND_DOWN); + if(weakOrChoppy) effScore += (double)g_gates.weakPenalty; + if(score < effScore) { rejScore++; continue; } + } + + // ── Gate 6: Minimum R:R ────────────────────────────────────────────── + { + double effRR = adjMinRR; + bool isWeak = (g_regimeData.regime == REGIME_WEAK_TREND_UP || + g_regimeData.regime == REGIME_WEAK_TREND_DOWN || + g_regimeData.regime == REGIME_CHOPPY); + if(isWeak && _Period < PERIOD_H4) effRR *= 1.05; + if(rr < effRR) { rejRR++; continue; } + } + + // ── Gate 7: Minimum confirmations ─────────────────────────────────── + if(!g_candidates[i].cascade.passesMinimum) { rejConf++; continue; } + + // ── Gate 8: Killzone (pair table req_kz) ──────────────────────────── + if(g_workingRequireKillzone && + !g_candidates[i].cascade.confirmations[CONF_KILLZONE].confirmed) { rejKZ++; continue; } + + // ── Gate 9: Confirmed opposing divergence (recent ≤3 bars) ────────── + if(Divergence_Enabled) + { + bool divBlocks = false; + for(int dv = 0; dv < ArraySize(g_divergences) && !divBlocks; dv++) + { + if(!g_divergences[dv].active || !g_divergences[dv].confirmed) continue; + if(TimeCurrent() - g_divergences[dv].time2 > 3*PeriodSeconds(_Period)) continue; + bool divBull = (g_divergences[dv].type == DIV_REGULAR_BULLISH || + g_divergences[dv].type == DIV_HIDDEN_BULLISH); + bool divBear = (g_divergences[dv].type == DIV_REGULAR_BEARISH || + g_divergences[dv].type == DIV_HIDDEN_BEARISH); + if( isBull && divBear) divBlocks = true; + if(!isBull && divBull) divBlocks = true; + } + if(divBlocks) { rejDiv++; continue; } + } + + // ── Composite score: base score + AMD timing bonus + conflict penalty ── + // AMD phase bonus: DISTRIBUTION (price at premium) favors SELL entries. + // ACCUMULATION (price at discount) favors BUY entries. + // This rewards correctly-timed entries without blocking wrong-phase ones. + double composite = score; + if(AMD_Enabled && g_amdData.active) + { + bool amdAligned = ( isBull && g_amdData.phase == AMD_ACCUMULATION) || + (!isBull && g_amdData.phase == AMD_DISTRIBUTION); + if(amdAligned) composite += 5.0; + } + // AMD confirmation from cascade (structural confirmation already computed) + if(g_candidates[i].cascade.confirmations[CONF_AMD_PHASE].confirmed) composite += 3.0; + if(conflict) composite -= (MathAbs(maxBull - maxBear) < 15) ? 25.0 : 12.0; + if(composite > bestComp) { bestComp = composite; bestIdx = i; } + } + + // Diagnostic log + if(g_verboseLog) + { + if(bestIdx < 0) + PrintFormat("[Entry] No candidate | tech=%d dir=%d CT=%d score=%d RR=%d conf=%d KZ=%d div=%d | regime=%s", + rejTech, rejDir, rejCT, rejScore, rejRR, rejConf, rejKZ, rejDiv, + GetRegimeString(g_regimeData.regime)); + else + PrintFormat("[Entry] Best: %s %s sc=%.0f rr=%.2f conf=%d | composite=%.0f", + g_candidates[bestIdx].type, + g_candidates[bestIdx].isBullish ? "BUY" : "SELL", + g_candidates[bestIdx].totalScore, g_candidates[bestIdx].rr, + g_candidates[bestIdx].cascade.totalConfirmed, bestComp); + } + return bestIdx; +} +//+------------------------------------------------------------------+ +//| EA_CheckSignals — pre-scan guards + build candidates + execute | +//+------------------------------------------------------------------+ +void EA_CheckSignals() +{ + g_ea_signal.isValid = false; + g_ea_signal.isBullish = false; + g_ea_signal.zoneTop = 0; + g_ea_signal.zoneBottom = 0; + g_ea_signal.zoneType = ""; + g_candidateCount = 0; + g_fix176CTExempt = false; + g_pendingZoneTop = 0; + g_pendingZoneBottom = 0; + g_pendingZoneType = ""; + g_pendingMRScore = 0; + + // ── Market context + scenario (computed by RunSharedAnalysis) ──── + // FIX#502 Section 10+17 merge: ComputeMarketContext() and + // DeriveScenarioProfile() now run at the END of RunSharedAnalysis(), + // after all detectors. g_mktCtx and g_scenarioProfile are ready here. + // No recomputation needed — single owner = RunSharedAnalysis. + + // ── FIX#502: Scenario gate — CHOPPY/UNKNOWN → no entry ─────────── + // Must run BEFORE other guards to avoid wasting time evaluating + // candidates when the market has no tradeable structure. + // isValid=false means DeriveScenarioProfile classified as CHOPPY. + if(!g_scenarioProfile.isValid) + { + if(g_verboseLog) + PrintFormat("[FIX#502] No entry — scenario=%s (choppy/unknown)", g_scenarioProfile.description); + return; + } + + // ── Guard 1: Consecutive loss halt ─────────────────────────────── + if(EA_MaxConsecLossHalt > 0) + { + int streak = (g_currentStreak < 0) ? MathAbs(g_currentStreak) : 0; + if(streak >= EA_MaxConsecLossHalt) + { + if(g_verboseLog) + PrintFormat("[Entry] Loss halt: %d/%d losses → no entries today", streak, EA_MaxConsecLossHalt); + return; + } + } + + // ── Guard 2: Context blocks all directions ──────────────────────── + if(g_mktCtx.valid && g_mktCtx.contextConfidence >= 60) + if(!g_mktCtx.allowBuy && !g_mktCtx.allowSell) return; + + // ── Guard 3: Time filter (D1+ exempt — position TF) ────────────── + if(EA_EnableTimeFilter && _Period < PERIOD_D1) + { + MqlDateTime dt; + TimeToStruct(TimeCurrent(), dt); + int cur = dt.hour * 60 + dt.min; + int s = EA_StartHour * 60 + EA_StartMinute; + int e = EA_EndHour * 60 + EA_EndMinute; + bool inWindow = (s <= e) ? (cur >= s && cur <= e) : (cur >= s || cur <= e); + if(!inWindow) return; + if(EA_FridayCloseHour > 0 && dt.day_of_week == 5 && dt.hour >= EA_FridayCloseHour) + { + if(EA_AvoidFridayClose) + for(int p = PositionsTotal()-1; p >= 0; p--) + if(g_ea_position.SelectByIndex(p) && + g_ea_position.Symbol() == _Symbol && + g_ea_position.Magic() == EA_MagicNumber) + g_ea_trade.PositionClose(g_ea_position.Ticket()); + return; + } + if(dt.day_of_week == 1 && !EA_TradeMondayEnabled) return; + if(dt.day_of_week == 2 && !EA_TradeTuesdayEnabled) return; + if(dt.day_of_week == 3 && !EA_TradeWednesdayEnabled) return; + if(dt.day_of_week == 4 && !EA_TradeThursdayEnabled) return; + if(dt.day_of_week == 5 && !EA_TradeFridayEnabled) return; + } + + // ── Guard 4: Spread ────────────────────────────────────────────── + if(EA_EnableSpreadFilter) + { + double spread = (double)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * g_point / g_pipValue; + if(spread > g_workingMaxSpreadPips) return; + if(EA_MaxSpreadATR > 0 && g_cachedATR > 0 && + (double)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * g_point > g_cachedATR * EA_MaxSpreadATR) return; + } + + // ── Guard 5: ATR floor — abnormally quiet market (holidays) ────── + if(g_cachedATR > 0 && g_pipValue > 0) + { + double atrPips = g_cachedATR / g_pipValue; + double floor = 0; + switch(_Period) + { + case PERIOD_M5: floor = 0.5; break; + case PERIOD_M15: floor = 2.5; break; + case PERIOD_H1: floor = 6.0; break; + case PERIOD_H4: floor = 10.0; break; + case PERIOD_D1: floor = 30.0; break; + } + if(floor > 0 && atrPips < floor) return; + } + + // ── Guard 6: Session spike — spread abnormally wide ────────────── + if(SPREAD_BlockHighSpread && g_spreadAnalysis.avgSpread > 0 && _Period < PERIOD_D1) + if((double)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) > g_spreadAnalysis.avgSpread * 1.3) return; + + // ── Guard 7: London/NY session open — M5/M15 only ──────────────── + // First 30 min after London (08:00) and NY (13:30) UTC = liquidity sweeps + // Skip this guard when MTF is STRONG: HTF delivery > session noise + if((_Period == PERIOD_M5 || _Period == PERIOD_M15) && g_workingRequireKillzone) + { + MqlDateTime dt; + TimeToStruct(TimeGMT(), dt); + int mins = dt.hour * 60 + dt.min; + if((mins >= 480 && mins < 510) || (mins >= 810 && mins < 840)) + { + bool strongMTF = (g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH || + g_mtfAnalysis.overallDirection == MTF_STRONG_BEARISH); + if(!strongMTF) return; + } + } + + // ── Guard 8: News ──────────────────────────────────────────────── + if(News_FilterEnabled && g_newsTradingBlocked) return; + + // ── Guard 9: Drawdown limits ───────────────────────────────────── + if(CheckDailyDrawdownLimit()) return; + if(CheckWeeklyTotalDrawdownLimit()) return; + + // ── Guard 10: D1 CHoCH direction ───────────────────────────────── + if(g_workingD1CHoCHGate) + { + UpdateD1CHoChBias(); + if(!g_d1CHoCH_Valid && g_workingBlockNeutral) return; + } + + // ── Daily trade count ──────────────────────────────────────────── + if(g_ea_stats.trades >= EA_MaxDailyTrades) return; + + // ── Same-bar guard ─────────────────────────────────────────────── + static datetime s_lastSignalBar = 0; + datetime curBar = iTime(_Symbol, _Period, 0); + if(curBar == s_lastSignalBar) return; + + // ── Build indicator signals ────────────────────────────────────── + if(EnableSignals) + { + datetime time_a[]; double open_a[], high_a[], low_a[], close_a[]; long tvol_a[]; + ArraySetAsSeries(time_a, true); ArraySetAsSeries(open_a, true); + ArraySetAsSeries(high_a, true); ArraySetAsSeries(low_a, true); + ArraySetAsSeries(close_a, true); ArraySetAsSeries(tvol_a, true); + if(CopyTime (_Symbol,_Period,0,100,time_a) > 0 && + CopyOpen (_Symbol,_Period,0,100,open_a) > 0 && + CopyHigh (_Symbol,_Period,0,100,high_a) > 0 && + CopyLow (_Symbol,_Period,0,100,low_a) > 0 && + CopyClose (_Symbol,_Period,0,100,close_a) > 0 && + CopyTickVolume(_Symbol,_Period,0,100,tvol_a) > 0) + { + UpdateActiveSignals(time_a, high_a, low_a, close_a); + // ── FIX#502: Use scenario-filtered signal generation ────── + // GenerateSignals_ForScenario() calls ONLY the techniques + // allowed by g_scenarioProfile, avoiding wasted computation. + // Falls back to GenerateSignals() if scenario is unknown. + if(g_scenarioProfile.isValid) + GenerateSignals_ForScenario(time_a, open_a, high_a, low_a, close_a, tvol_a); + else + GenerateSignals(time_a, open_a, high_a, low_a, close_a, tvol_a); + } + } + + // ── Count open positions ───────────────────────────────────────── + bool hasBuy = false, hasSell = false; + int sameDirBuy = 0, sameDirSell = 0; + for(int i = PositionsTotal()-1; i >= 0; i--) + { + if(!g_ea_position.SelectByIndex(i)) continue; + if(g_ea_position.Symbol() != _Symbol || (long)g_ea_position.Magic() != EA_MagicNumber) continue; + if(g_ea_position.PositionType() == POSITION_TYPE_BUY) { hasBuy = true; sameDirBuy++; } + if(g_ea_position.PositionType() == POSITION_TYPE_SELL) { hasSell = true; sameDirSell++; } + } + g_ea_has_open_buy = hasBuy; + g_ea_has_open_sell = hasSell; + + // Same-direction cap: 3 legs = 1 logical trade + // If already at limit, block same-direction trades + int logicalBuy = sameDirBuy / 3; + int logicalSell = sameDirSell / 3; + if(logicalBuy >= EA_MaxOpenTrades) { if(g_verboseLog) PrintFormat("[Entry] Max trades: %d/%d BUY open", logicalBuy, EA_MaxOpenTrades); return; } + if(logicalSell >= EA_MaxOpenTrades) { if(g_verboseLog) PrintFormat("[Entry] Max trades: %d/%d SELL open", logicalSell, EA_MaxOpenTrades); return; } + + // ── Select best candidate ──────────────────────────────────────── + int bestIdx = SelectBestCandidate(); + if(bestIdx < 0 || !g_candidates[bestIdx].valid) return; + + // MQL5: cannot take reference to array element — access g_candidates[bestIdx] directly + + // ── Hedge guard ────────────────────────────────────────────────── + if(EA_HedgeMode == HEDGE_NO_HEDGE) + { + if(g_candidates[bestIdx].isBullish && hasSell) return; + if(!g_candidates[bestIdx].isBullish && hasBuy) return; + } + + // ── Choppy + MTF split block ───────────────────────────────────── + // Never trade when regime is choppy AND MTF has no directional edge + if(MTF_Enabled && g_mtfAnalysis.totalTFs >= 2 && g_regimeValid) + { + bool choppy = (g_regimeData.regime == REGIME_CHOPPY || + g_regimeData.regime == REGIME_RANGING || + g_regimeData.regime == REGIME_RANGING_TIGHT || + g_regimeData.regime == REGIME_RANGING_WIDE); + if(choppy) + { + int bTF = g_mtfAnalysis.bullishTFs; + int brTF = g_mtfAnalysis.bearishTFs; + bool edge = (g_candidates[bestIdx].isBullish && bTF >= brTF + 2) || + (!g_candidates[bestIdx].isBullish && brTF >= bTF + 2); + if(!edge) + { + if(g_verboseLog) + PrintFormat("[Entry] Choppy+split block: %s BullTF=%d BearTF=%d (need gap≥2)", + g_candidates[bestIdx].isBullish ? "BUY" : "SELL", bTF, brTF); + return; + } + } + } + + // ── Populate g_ea_signal ───────────────────────────────────────── + g_ea_signal.isBullish = g_candidates[bestIdx].isBullish; + g_ea_signal.type = g_candidates[bestIdx].type; + g_ea_signal.entryPrice = g_candidates[bestIdx].entryPrice; + g_ea_signal.stopLoss = g_candidates[bestIdx].stopLoss; + g_ea_signal.tp1 = g_candidates[bestIdx].tp1; + g_ea_signal.tp2 = g_candidates[bestIdx].tp2; + g_ea_signal.tp3 = g_candidates[bestIdx].tp3; + g_ea_signal.score = g_candidates[bestIdx].totalScore; + g_ea_signal.zoneTop = g_candidates[bestIdx].zoneTop; + g_ea_signal.zoneBottom = g_candidates[bestIdx].zoneBottom; + g_ea_signal.zoneType = g_candidates[bestIdx].zoneType; + + // ── SmartEntry validation ──────────────────────────────────────── + if(SmartEntry_Enabled) + { + g_ea_signal.isValid = true; + SmartEntryDecision sd = EvaluateSmartEntry( + g_candidates[bestIdx].isBullish, g_candidates[bestIdx].entryPrice, g_candidates[bestIdx].stopLoss, + g_candidates[bestIdx].tp1, g_candidates[bestIdx].tp2, g_candidates[bestIdx].tp3, g_candidates[bestIdx].cascade); + g_smartEntry = sd; + if(!sd.shouldEnter) + { + g_ea_signal.isValid = false; + g_ea_signal.score = 0; + if(g_verboseLog) + PrintFormat("[Entry] SmartEntry rejected: %s %s | reason=%s | score=%d | WP=%.0f%% | EV=%.2fR", + g_candidates[bestIdx].type, g_candidates[bestIdx].isBullish ? "BUY" : "SELL", + sd.rejectReason, sd.rawScore, sd.winProbability, sd.expectedValue); + return; + } + g_ea_signal.score = sd.adjustedScore; + g_ea_signal.isCounterTrend = sd.isCounterTrend; + if(PosSize_Enabled) + { + double slPts = MathAbs(g_candidates[bestIdx].entryPrice - g_candidates[bestIdx].stopLoss) / _Point; + if(slPts > 0) + { + CalculatePositionSize(slPts, (int)sd.adjustedScore); + g_smartEntry.recommendedRisk = g_posSizeData.adjustedRiskPercent; + g_smartEntry.recommendedLots = g_posSizeData.finalLotSize; + g_smartEntry.positionMultiplier = g_posSizeData.finalMultiplier; + g_smartEntry.kellyFraction = g_evData.kellyFraction; + } + } + } + + // ── Mark bar done and signal valid ─────────────────────────────── + s_lastSignalBar = curBar; + g_ea_signal.isValid = true; + g_seReEntryReady = false; + if(g_candidates[bestIdx].technique == TECH_TBS && g_candidates[bestIdx].sourceIndex >= 0 && + g_candidates[bestIdx].sourceIndex < ArraySize(g_tbsSetups)) + g_tbsSetups[g_candidates[bestIdx].sourceIndex].status = TBS_ACTIVE; + + PrintFormat("* VALID SIGNAL! Type: %s | Dir: %s | Score: %.0f | Entry: %.5f | SL: %.5f | TP1: %.5f | Candidates: %d", + g_candidates[bestIdx].type, g_candidates[bestIdx].isBullish ? "BUY" : "SELL", g_ea_signal.score, + g_candidates[bestIdx].entryPrice, g_candidates[bestIdx].stopLoss, g_candidates[bestIdx].tp1, g_candidateCount); +} +//+------------------------------------------------------------------+ +//| * v6.40 NEW: Reset Daily Drawdown Tracking at Midnight | +//+------------------------------------------------------------------+ +void ResetDailyDrawdown() +{ + // * v8.0 FIX TIMEZONE: TimeGMT() not TimeCurrent() -- confirmed by backtest log: reset at 01:05/01:15 broker time instead of 00:00 GMT + datetime gmtNow = TimeGMT(); + datetime currentDate = gmtNow - (gmtNow % 86400); // True GMT midnight + // Check if new day + if(currentDate != g_lastDDResetDate) + { + // Save previous day stats + double prevDD = g_currentDailyDD; + bool wasBlocked = g_dailyDDLimitReached; + int blockedCount = g_dailyDDBlockCount; + // Reset for new day + // * FIX#DD_STARTBAL: Cap daily start balance at BacktestInitialBalance. + // PROBLEM: After wins, balance=$21,221. Daily DD reset stores this as start balance. + // Then: dailyLossLimit = $21,221 × 4.5% = $954 — but real FTMO limit is on initial capital. + // EA held 9.45 lots × SL=792pts = $7,493 loss. That's 74% of $10k, but only 35% of $21k + // so the "check" allowed it. Start balance must always reference BacktestInitialBalance. + double _rawDailyBal = AccountInfoDouble(ACCOUNT_BALANCE); + double _rawDailyEq = AccountInfoDouble(ACCOUNT_EQUITY); + g_dailyStartBalance = (g_effectiveBIB > 0 && _rawDailyBal > g_effectiveBIB) + ? g_effectiveBIB : _rawDailyBal; + g_dailyStartEquity = (g_effectiveBIB > 0 && _rawDailyEq > g_effectiveBIB) + ? g_effectiveBIB : _rawDailyEq; + g_lastDDResetDate = currentDate; + g_dailyDDLimitReached = false; + g_currentDailyDD = 0.0; + g_dailyDDBlockedTime = 0; + g_dailyDDBlockCount = 0; + // Log reset + MqlDateTime dt; + TimeToStruct(currentDate, dt); + PrintFormat("==========================================================="); + PrintFormat("[SYNC] DAILY DD RESET | Date: %04d.%02d.%02d", dt.year, dt.mon, dt.day); + PrintFormat(" New Starting Balance: $%.2f", g_dailyStartBalance); + if(wasBlocked) + { + PrintFormat("[OK] Trading RESUMED after DD limit"); + PrintFormat(" Previous DD: %.2f%% | Blocks: %d", prevDD, blockedCount); + } + PrintFormat("==========================================================="); + } +} +//+------------------------------------------------------------------+ +//| * v6.40 NEW: Calculate Current Daily Drawdown | +//+------------------------------------------------------------------+ +double CalculateDailyDrawdown() +{ + double currentBalance = AccountInfoDouble(ACCOUNT_BALANCE); + double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY); + // Use WORST case current level: Lower of balance or equity + // This accounts for both realized losses AND floating losses + double currentLevel = MathMin(currentBalance, currentEquity); + // * v8.03 FIX: Use HIGHEST of startBalance or startEquity as reference + // If day starts with open profitable trades, startEquity > startBalance. + // Using only startBalance UNDERREPORTS the drawdown: + // Example: startBalance=$10,000 startEquity=$10,500 (floating +$500) + // Current equity=$9,500 -> real loss=$1,000 from peak + // OLD: DD = (10000-9500)/10000 = 5.0% <- WRONG + // NEW: DD = (10500-9500)/10500 = 9.52% <- CORRECT (matches FTMO expectation) + // If day starts flat (no open trades): startBalance==startEquity -> no change + if(g_dailyStartBalance > 0) + { + double startLevel = MathMax(g_dailyStartBalance, + g_dailyStartEquity > 0 ? g_dailyStartEquity : g_dailyStartBalance); + double dailyDD = ((startLevel - currentLevel) / startLevel) * 100.0; + return MathMax(0.0, dailyDD); // Never negative + } + return 0.0; +} +//+------------------------------------------------------------------+ +//| * v6.40 NEW: Check if Daily Drawdown Limit Exceeded | +//+------------------------------------------------------------------+ +bool CheckDailyDrawdownLimit() +{ + if(!EA_EnableDrawdownProtection) + return false; // Protection disabled + // Reset if new day + ResetDailyDrawdown(); + // Calculate current DD + g_currentDailyDD = CalculateDailyDrawdown(); + // Check limit + if(g_currentDailyDD >= EA_MaxDailyDrawdownPercent) + { + if(!g_dailyDDLimitReached) // First time hit today + { + g_dailyDDLimitReached = true; + g_dailyDDBlockedTime = TimeCurrent(); + double currentBalance = AccountInfoDouble(ACCOUNT_BALANCE); + double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY); + double lossAmount = g_dailyStartBalance - MathMin(currentBalance, currentEquity); + PrintFormat("==========================================================="); + PrintFormat("[STOP] DAILY DD LIMIT REACHED!"); + PrintFormat(" Start Balance: $%.2f", g_dailyStartBalance); + PrintFormat(" Current Balance: $%.2f", currentBalance); + PrintFormat(" Current Equity: $%.2f", currentEquity); + PrintFormat(" Loss Amount: $%.2f", lossAmount); + PrintFormat(" Daily DD: %.2f%% (Limit: %.2f%%)", + g_currentDailyDD, EA_MaxDailyDrawdownPercent); + // Check if EA should stop completely or just block + if(EA_StopOnDrawdown) + { + PrintFormat("[STOP] STOPPING EA (EA_StopOnDrawdown = true)"); + PrintFormat(" Manual restart required!"); + PrintFormat("==========================================================="); + // Optional: Send email alert + if(EA_EmailOnDrawdown) + { + string subject = "[STOP] Daily DD Limit - EA STOPPED - " + _Symbol; + string body = StringFormat( + "Daily Drawdown Limit Reached - EA STOPPED!\n\n" + "Symbol: %s\n" + "Starting Balance: $%.2f\n" + "Current Equity: $%.2f\n" + "Loss: $%.2f (%.2f%%)\n" + "Limit: %.2f%%\n\n" + "EA has been STOPPED.\n" + "Manual restart required.\n", + _Symbol, g_dailyStartBalance, currentEquity, + lossAmount, g_currentDailyDD, EA_MaxDailyDrawdownPercent + ); + SendMail(subject, body); + } + // * v8.0 FIX: Close ALL open positions before removing EA + // Previously ExpertRemove() left open trades with no trailing/BE management + Print("[STOP] v8.0: Closing all positions before EA removal (DD limit)..."); + for(int _p = PositionsTotal() - 1; _p >= 0; _p--) + { + if(g_ea_position.SelectByIndex(_p)) + { + if(g_ea_position.Symbol() == _Symbol && g_ea_position.Magic() == EA_MagicNumber) + { + g_ea_trade.PositionClose(g_ea_position.Ticket()); + Print("[STOP] Closed position #", g_ea_position.Ticket(), " before EA removal"); + } + } + } + // * FIX#DD_FLUSH: Flush entry groups BEFORE ExpertRemove so stats are saved. + // Without this: trade opened → DD triggered → ExpertRemove → OnDeinit calls + // ForceFlushAllEntryGroups AFTER positions are already closed → totalPnL=0 + // → reported as 0 trades. Must flush here while we still have group data. + ForceFlushAllEntryGroups(); + // Stop EA completely + ExpertRemove(); // Remove EA from chart + } + else + { + PrintFormat("[WARN] BLOCKING SIGNALS (EA_StopOnDrawdown = false)"); + PrintFormat(" Trading blocked until 00:00 GMT"); + PrintFormat(" Will auto-resume next day"); + PrintFormat("==========================================================="); + // Optional: Send email alert + if(EA_EmailOnDrawdown) + { + string subject = "[STOP] Daily DD Limit - Trading Blocked - " + _Symbol; + string body = StringFormat( + "Daily Drawdown Limit Reached!\n\n" + "Symbol: %s\n" + "Starting Balance: $%.2f\n" + "Current Equity: $%.2f\n" + "Loss: $%.2f (%.2f%%)\n" + "Limit: %.2f%%\n\n" + "Trading blocked until 00:00 GMT.\n" + "Will auto-resume next day.\n", + _Symbol, g_dailyStartBalance, currentEquity, + lossAmount, g_currentDailyDD, EA_MaxDailyDrawdownPercent + ); + SendMail(subject, body); + } + } + } + else + { + // Subsequent blocks (already logged first time) + g_dailyDDBlockCount++; + // Log every 100 blocks to avoid spam + if(g_dailyDDBlockCount % 100 == 0) + { + PrintFormat("[STOP] DD limit still active | Blocks: %d | DD: %.2f%%", + g_dailyDDBlockCount, g_currentDailyDD); + } + } + return true; // Limit exceeded - block trading + } + return false; // OK to trade +} +//+------------------------------------------------------------------+ +//| EA_CheckSignals - ΠΛΗΡΗΣ ΔΙΟΡΘΩΜΕΝΗ ΕΚΔΟΣΗ v7.0 | +//| Τοποθεσία: Replace around line ~33900-34800 | +//| Scans ALL techniques, builds candidates, picks best one | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| [v6.41] Reset Weekly Drawdown Tracking (Monday 00:00) | +//+------------------------------------------------------------------+ +void ResetWeeklyDrawdown() +{ + // * v8.01 FIX BUG#4: Removed dead MqlDateTime dt / TimeToStruct(TimeCurrent()) -- dt was never read + // * v8.0 FIX TIMEZONE: Use TimeGMT() for consistent Monday 00:00 GMT detection + datetime gmtCur = TimeGMT(); + datetime weekStart = gmtCur - ((gmtCur + 259200) % 604800); + if(weekStart != g_lastWeeklyResetDate) + { + double prevDD = g_currentWeeklyDD; + bool wasBlocked = g_weeklyDDLimitReached; + // * FIX#410: Cap at BacktestInitialBalance (same as daily FIX#DD_STARTBAL). + { + double _wb = AccountInfoDouble(ACCOUNT_BALANCE); + double _we = AccountInfoDouble(ACCOUNT_EQUITY); + g_weeklyStartBalance = (g_effectiveBIB > 0 && _wb > g_effectiveBIB) + ? g_effectiveBIB : _wb; + g_weeklyStartEquity = (g_effectiveBIB > 0 && _we > g_effectiveBIB) + ? g_effectiveBIB : _we; + } + g_lastWeeklyResetDate = weekStart; + g_weeklyDDLimitReached = false; + g_currentWeeklyDD = 0.0; + PrintFormat("[v6.41] WEEKLY DD RESET | New Start Balance: $%.2f", g_weeklyStartBalance); + if(wasBlocked) + PrintFormat(" Trading RESUMED (prev weekly DD: %.2f%%)", prevDD); + } +} +//+------------------------------------------------------------------+ +//| [v6.41] Check Weekly + Total Drawdown Limits | +#ifdef COMPILE_AS_EA // EA trade execution functions +//+------------------------------------------------------------------+ +bool CheckWeeklyTotalDrawdownLimit() +{ + if(!EA_EnableDrawdownProtection) + return false; + // --- Weekly DD --- + ResetWeeklyDrawdown(); + double currentLevel = MathMin(AccountInfoDouble(ACCOUNT_BALANCE), AccountInfoDouble(ACCOUNT_EQUITY)); + if(g_weeklyStartBalance > 0) + { + // * v8.04 FIX: Use HIGHEST of startBalance or startEquity as reference + // Same fix as daily DD -- if week starts with open profit, startEquity > startBalance. + // Using only startBalance UNDERREPORTS weekly DD (FTMO uses equity-based weekly limit). + double weekStartLevel = MathMax(g_weeklyStartBalance, + g_weeklyStartEquity > 0 ? g_weeklyStartEquity : g_weeklyStartBalance); + g_currentWeeklyDD = MathMax(0.0, ((weekStartLevel - currentLevel) / weekStartLevel) * 100.0); + if(EA_MaxWeeklyDrawdownPercent > 0 && g_currentWeeklyDD >= EA_MaxWeeklyDrawdownPercent) + { + if(!g_weeklyDDLimitReached) + { + g_weeklyDDLimitReached = true; + double weeklyLoss = g_weeklyStartBalance - currentLevel; + PrintFormat("==========================================================="); + PrintFormat("[STOP] WEEKLY DD LIMIT REACHED!"); + PrintFormat(" Week Start Balance: $%.2f", g_weeklyStartBalance); + PrintFormat(" Current Level: $%.2f | Loss: $%.2f", currentLevel, weeklyLoss); + PrintFormat(" Weekly DD: %.2f%% (Limit: %.2f%%)", g_currentWeeklyDD, EA_MaxWeeklyDrawdownPercent); + // * v8.01 FIX BUG#5: Weekly DD now consistent with Daily DD -- email + ExpertRemove + if(EA_EmailOnDrawdown) + { + string subject = "[STOP] Weekly DD Limit - " + (EA_StopOnDrawdown ? "EA STOPPED" : "Trading Blocked") + " - " + _Symbol; + string body = StringFormat( + "Weekly Drawdown Limit Reached!\n\n" + "Symbol: %s\n" + "Week Start Balance: $%.2f\n" + "Current Level: $%.2f\n" + "Loss: $%.2f (%.2f%%)\n" + "Weekly Limit: %.2f%%\n\n" + "%s\n", + _Symbol, g_weeklyStartBalance, currentLevel, + weeklyLoss, g_currentWeeklyDD, EA_MaxWeeklyDrawdownPercent, + EA_StopOnDrawdown ? "EA has been STOPPED. Manual restart required." : "Trading blocked until Monday 00:00 GMT." + ); + SendMail(subject, body); + } + if(EA_StopOnDrawdown) + { + PrintFormat("[STOP] STOPPING EA (EA_StopOnDrawdown = true -- Weekly DD limit)"); + PrintFormat("==========================================================="); + // Close all positions before removing + for(int _p = PositionsTotal() - 1; _p >= 0; _p--) + { + if(g_ea_position.SelectByIndex(_p)) + { + if(g_ea_position.Symbol() == _Symbol && g_ea_position.Magic() == EA_MagicNumber) + { + g_ea_trade.PositionClose(g_ea_position.Ticket()); + Print("[STOP] Closed position #", g_ea_position.Ticket(), " (Weekly DD stop)"); + } + } + } + ExpertRemove(); + } + else + { + PrintFormat("[WARN] BLOCKING SIGNALS (EA_StopOnDrawdown = false)"); + PrintFormat(" Trading blocked until Monday 00:00 GMT"); + PrintFormat("==========================================================="); + } + } + return true; + } + } + // --- Total DD (from peak balance) --- + // * v9.59 FIX#226: Use MathMax(balance,equity) for peak so open profitable + // positions raise the peak; MathMin(balance,equity) already computed as + // currentLevel above so floating losses are included in Total DD. + double currentBalance = AccountInfoDouble(ACCOUNT_BALANCE); + double currentEquityForPeak = AccountInfoDouble(ACCOUNT_EQUITY); + double peakCandidate = MathMax(currentBalance, currentEquityForPeak); + if(peakCandidate > g_peakBalance) + g_peakBalance = peakCandidate; + if(g_peakBalance > 0) + { + g_currentTotalDD = MathMax(0.0, ((g_peakBalance - currentLevel) / g_peakBalance) * 100.0); + if(EA_MaxTotalDrawdownPercent > 0 && g_currentTotalDD >= EA_MaxTotalDrawdownPercent) + { + if(!g_totalDDLimitReached) + { + g_totalDDLimitReached = true; + PrintFormat("TOTAL DD LIMIT! %.2f%% >= %.2f%% | Peak: $%.2f | Now: $%.2f", + g_currentTotalDD, EA_MaxTotalDrawdownPercent, g_peakBalance, currentLevel); + if(EA_StopOnDrawdown) + { + PrintFormat("STOPPING EA (Total DD limit)"); + // * v8.0 FIX: Close all positions before ExpertRemove + for(int _p = PositionsTotal() - 1; _p >= 0; _p--) + { + if(g_ea_position.SelectByIndex(_p)) + { + if(g_ea_position.Symbol() == _Symbol && g_ea_position.Magic() == EA_MagicNumber) + g_ea_trade.PositionClose(g_ea_position.Ticket()); + } + } + ExpertRemove(); + } + } + return true; + } + } + // * v7.4: $ loss limits removed - use DD% instead (unified risk group) + return false; +} + +//+------------------------------------------------------------------+ +//| FIX#383-384: CONFLUENCE DETECTOR + PARTIAL FADE EXIT — v10.64 | +//+------------------------------------------------------------------+ + +//+------------------------------------------------------------------+ +//| FIX#383: MULTI-ZONE CONFLUENCE DETECTOR | +//| Checks if entry price sits inside multiple ICT zones at once. | +//| OB + FVG + Killzone = A+ confluence → score boost + lot scale. | +//| Returns confluence bonus points to add to candidate totalScore. | +//+------------------------------------------------------------------+ +int ComputeZoneConfluenceBonus(int idx) +{ + if(idx < 0 || idx >= g_candidateCount) return 0; + + double entry = g_candidates[idx].entryPrice; + bool isBull = g_candidates[idx].isBullish; + double atr = g_cachedATR; + if(atr <= 0) return 0; + + double proximity = atr * 0.30; // zones must overlap within 0.3×ATR of entry + int zoneCount = 0; + string zonesHit = ""; + + // ── 1. FVG ── + for(int i = 0; i < ArraySize(FVG_Array); i++) + { + if(FVG_Array[i].status != FVG_STATUS_ACTIVE) continue; + if(FVG_Array[i].isBullish != isBull) continue; + if(entry >= FVG_Array[i].bottom - proximity && + entry <= FVG_Array[i].top + proximity) + { + zoneCount++; + zonesHit += "FVG "; + break; + } + } + + // ── 2. Order Block ── + for(int i = 0; i < ArraySize(OB_Array); i++) + { + if(!OB_Array[i].active || OB_Array[i].mitigated) continue; + if(OB_Array[i].isBullish != isBull) continue; + if(entry >= OB_Array[i].bottom - proximity && + entry <= OB_Array[i].top + proximity) + { + zoneCount++; + zonesHit += "OB "; + break; + } + } + + // ── 3. OTE Zone ── + for(int i = 0; i < ArraySize(OTE_Array); i++) + { + if(!OTE_Array[i].active || !OTE_Array[i].isValid) continue; + if(OTE_Array[i].isBullish != isBull) continue; + double oteTop = MathMax(OTE_Array[i].level786, OTE_Array[i].level618); + double oteBot = MathMin(OTE_Array[i].level786, OTE_Array[i].level618); + if(entry >= oteBot - proximity && entry <= oteTop + proximity) + { + zoneCount++; + zonesHit += "OTE "; + break; + } + } + + // ── 4. Active Killzone ── + if(g_isInKillzone) + { + zoneCount++; + zonesHit += "KZ "; + } + + // ── 5. Unswept Liquidity within 1.0×ATR (confirms institutional interest) ── + for(int i = 0; i < ArraySize(LIQ_Array); i++) + { + if(!LIQ_Array[i].isValid || LIQ_Array[i].swept) continue; + bool liqAligned = isBull ? !LIQ_Array[i].isBSL // SSL swept = demand below + : LIQ_Array[i].isBSL; // BSL swept = supply above + if(liqAligned && MathAbs(LIQ_Array[i].price - entry) < atr * 1.0) + { + zoneCount++; + zonesHit += "LIQ "; + break; + } + } + + if(zoneCount < 2) return 0; // Need at least 2 zones + + // Score bonus: 2 zones = +10, 3 zones = +20, 4+ zones = +30 + int bonus = 0; + if(zoneCount >= 4) bonus = 30; + else if(zoneCount == 3) bonus = 20; + else bonus = 10; + + if(g_verboseLog) + PrintFormat("[FIX#383] Confluence: %d zones [%s] on %s %s → +%d pts", + zoneCount, zonesHit, + g_candidates[idx].isBullish ? "BUY" : "SELL", + EnumToString(g_candidates[idx].technique), + bonus); + + return bonus; +} + +//+------------------------------------------------------------------+ +//| FIX#384: PARTIAL CLOSE ON MOMENTUM FADE | +//| While in a profitable trade, if momentum is visibly weakening | +//| (RSI diverging, candle bodies shrinking, ATR contracting), | +//| close 50% of the remaining position to lock in partial profit. | +//| The remaining 50% runs with the structure trail (FIX#382). | +//| Fires ONCE per trade — guarded by partialCloseDone flag. | + + +//+------------------------------------------------------------------+ +//| FIX#378-382: MARKET CONTEXT + SMART EXIT 2.0 — v10.64 | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| FIX#378: COMPUTE MARKET CONTEXT | +//| Reads D1+H4 structure, ADX, MTF, regime to determine phase. | +//| Called once per bar at start of EA_CheckSignals. | +//| Sets allowBuy/allowSell and technique multipliers. | +//+------------------------------------------------------------------+ +MarketContext ComputeMarketContext() +{ + MarketContext ctx; + ZeroMemory(ctx); + ctx.computedAt = TimeCurrent(); + ctx.valid = false; + + // Default: all techniques allowed, neutral multipliers + ctx.allowBuy = true; + ctx.allowSell = true; + ctx.useFVG = true; ctx.useOB = true; ctx.useTC = true; + ctx.useBOS = true; ctx.useOTE = true; ctx.useLIQ = true; + ctx.fvgScoreMult = 1.0; ctx.obScoreMult = 1.0; ctx.tcScoreMult = 1.0; + ctx.bosScoreMult = 1.0; ctx.oteScoreMult = 1.0; ctx.liqScoreMult = 1.0; + ctx.contextConfidence = 50; + + // --- Read state --- + // FIX#505f: use g_mtfAnalysis.overallDirection (D1-macro-corrected) + bool mtfBull = (g_mtfAnalysis.overallDirection == MTF_BULLISH || + g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH); + bool mtfBear = (g_mtfAnalysis.overallDirection == MTF_BEARISH || + g_mtfAnalysis.overallDirection == MTF_STRONG_BEARISH); + bool mtfStrBull= (g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH); + bool mtfStrBear= (g_mtfAnalysis.overallDirection == MTF_STRONG_BEARISH); + bool structBull= g_isBullishStructure; + double adx = g_cachedADX; + bool trending = (adx > 25.0); + bool strongTrend=(adx > 40.0); + bool ranging = (adx < 20.0); + ENUM_MARKET_REGIME reg = g_regimeData.regime; + bool regBull = (reg == REGIME_TREND_UP || reg == REGIME_STRONG_TREND_UP || reg == REGIME_WEAK_TREND_UP); + bool regBear = (reg == REGIME_TREND_DOWN || reg == REGIME_STRONG_TREND_DOWN || reg == REGIME_WEAK_TREND_DOWN); + bool regChop = (reg == REGIME_CHOPPY || reg == REGIME_RANGING || reg == REGIME_RANGING_TIGHT || reg == REGIME_RANGING_WIDE); + + // ── FIX#501: CORRECT PHASE CLASSIFICATION USING HTF RANGE SCORE ── + // Problem: current-TF ADX alone misclassifies the market. + // Dec2024: H1 ADX=22 → ranging=false, trending=false → DISTRIBUTION → allowSell=true everywhere + // Reality: H4+D1 were ranging → should be RANGING → allowSell only at premium + // + // Fix: g_rangeConfScore uses 3-TF hierarchy (H4+D1 ADX, MA slope, rangeWidth, volContraction). + // Score >= 3: HTF confirmed ranging → force ranging=true, trending=false + // Score == 2: HTF probable ranging → force ranging=true if current TF not strongly trending + // Score 0-1: no ranging signal → keep original ADX-based values + // strongTrend (ADX>40) always wins → impulse overrides range context + if(!strongTrend) + { + if(g_rangeConfScore >= 3) + { + // Confirmed: 3+ independent levels agree market is ranging + ranging = true; + trending = false; + if(g_verboseLog) + PrintFormat("[FIX#501] Phase override: RangeConf=%d/5 ADX=%.0f → forced RANGING", g_rangeConfScore, adx); + } + else if(g_rangeConfScore == 2 && adx < 35.0) + { + // Probable: 2 levels agree, current TF not strongly trending + ranging = true; + trending = false; + if(g_verboseLog) + PrintFormat("[FIX#501] Phase override: RangeConf=%d/5 ADX=%.0f<35 → forced RANGING", g_rangeConfScore, adx); + } + } + + // --- Determine Phase --- + if(strongTrend && mtfStrBull && structBull && regBull) + { + ctx.phase = MPHASE_MARKUP; + ctx.phaseDescription = "Markup (strong bull)"; + ctx.allowBuy = true; ctx.allowSell = false; + // In markup: TC + BOS are primary, OB/FVG pullback entries valid + ctx.tcScoreMult = 1.30; + ctx.bosScoreMult = 1.20; + ctx.obScoreMult = 1.10; + ctx.fvgScoreMult = 1.10; + ctx.oteScoreMult = 0.80; // OTE less reliable in strong trend + ctx.liqScoreMult = 1.20; + ctx.contextConfidence = 85; + } + else if(strongTrend && mtfStrBear && !structBull && regBear) + { + ctx.phase = MPHASE_MARKDOWN; + ctx.phaseDescription = "Markdown (strong bear)"; + ctx.allowBuy = false; ctx.allowSell = true; + ctx.tcScoreMult = 1.30; + ctx.bosScoreMult = 1.20; + ctx.obScoreMult = 1.10; + ctx.fvgScoreMult = 1.10; + ctx.oteScoreMult = 0.80; + ctx.liqScoreMult = 1.20; + ctx.contextConfidence = 85; + } + else if(trending && mtfBull && structBull) + { + ctx.phase = MPHASE_MARKUP; + ctx.phaseDescription = "Markup (moderate bull)"; + ctx.allowBuy = true; ctx.allowSell = false; + ctx.tcScoreMult = 1.15; + ctx.bosScoreMult = 1.10; + ctx.obScoreMult = 1.05; + ctx.fvgScoreMult = 1.05; + ctx.contextConfidence = 70; + } + else if(trending && mtfBear && !structBull) + { + ctx.phase = MPHASE_MARKDOWN; + ctx.phaseDescription = "Markdown (moderate bear)"; + ctx.allowBuy = false; ctx.allowSell = true; + ctx.tcScoreMult = 1.15; + ctx.bosScoreMult = 1.10; + ctx.obScoreMult = 1.05; + ctx.fvgScoreMult = 1.05; + ctx.contextConfidence = 70; + } + // ranging=true can come from: (a) current ADX<20, or (b) FIX#501 HTF override above. + // In case (b), regChop may still say WEAK_TREND (current TF regime hasn't caught up). + // Solution: fire RANGING phase on ranging=true alone — the HTF score is the authoritative signal. + else if(ranging) + { + ctx.phase = MPHASE_RANGING; + // Distinguish: confirmed HTF range vs current-TF-only ranging + ctx.phaseDescription = (g_rangeConfScore >= 2) ? "Ranging (HTF confirmed)" : "Ranging / Choppy"; + ctx.allowBuy = true; ctx.allowSell = true; + // In ranging: OB + FVG mean-reversion dominant, TC useless + ctx.fvgScoreMult = 1.25; + ctx.obScoreMult = 1.25; + ctx.oteScoreMult = 1.15; + ctx.tcScoreMult = 0.60; // TC loses in ranging + ctx.bosScoreMult = 0.70; + ctx.liqScoreMult = 1.10; + // Higher confidence when HTF confirms — drives Gate 4 threshold (needs >= 65) + ctx.contextConfidence = (g_rangeConfScore >= 3) ? 75 : + (g_rangeConfScore >= 2) ? 70 : 65; + } + else if(reg == REGIME_BREAKOUT) + { + // Breakout direction based on MTF + if(mtfBull || structBull) + { + ctx.phase = MPHASE_BREAKOUT_BULL; + ctx.phaseDescription = "Bullish Breakout"; + ctx.allowBuy = true; ctx.allowSell = false; + } + else + { + ctx.phase = MPHASE_BREAKOUT_BEAR; + ctx.phaseDescription = "Bearish Breakout"; + ctx.allowBuy = false; ctx.allowSell = true; + } + ctx.bosScoreMult = 1.40; // BOS is the primary technique in breakouts + ctx.liqScoreMult = 1.30; + ctx.tcScoreMult = 1.20; + ctx.fvgScoreMult = 0.80; // FVGs unreliable during breakout + ctx.contextConfidence = 75; + } + else if(!trending && !ranging) + { + // Transition / indeterminate — use structure as tiebreaker + if(structBull && (mtfBull || !mtfBear)) + { + ctx.phase = MPHASE_ACCUMULATION; + ctx.phaseDescription = "Accumulation (ranging-low / pre-bull)"; + ctx.allowBuy = true; ctx.allowSell = false; + ctx.fvgScoreMult = 1.15; + ctx.obScoreMult = 1.15; + ctx.liqScoreMult = 1.20; // sweeps common in accumulation + ctx.tcScoreMult = 0.75; + ctx.contextConfidence = 55; + } + else if(!structBull && (mtfBear || !mtfBull)) + { + ctx.phase = MPHASE_DISTRIBUTION; + ctx.phaseDescription = "Distribution (ranging-high / pre-bear)"; + ctx.allowBuy = false; ctx.allowSell = true; + ctx.fvgScoreMult = 1.15; + ctx.obScoreMult = 1.15; + ctx.liqScoreMult = 1.20; + ctx.tcScoreMult = 0.75; + ctx.contextConfidence = 55; + } + else + { + ctx.phase = MPHASE_UNKNOWN; + ctx.phaseDescription = "Indeterminate — all techniques"; + ctx.contextConfidence = 35; + } + } + else + { + ctx.phase = MPHASE_UNKNOWN; + ctx.phaseDescription = "Unknown / Mixed signals"; + ctx.contextConfidence = 35; + } + + // --- Find nearest structural levels for TP targeting --- + double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + double atr = g_cachedATR; + ctx.nearestResistance = 0; + ctx.nearestSupport = 0; + double bestResD = DBL_MAX, bestSupD = DBL_MAX; + int sz = ArraySize(STRUCT_Array); + for(int i = sz - 1; i >= MathMax(0, sz - 120); i--) + { + if(!STRUCT_Array[i].isValid || STRUCT_Array[i].broken) continue; + if(STRUCT_Array[i].age > 150) continue; + double sp = STRUCT_Array[i].price; + if(STRUCT_Array[i].isHigh && sp > bid + atr * 0.3) + { + double d = sp - bid; + if(d < bestResD) { bestResD = d; ctx.nearestResistance = sp; } + } + else if(!STRUCT_Array[i].isHigh && sp < bid - atr * 0.3) + { + double d = bid - sp; + if(d < bestSupD) { bestSupD = d; ctx.nearestSupport = sp; } + } + } + + ctx.valid = true; + + // ── FIX#501: MTF RANGE OVERRIDE — final scan before entry gate ────── + // This is the architectural "market scan" step: + // After the phase is determined from current-TF signals, we cross-check + // against the HTF context (H4 + D1 for H1 EA) to detect the case where + // current TF appears to be trending BUT the higher TFs are ranging. + // + // Framework scenarios (applied here as hard allowBuy/allowSell overrides): + // + // DANGEROUS (D1 trending + H4 ranging): + // → D1 direction ONLY, and ONLY at H4 premium/discount boundary + // → e.g. D1=BEAR, H4=ranging: SELL only at H4 premium (>60%) + // → NEVER sell at H4 discount — that's where bounces happen + // + // IDEAL (D1 ranging + H4 ranging): + // → Both directions allowed, but ONLY at range boundaries + // → SELL at premium, BUY at discount + // → Middle zone (40-60%): no new entries + // + // ACCEPTABLE (D1 ranging + H4 trending): + // → H4 direction only (existing phase logic handles this correctly) + // → No override needed + // + // Gate fires only when g_rangeConfScore >= 2 (L1a OR L1b confirmed ranging) + // and range width is meaningful (> 1.5 ATR to avoid noise) + if(g_rangeConfScore >= 2 && g_regimeData.valid && + g_regimeData.rangeHigh > g_regimeData.rangeLow && g_cachedATR > 0) + { + double _rH = g_regimeData.rangeHigh; + double _rL = g_regimeData.rangeLow; + double _rW = _rH - _rL; + + if(_rW > g_cachedATR * 1.5) // meaningful range — ignore noise + { + double _bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + double _pos = (_bid - _rL) / _rW; // 0.0 = range bottom, 1.0 = range top + + // Thresholds: confirmed range (score≥3) uses tighter zones + double _sellBlock = (g_rangeConfScore >= 3) ? 0.40 : 0.30; + double _buyBlock = (g_rangeConfScore >= 3) ? 0.60 : 0.70; + + // FIX#506 Bug#1: use g_d1CHoCH_Valid/Bear/Bull (same source as D1 hard gate). + // OLD bug: _d1Bear = _d1Trending && !g_isBullishStructure used H1 local structure + // → conflict with D1 hard gate that uses g_d1CHoCH → BUY=N,SELL=N deadlock. + bool _d1Bear = (g_d1CHoCH_Valid && g_d1CHoCH_Bear); + bool _d1Bull = (g_d1CHoCH_Valid && g_d1CHoCH_Bull); + + string _overrideReason = ""; + + if(_pos < _sellBlock) + { + // Price at DISCOUNT (range bottom) — block SELL + // ICT: never sell at discount. Price here bounces UP. + if(ctx.allowSell) + { + ctx.allowSell = false; + _overrideReason = StringFormat("SELL-BLOCKED discount %.0f%%<%.0f%%", _pos*100, _sellBlock*100); + } + // If D1 is NOT trending bear, can allow BUY at discount + if(!_d1Bear && !ctx.allowBuy) + ctx.allowBuy = true; + } + else if(_pos > _buyBlock) + { + // Price at PREMIUM (range top) — block BUY + // ICT: never buy at premium. Price here reverses DOWN. + if(ctx.allowBuy) + { + ctx.allowBuy = false; + _overrideReason = StringFormat("BUY-BLOCKED premium %.0f%%>%.0f%%", _pos*100, _buyBlock*100); + } + // If D1 is NOT trending bull, can allow SELL at premium + if(!_d1Bull && !ctx.allowSell) + ctx.allowSell = true; + } + else + { + // Middle zone (40-60%): no edge for either direction + // Both blocked — wait for price to reach a boundary + if(g_rangeConfScore >= 3) + { + ctx.allowBuy = false; + ctx.allowSell = false; + _overrideReason = StringFormat("BOTH-BLOCKED middle %.0f%% (range 40-60%%)", _pos*100); + } + } + + // D1 trending bear override: even at premium, no BUY allowed + if(_d1Bear && ctx.allowBuy) + { + ctx.allowBuy = false; + if(_overrideReason == "") + _overrideReason = "BUY-BLOCKED D1-trending-bear"; + } + // D1 trending bull override: even at discount, no SELL allowed + if(_d1Bull && ctx.allowSell) + { + ctx.allowSell = false; + if(_overrideReason == "") + _overrideReason = "SELL-BLOCKED D1-trending-bull"; + } + + // Ensure confidence is high enough that Gate 4 fires (>= 65) + if(_overrideReason != "") + { + ctx.contextConfidence = MathMax(ctx.contextConfidence, 70); + ctx.phaseDescription = ctx.phaseDescription + " [" + _overrideReason + "]"; + if(g_verboseLog) + PrintFormat("[FIX#501] MarketCtx override | RangeConf=%d/5 pos=%.0f%% | %s | allow: BUY=%s SELL=%s", + g_rangeConfScore, _pos*100, _overrideReason, + ctx.allowBuy ? "Y":"N", ctx.allowSell ? "Y":"N"); + } + } + } + + // ── FIX#506: D1 HARD GATE ───────────────────────────────────────────── + // This is the SINGLE point where D1 macro direction constrains allowBuy/allowSell. + // Runs AFTER all phase detection (MARKUP/MARKDOWN/RANGING/ACCUM/DIST) and + // AFTER FIX#501 range position scan — so it cannot be overridden by any of them. + // + // ICT principle: D1 is the macro context. H1 phase shows HOW price moves + // within that context — not whether to trade against it. + // D1=BEAR: MARKUP on H1 = pullback bounce. SELL at the bounce. Never BUY. + // D1=BULL: MARKDOWN on H1 = pullback dip. BUY at the dip. Never SELL. + // D1=neutral (g_d1CHoCH_Valid=false): no constraint. Phase logic decides. + // + // Replaces: FIX#505 override, FIX#505c, FIX#505d. + // Single source of truth: g_d1CHoCH_Valid + g_d1CHoCH_Bear/Bull (CHoCH structural). + if(g_d1CHoCH_Valid) + { + if(g_d1CHoCH_Bear && ctx.allowBuy) + { + ctx.allowBuy = false; + ctx.phaseDescription = ctx.phaseDescription + " [D1=BEAR:no-buy]"; + if(g_verboseLog) + PrintFormat("[FIX#506] D1 hard gate: D1=BEAR → allowBuy=false | phase=%s", + ctx.phaseDescription); + } + if(g_d1CHoCH_Bull && ctx.allowSell) + { + ctx.allowSell = false; + ctx.phaseDescription = ctx.phaseDescription + " [D1=BULL:no-sell]"; + if(g_verboseLog) + PrintFormat("[FIX#506] D1 hard gate: D1=BULL → allowSell=false | phase=%s", + ctx.phaseDescription); + } + } + + // ── FIX#507: AMD PHASE GATE ─────────────────────────────────────────── + // AMD (Accumulation-Manipulation-Distribution) provides session-based + // context that overrides phase logic for entries. + // + // ACCUMULATION (Asian tight range): + // Direction unknown — institutions are building positions. + // No entries: AMD_AvoidAccum input controls this. + // + // MANIPULATION (sweep of Asian range): + // highSwept=true → fake breakout UP → real move = DOWN → SELL only + // lowSwept=true → fake breakout DOWN → real move = UP → BUY only + // This is the ICT Judas Swing / Stop Hunt setup — highest quality entry. + // + // DISTRIBUTION (price moving toward target): + // Early (<33%): enforce direction per distDirection + // Late (>75%): near target → avoid new entries (reversal risk) + // Transition (>100%): cycle complete → no entries + if(AMD_Enabled && g_amdData.valid) + { + switch(g_amdData.phase) + { + case AMD_ACCUMULATION: + if(AMD_AvoidAccum) + { + ctx.allowBuy = false; + ctx.allowSell = false; + ctx.phaseDescription = ctx.phaseDescription + " [AMD:Accum-avoid]"; + if(g_verboseLog) + Print("[FIX#507] AMD=ACCUMULATION → no entries (AMD_AvoidAccum=true)"); + } + break; + + case AMD_MANIPULATION: + { + // Direction is known: opposite of the sweep + bool _amdSell = (g_amdData.distDirection == CRT_BEARISH); // highSwept → SELL + bool _amdBuy = (g_amdData.distDirection == CRT_BULLISH); // lowSwept → BUY + if(AMD_TradeInManip) + { + // Lock direction to AMD manipulation direction + if(_amdSell && ctx.allowBuy) + { + ctx.allowBuy = false; + ctx.phaseDescription = ctx.phaseDescription + " [AMD:Manip-SELL]"; + } + if(_amdBuy && ctx.allowSell) + { + ctx.allowSell = false; + ctx.phaseDescription = ctx.phaseDescription + " [AMD:Manip-BUY]"; + } + if(g_verboseLog) + PrintFormat("[FIX#507] AMD=MANIPULATION %s → allow: BUY=%s SELL=%s", + _amdSell ? "SELL" : "BUY", + ctx.allowBuy ? "Y" : "N", ctx.allowSell ? "Y" : "N"); + } + break; + } + + case AMD_DISTRIBUTION: + { + bool _amdSell = (g_amdData.distDirection == CRT_BEARISH); + bool _amdBuy = (g_amdData.distDirection == CRT_BULLISH); + if(AMD_TradeInDist) + { + // Late or complete: near target — avoid new entries + if(g_amdData.distProgress >= 75.0) + { + ctx.allowBuy = false; + ctx.allowSell = false; + ctx.phaseDescription = ctx.phaseDescription + " [AMD:Dist-late]"; + if(g_verboseLog) + PrintFormat("[FIX#507] AMD=DISTRIBUTION LATE (%.0f%%) → no entries", + g_amdData.distProgress); + } + else + { + // Early/middle: enforce direction + if(_amdSell && ctx.allowBuy) + { + ctx.allowBuy = false; + ctx.phaseDescription = ctx.phaseDescription + " [AMD:Dist-SELL]"; + } + if(_amdBuy && ctx.allowSell) + { + ctx.allowSell = false; + ctx.phaseDescription = ctx.phaseDescription + " [AMD:Dist-BUY]"; + } + if(g_verboseLog) + PrintFormat("[FIX#507] AMD=DISTRIBUTION %.0f%% %s → allow: BUY=%s SELL=%s", + g_amdData.distProgress, + _amdSell ? "SELL" : "BUY", + ctx.allowBuy ? "Y" : "N", ctx.allowSell ? "Y" : "N"); + } + } + break; + } + + default: break; + } + } + + return ctx; +} + +// ══════════════════════════════════════════════════════════════════════ +// FIX#502: SCENARIO PROFILE — SECTION 10 EXTENSION +// DeriveScenarioProfile() maps the current MarketContext phase to one of +// the 12 concrete trading scenarios, setting entry/SL/TP methods and +// which ICT techniques are allowed for that scenario. +// +// Helper functions use only globals already populated by RunSharedAnalysis: +// g_regimeData, g_cachedATR, g_cachedRSI, g_rangeConfScore, +// g_isBullishStructure, g_lastCHoCHTime, OTE_Array, g_mtfAnalysis +// ══════════════════════════════════════════════════════════════════════ + +// ── Helper: Is price in a pullback zone (Fib 38-79% of last swing)? ── +// Proxy: OTE zone is active AND aligned with current structure direction. +// OTE zones are pre-computed by DetectOTEZones() in RunSharedAnalysis. +bool IsPullbackActive() +{ + double price = SymbolInfoDouble(_Symbol, SYMBOL_BID); + int n = ArraySize(OTE_Array); + for(int i = 0; i < n; i++) + { + if(!OTE_Array[i].isValid || !OTE_Array[i].active) continue; + // OTE zone must be aligned with current structure direction + if(OTE_Array[i].isBullish != g_isBullishStructure) continue; + double oteTop = MathMax(OTE_Array[i].level618, OTE_Array[i].level786); + double oteBottom = MathMin(OTE_Array[i].level618, OTE_Array[i].level786); + // Price must be inside or just above/below the OTE zone + double margin = g_cachedATR * 0.5; + if(price >= oteBottom - margin && price <= oteTop + margin) + return true; + } + return false; +} + +// ── Helper: Is there a flag/consolidation pattern inside current trend? ── +// Proxy: MARKUP/MARKDOWN phase + ATR contracting + tight range width. +// Flag = price compressed after a strong move, before continuation. +bool IsFlagPattern() +{ + // Need to be in a trend + bool inTrend = (g_regimeData.regime == REGIME_TREND_UP || + g_regimeData.regime == REGIME_TREND_DOWN || + g_regimeData.regime == REGIME_WEAK_TREND_UP|| + g_regimeData.regime == REGIME_WEAK_TREND_DOWN); + if(!inTrend) return false; + // Flag = vol contracting (ATR squeezing) inside trend + if(!g_regimeData.isVolContracting) return false; + // Range must be tight — flag is narrow consolidation, not full ranging + // rangeWidthATR < 2.5 = tight flag; > 4.0 = full range (not a flag) + if(g_regimeData.rangeWidthATR <= 0 || g_regimeData.rangeWidthATR > 2.5) return false; + return true; +} + +// ── Helper: Is price retesting a recently broken level? ── +// Proxy: regime just transitioned to BREAKOUT + price near rangeHigh/rangeLow. +// Retest = price returns to test the boundary it just broke through. +bool IsRetestInProgress() +{ + if(g_regimeData.regime != REGIME_BREAKOUT) return false; + if(g_regimeData.rangeHigh <= g_regimeData.rangeLow) return false; + double price = SymbolInfoDouble(_Symbol, SYMBOL_BID); + double margin = g_cachedATR * 1.0; // ±1 ATR around broken level + bool nearHigh = (price >= g_regimeData.rangeHigh - margin && + price <= g_regimeData.rangeHigh + margin); + bool nearLow = (price >= g_regimeData.rangeLow - margin && + price <= g_regimeData.rangeLow + margin); + return (nearHigh || nearLow); +} + +// ── Helper: Is this a fakeout (false breakout back into range)? ── +// Proxy: FIX#501 TRAP condition — price poked outside range but +// current bid is back inside, and volume is not expanding. +bool IsFakeoutCondition() +{ + if(g_rangeConfScore < 2) return false; // Need confirmed range context + if(g_regimeData.rangeHigh <= g_regimeData.rangeLow) return false; + double price = SymbolInfoDouble(_Symbol, SYMBOL_BID); + // Price must be INSIDE the range now (returned after poke) + bool insideRange = (price > g_regimeData.rangeLow + g_cachedATR * 0.1 && + price < g_regimeData.rangeHigh - g_cachedATR * 0.1); + if(!insideRange) return false; + // No strong volume expansion (fakeout = weak move) + if(g_regimeData.isVolExpanding) return false; + // barsInRegime must be recent — fakeout resolves quickly + return (g_regimeData.barsInRegime <= 3); +} + +// ── Helper: Is this a range compression setup (coiling spring)? ── +// Proxy: RANGING + isVolContracting + rangeWidthATR shrinking. +// rangeWidthATR < 2.0 = very tight — energy building for breakout. +bool IsCompressionSetup() +{ + bool ranging = (g_regimeData.regime == REGIME_RANGING_TIGHT || + g_regimeData.regime == REGIME_RANGING_WIDE || + g_rangeConfScore >= 3); + if(!ranging) return false; + if(!g_regimeData.isVolContracting) return false; + // Very tight range = compression + return (g_regimeData.rangeWidthATR > 0 && g_regimeData.rangeWidthATR < 2.0); +} + +// ── Helper: Was there a recent CHoCH (Change of Character = reversal signal)? ── +// Uses g_lastCHoCHTime — set by DetectMarketStructure in RunSharedAnalysis. +bool HasRecentCHoCH() +{ + if(g_lastCHoCHTime <= 0) return false; + // CHoCH window is TF-relative: equivalent to ~1 hour on any timeframe. + // Hard 5-bar window was too narrow on H1/H4 — valid reversals expired before + // the scenario gate could act on them. + // Floor of 5 bars preserved for fast TFs (M1/M5) where 1hr = many bars. + int barsPerHour = (int)MathMax(1, 3600 / PeriodSeconds(_Period)); + int window = MathMax(5, barsPerHour); + int barsSince = (int)((TimeCurrent() - g_lastCHoCHTime) / PeriodSeconds(_Period)); + return (barsSince <= window); +} + +// ── Main: DeriveScenarioProfile ─────────────────────────────────────── +// Called once per bar in EA_CheckSignals, immediately after ComputeMarketContext. +// Maps ctx.phase + helper conditions → ScenarioProfile with: +// - scenario enum (which of the 12) +// - entryStyle, slMethod, tpMethod +// - which techniques are allowed (gates for AddCandidate) +// - isValid (false = choppy/unknown → EA_CheckSignals returns immediately) +ScenarioProfile DeriveScenarioProfile(const MarketContext &ctx) +{ + ScenarioProfile sp; + ZeroMemory(sp); + sp.scenario = SCEN_UNKNOWN; + sp.entryStyle = ES_RETEST; + sp.slMethod = SL_ATR; + sp.tpMethod = TP_FIXED_RR; + sp.minRR = g_workingMinRiskReward > 0 ? g_workingMinRiskReward : 1.5; + sp.direction = 0; + sp.confidence = 0; + sp.isValid = false; + + switch(ctx.phase) + { + // ── MARKUP / MARKDOWN (trending) ────────────────────────────── + case MPHASE_MARKUP: + case MPHASE_MARKDOWN: + { + sp.direction = (ctx.phase == MPHASE_MARKUP) ? 1 : -1; + bool _isBullTrend = (ctx.phase == MPHASE_MARKUP); + + // ── FIX#503b: TREND EXHAUSTION GATE (uses PEAK, not raw score) ─── + // ROOT: Dec 20 2023 — FOMC +130p rally, EA entered 4 BUY at top. + // FIX#503b: g_exhaustionPeak = max of last 4 bars — more robust. + // If score was rising (2→2→3), peak=3 catches it even when raw=2. + // Score 0-1: healthy → proceed normally + // Score 2 : caution → OB/FVG only, confidence -20%, TC/BOS off + // Score 3 : weakening → pullback only, else WAIT + // Score 4-5: overextended → if CHoCH: Reversal, else WAIT + // Also uses g_exhaustionTrend (weighted score) for combined threshold. + + // FIX#511: rangeConfScore threshold raised 2→4 (confirmed range only, not probable). + // rangeConfScore=2 is "probable range" — too early to block trend entries. + // Only confirmed range (score=4+) combined with exhaustion justifies a WAIT. + if(g_exhaustionPeak >= 4 || + (g_exhaustionPeak >= 3 && g_exhaustionTrend > 0.45) || + (g_exhaustionPeak >= 3 && g_rangeConfScore >= 4)) + { + // OVEREXTENDED: trend has run far, momentum gone + // Check for Reversal setup in the OPPOSITE direction + if(HasRecentCHoCH()) + { + // CHoCH fired → confirmed structure flip → Reversal + // Note: exhaustionScore IS the regime weakness — no need for WEAK_TREND gate + sp.scenario = SCEN_REVERSAL; + sp.direction = _isBullTrend ? -1 : 1; // OPPOSITE to trend + sp.entryStyle = ES_RETEST; + sp.slMethod = SL_STRUCTURE; + sp.tpMethod = TP_PREV_HIGHLOW; + sp.allowTC = false; + sp.allowFVG = true; + sp.allowOB = true; + sp.allowBOS = true; + sp.allowOTE = true; + sp.allowLIQ = true; + sp.allowMEANREV = false; + sp.minRR = sp.minRR * 1.2; + sp.confidence = 70; // CHoCH confirmed = higher conviction than normal Reversal + sp.description = _isBullTrend ? "Exhausted Bull→Reversal Sell" : "Exhausted Bear→Reversal Buy"; + sp.isValid = true; + } + else + { + // No CHoCH yet → Scenario 11 transition: market looking for direction + // WAIT — do not enter in either direction + sp.scenario = SCEN_CHOPPY; + sp.isValid = false; + sp.description = "Overextended Trend — wait for CHoCH/structure"; + } + break; + } + else if(g_exhaustionPeak == 3) + { + // WEAKENING: trend valid but losing steam + // Allow pullback entries only — no momentum/breakout chasing + if(IsPullbackActive()) + { + sp.scenario = SCEN_TREND_PULLBACK; + sp.entryStyle = ES_FIB; + sp.slMethod = SL_FIB78; + sp.tpMethod = TP_PREV_HIGHLOW; + sp.allowTC = false; // No momentum entries in weakening trend + sp.allowFVG = true; + sp.allowOB = true; + sp.allowBOS = false; + sp.allowOTE = true; + sp.allowLIQ = true; + sp.allowMEANREV = false; + sp.confidence = 60; // Lower conviction — trend weakening + sp.description = _isBullTrend ? "Weakening Bull Pullback" : "Weakening Bear Pullback"; + sp.isValid = true; + } + else + { + // No pullback setup — degrade to low-conviction trend entry + // instead of full WAIT. Precision tools only, no momentum. + sp.scenario = SCEN_TREND_PULLBACK; + sp.entryStyle = ES_RETEST; + sp.slMethod = SL_STRUCTURE; + sp.tpMethod = TP_PREV_HIGHLOW; + sp.allowTC = false; + sp.allowFVG = true; + sp.allowOB = true; + sp.allowBOS = false; + sp.allowOTE = true; + sp.allowLIQ = true; + sp.allowMEANREV = false; + sp.confidence = 45; + sp.isValid = true; + sp.description = _isBullTrend ? "Weakening Bull [low-conv]" : "Weakening Bear [low-conv]"; + } + break; + } + else if(g_exhaustionPeak == 2) + { + // CAUTION: proceed but with restrictions + // TC (momentum) and BOS blocked — only OB/FVG with confirmation + // Falls through to normal logic below with reduced confidence + sp.confidence = MathMax(50, ctx.contextConfidence - 20); + } + // Score 0-1: healthy trend → fall through to normal logic below + // ── END FIX#503 ───────────────────────────────────────────────── + + if(IsFlagPattern()) + { + // S5/S6: Bull/Bear Flag — consolidation inside trend, expect breakout + sp.scenario = (ctx.phase == MPHASE_MARKUP) ? SCEN_BULL_FLAG : SCEN_BEAR_FLAG; + sp.entryStyle = ES_BREAKOUT; + sp.slMethod = SL_FLAG; // SL beyond flag boundary + sp.tpMethod = TP_RANGE_PROJ; // Target = flagpole height projected + sp.allowTC = true; // Trend continuation = ideal for flag breakout + sp.allowFVG = false; // FVG less reliable in tight flag + sp.allowOB = false; + sp.allowBOS = true; // BOS at flag breakout = confirmation + sp.allowOTE = false; + sp.allowLIQ = true; + sp.allowMEANREV = false; + sp.minRR = sp.minRR; + sp.confidence = 70; + sp.description = (ctx.phase==MPHASE_MARKUP) ? "Bull Flag" : "Bear Flag"; + sp.isValid = true; + } + else if(HasRecentCHoCH() && + (g_regimeData.regime == REGIME_WEAK_TREND_UP || + g_regimeData.regime == REGIME_WEAK_TREND_DOWN || + g_regimeData.trendEfficiency < 0.4)) + { + // S7/S8: Reversal — CHoCH confirmed AND trend weakening (efficiency < 0.4) + // FIX#502-CAL2: Require regime weakening to avoid false Reversal in strong trends. + // Root: Nov 2024 downtrend was STRONG, CHoCH within it = pullback, not reversal. + // Now: CHoCH alone in STRONG/TREND regime → falls through to Pullback/Continuation. + sp.scenario = SCEN_REVERSAL; + sp.entryStyle = ES_RETEST; // Wait for retest of broken level + sp.slMethod = SL_STRUCTURE; // SL beyond last swing point + sp.tpMethod = TP_PREV_HIGHLOW; + sp.allowTC = false; // TC = trend following, useless in reversal + sp.allowFVG = true; // FVG at retest zone = confluence + sp.allowOB = true; // OB = key reversal technique + sp.allowBOS = true; // BOS = primary reversal signal + sp.allowOTE = true; // OTE at reversal Fib = entry precision + sp.allowLIQ = true; // Liquidity sweep before reversal = common + sp.allowMEANREV = false; + sp.minRR = sp.minRR * 1.2; // Reversal needs better RR — higher risk + sp.confidence = 65; + // FIX#509: Reversal requires zone confirmation (retest or pullback active) + // Root: T013/T047 in v11.17 — Reversal opened without price being in + // a retest/OTE zone → 0.97R near-miss → full SL. + // Fix: isValid only when price is in a confirmed reversal zone. + { + bool _zone = IsRetestInProgress() || IsPullbackActive(); + sp.isValid = _zone; + sp.description = ((ctx.phase==MPHASE_MARKUP) ? "Reversal Down" : "Reversal Up") + + (_zone ? "" : " [no-zone WAIT]"); + } + } + else if(IsPullbackActive()) + { + // S3/S4: Pullback — trend intact, price retracing to OTE zone + sp.scenario = SCEN_TREND_PULLBACK; + sp.entryStyle = ES_FIB; // Enter at Fib 50-62% + sp.slMethod = SL_FIB78; // SL below Fib 78.6% (trend invalidation) + sp.tpMethod = TP_PREV_HIGHLOW;// Target = previous swing high/low + sp.allowTC = false; // TC = momentum entry, not pullback + sp.allowFVG = true; // FVG in Fib zone = high-confluence entry + sp.allowOB = true; // OB in Fib zone = institutional support + sp.allowBOS = false; // No BOS needed — trend intact + sp.allowOTE = true; // OTE = Optimal Trade Entry = designed for this + sp.allowLIQ = true; // Liquidity sweep at pullback low = common + sp.allowMEANREV = false; + sp.confidence = 80; + sp.description = (ctx.phase==MPHASE_MARKUP) ? "Pullback Buy" : "Pullback Sell"; + sp.isValid = true; + } + else + { + // Trend continuation — no specific pattern, standard trend entry. + // FIX#515: Gate technique access by regime strength. + // ROOT: STRONG/CONFIRMED regimes have clean structure → all techniques valid. + // TRENDING (moderate) regime has less clean structure → restrict to + // OB/FVG/OTE only (high-precision tools). TC/BOS in moderate trends + // produce late momentum entries at unfavorable price levels. + sp.scenario = SCEN_TREND_PULLBACK; + sp.entryStyle = ES_RETEST; + sp.slMethod = SL_STRUCTURE; + sp.tpMethod = TP_PREV_HIGHLOW; + sp.allowMEANREV = false; + + bool _strongRegime = (g_regimeData.regime == REGIME_STRONG_TREND_UP || + g_regimeData.regime == REGIME_STRONG_TREND_DOWN || + g_regimeData.trendEfficiency >= 0.65); + if(_strongRegime) + { + // Strong, efficient trend — all momentum/structure techniques valid + sp.allowTC = true; + sp.allowFVG = true; + sp.allowOB = true; + sp.allowBOS = true; + sp.allowOTE = true; + sp.allowLIQ = true; + sp.confidence = 75; // higher conviction in strong trend + sp.description = (ctx.phase==MPHASE_MARKUP) ? "Strong Trend Up" : "Strong Trend Down"; + } + else + { + // Moderate trend — precision tools only, no momentum chasing + sp.allowTC = false; // TC entries are late in moderate trends + sp.allowFVG = true; + sp.allowOB = true; + sp.allowBOS = false; // BOS in moderate trend = premature breakout + sp.allowOTE = true; + sp.allowLIQ = true; + sp.confidence = 65; + sp.description = (ctx.phase==MPHASE_MARKUP) ? "Trend Up" : "Trend Down"; + } + sp.isValid = true; + } + + // ── FIX#503 POST: Apply caution restrictions (score==2) ────────── + // After normal scenario logic sets allowXxx flags: + // If caution, block momentum/breakout techniques — only OB/FVG allowed. + // This applies to ALL sub-scenarios (Flag, Pullback, Trend Up/Down). + if(g_exhaustionPeak == 2 && sp.isValid) + { + sp.allowTC = false; // No momentum chasing in cautious trend + sp.allowBOS = false; // BOS = breakout confirmation — risky when exhausting + sp.description = sp.description + " [Caution]"; + } + // ── END FIX#503 POST ───────────────────────────────────────────── + break; + } + + // ── BREAKOUT ────────────────────────────────────────────────── + case MPHASE_BREAKOUT_BULL: + case MPHASE_BREAKOUT_BEAR: + { + sp.direction = (ctx.phase == MPHASE_BREAKOUT_BULL) ? 1 : -1; + + // FIX#503b: use g_breakoutConfScore to distinguish genuine vs fakeout + if(g_breakoutConfScore <= 1 && IsRetestInProgress()) + { + // Weak breakout + retest = likely fakeout → fade it + sp.scenario = SCEN_FAKEOUT; + sp.entryStyle = ES_FADE; + sp.slMethod = SL_WICK; + sp.tpMethod = TP_RANGE_PROJ; + sp.allowTC = false; + sp.allowFVG = true; + sp.allowOB = true; + sp.allowBOS = false; + sp.allowOTE = false; + sp.allowLIQ = true; + sp.allowMEANREV = true; + sp.minRR = 1.5; + sp.confidence = 60; + sp.description = "Weak Breakout→Fakeout"; + sp.isValid = true; + } + else if(g_breakoutConfScore <= 1) + { + // Weak breakout, no retest yet — WAIT + sp.scenario = SCEN_CHOPPY; + sp.isValid = false; + sp.description = StringFormat("Weak Breakout [conf=%d] — wait retest", + g_breakoutConfScore); + } + else if(IsRetestInProgress()) + { + // S1/S2: Genuine breakout + retest — ideal entry + sp.scenario = SCEN_BREAKOUT_RETEST; + sp.entryStyle = ES_RETEST; + sp.slMethod = SL_CONSOLIDATION; + sp.tpMethod = TP_RANGE_PROJ; + sp.allowTC = false; + sp.allowFVG = true; + sp.allowOB = true; + sp.allowBOS = true; + sp.allowOTE = false; + sp.allowLIQ = true; + sp.allowMEANREV = false; + sp.confidence = 70 + (g_breakoutConfScore * 5); + sp.description = StringFormat("Breakout Retest [conf=%d]", g_breakoutConfScore); + sp.isValid = true; + } + else + { + // Genuine breakout in progress — aggressive + sp.scenario = SCEN_BREAKOUT_RETEST; + sp.entryStyle = ES_BREAKOUT; + sp.slMethod = SL_CONSOLIDATION; + sp.tpMethod = TP_RANGE_PROJ; + sp.allowTC = true; + sp.allowFVG = false; + sp.allowOB = false; + sp.allowBOS = true; + sp.allowOTE = false; + sp.allowLIQ = true; + sp.allowMEANREV = false; + sp.confidence = 60 + (g_breakoutConfScore * 4); + sp.description = "Breakout"; + sp.isValid = true; + } + break; + } + + // ── RANGING ─────────────────────────────────────────────────── + case MPHASE_RANGING: + { + sp.direction = 0; // Both directions, gated by position in range + + if(IsFakeoutCondition()) + { + // S9: Fakeout — false breakout, fade back into range + sp.scenario = SCEN_FAKEOUT; + sp.entryStyle = ES_FADE; + sp.slMethod = SL_WICK; // SL beyond the fakeout wick + sp.tpMethod = TP_RANGE_PROJ; // Target = 50% range, then opposite + sp.allowTC = false; + sp.allowFVG = true; + sp.allowOB = true; + sp.allowBOS = false; + sp.allowOTE = false; + sp.allowLIQ = true; + sp.allowMEANREV = true; // Mean reversion = ideal for fakeout + sp.minRR = 1.5; + sp.confidence = 75; + sp.description = "Fakeout Fade"; + sp.isValid = true; + } + else if(IsCompressionSetup()) + { + // S12: Range compression (coiling spring) — wait for breakout + sp.scenario = SCEN_COMPRESSION; + sp.entryStyle = ES_BREAKOUT; + sp.slMethod = SL_CONSOLIDATION; + sp.tpMethod = TP_RANGE_PROJ; + sp.allowTC = true; + sp.allowFVG = false; + sp.allowOB = false; + sp.allowBOS = true; + sp.allowOTE = false; + sp.allowLIQ = true; + sp.allowMEANREV = false; + sp.minRR = 2.0; // Compression breakouts are explosive + sp.confidence = 70; + sp.description = "Compression Breakout"; + sp.isValid = true; + } + else + { + // Standard range fade — sell premium, buy discount + sp.scenario = SCEN_RANGE_FADE; + sp.entryStyle = ES_FADE; + sp.slMethod = SL_WICK; + sp.tpMethod = TP_RANGE_PROJ; + sp.allowTC = false; // TC = trend following, useless in range + sp.allowFVG = true; + sp.allowOB = true; + sp.allowBOS = false; + sp.allowOTE = true; + sp.allowLIQ = true; + sp.allowMEANREV = true; // Mean reversion = primary technique + sp.minRR = 1.5; + sp.confidence = (g_rangeConfScore >= 3) ? 75 : 65; + sp.description = "Range Fade"; + sp.isValid = true; + } + break; + } + + // ── ACCUMULATION / DISTRIBUTION (pre-reversal) ──────────────── + case MPHASE_ACCUMULATION: + case MPHASE_DISTRIBUTION: + { + // FIX#503b: use g_reversalConfScore to assess setup quality + // reversalConfScore=0 means: no CHoCH, no divergence, no exhaustion → weak setup + // Require at least 1 confirmation before entering + bool _isAccum = (ctx.phase == MPHASE_ACCUMULATION); + sp.scenario = SCEN_REVERSAL; + sp.direction = _isAccum ? 1 : -1; + sp.entryStyle = ES_RETEST; + sp.slMethod = SL_STRUCTURE; + sp.tpMethod = TP_FIB_EXT; + sp.allowTC = false; + sp.allowFVG = true; + sp.allowOB = true; + sp.allowBOS = true; + sp.allowOTE = true; + sp.allowLIQ = true; + sp.allowMEANREV = false; + sp.minRR = sp.minRR * 1.2; + // FIX#509: Exhaustion-aware branching + zone confirmation + // Peak=4+: overextended pre-reversal — only valid with CHoCH AND zone + // Peak=3: weakening — needs zone confirmation + // Peak=0-2: normal — needs reversalConfScore>=1 AND zone + { + bool _zone = IsRetestInProgress() || IsPullbackActive(); + if(g_exhaustionPeak >= 4) + { + // High exhaustion: CHoCH required + zone required + bool _choch = HasRecentCHoCH(); + sp.isValid = _choch && _zone; + sp.confidence = _choch ? 75 : 40; + sp.description = StringFormat("%s [Peak=%d%s%s]", + _isAccum ? "Accumulation" : "Distribution", + g_exhaustionPeak, + _choch ? " CHoCH" : " no-CHoCH WAIT", + _zone ? "" : " no-zone WAIT"); + } + else if(g_exhaustionPeak == 3) + { + // Moderate exhaustion: zone required, conf>=1 + sp.isValid = (g_reversalConfScore >= 1) && _zone; + sp.confidence = ctx.contextConfidence; + sp.description = StringFormat("%s [Peak=3%s]", + _isAccum ? "Accumulation" : "Distribution", + _zone ? "" : " no-zone WAIT"); + } + else + { + // Normal: conf>=1 + zone + sp.isValid = (g_reversalConfScore >= 1) && _zone; + sp.confidence = ctx.contextConfidence + (g_reversalConfScore * 5); + sp.description = StringFormat("%s%s", + _isAccum ? "Accumulation" : "Distribution", + _zone ? "" : " [no-zone WAIT]"); + } + } + break; + } + + // ── UNKNOWN / DEFAULT → CHOPPY → no trade ───────────────────── + default: + sp.scenario = SCEN_CHOPPY; + sp.isValid = false; // EA_CheckSignals returns immediately + sp.description = "Choppy/Unknown — no entry"; + break; + } + + // ── FIX#509: Universal Peak≥4 guard (final safety) ─────────────────── + // After ALL phase cases: if exhaustion peak is high AND scenario is not + // a reversal/fade → block. Prevents trend-following entries on overextended + // moves regardless of which phase branch was taken. + // Evidence: T005 v11.17 — TC SELL Trend Up | Peak=4 → -$441 (would be blocked) + // Exempt: REVERSAL, RANGE_FADE, FAKEOUT (these trade AGAINST the exhausted direction) + if(g_exhaustionPeak >= 4 && sp.isValid) + { + bool _exempt = (sp.scenario == SCEN_REVERSAL || + sp.scenario == SCEN_RANGE_FADE || + sp.scenario == SCEN_FAKEOUT); + if(!_exempt) + { + if(g_verboseLog) + PrintFormat("[FIX#509] BLOCKED Peak=%d guard: %s", + g_exhaustionPeak, sp.description); + sp.isValid = false; + sp.description = sp.description + StringFormat(" [Peak=%d BLOCKED]", g_exhaustionPeak); + } + } + + if(g_verboseLog && sp.isValid) + PrintFormat("[FIX#502] Scenario=%s | Phase=%d | Entry=%d | SL=%d | TP=%d | conf=%d | minRR=%.1f", + sp.description, (int)ctx.phase, + (int)sp.entryStyle, (int)sp.slMethod, (int)sp.tpMethod, + sp.confidence, sp.minRR); + + return sp; +} +// ── FIX#502: SCENARIO WEIGHT MULTIPLIER ────────────────────────────── +// Returns an additional score multiplier for a technique based on the +// current scenario. Compounds with the existing context multiplier. +// Values > 1.0 = technique is particularly effective in this scenario. +// Values < 1.0 = technique is less reliable (should have been gated, but +// acts as soft penalty for techniques not hard-blocked). +// Called from ApplyContextMultiplierToCandidate(). +double GetScenarioWeightMult(ENUM_SCENARIO scenario, ENUM_ENTRY_TECHNIQUE tech) +{ + switch(scenario) + { + case SCEN_TREND_PULLBACK: + // OTE is the primary technique for pullback — high weight + // FVG/OB at fib zone = strong confluence + // TC = momentum continuation, not pullback — reduce + if(tech == TECH_OTE) return 1.30; + if(tech == TECH_FVG) return 1.20; + if(tech == TECH_OB) return 1.20; + if(tech == TECH_LIQ_SWEEP) return 1.10; + if(tech == TECH_TREND_CONT) return 0.70; + if(tech == TECH_BOS_RETEST) return 0.80; + break; + + case SCEN_BREAKOUT_RETEST: + // BOS is the primary signal at breakout + // FVG/OB at retest zone = high confluence + // OTE = not ideal at breakout level + if(tech == TECH_BOS_RETEST) return 1.40; + if(tech == TECH_FVG) return 1.20; + if(tech == TECH_OB) return 1.20; + if(tech == TECH_LIQ_SWEEP) return 1.30; + if(tech == TECH_TREND_CONT) return 1.10; + if(tech == TECH_OTE) return 0.70; + break; + + case SCEN_RANGE_FADE: + case SCEN_FAKEOUT: + // FVG/OB at range boundary = primary techniques + // Mean reversion = designed for this + // TC = useless in range + if(tech == TECH_FVG) return 1.30; + if(tech == TECH_OB) return 1.30; + if(tech == TECH_OTE) return 1.20; + if(tech == TECH_MEAN_REV) return 1.30; + if(tech == TECH_LIQ_SWEEP) return 1.10; + if(tech == TECH_TREND_CONT) return 0.50; + if(tech == TECH_BOS_RETEST) return 0.60; + break; + + case SCEN_REVERSAL: + // BOS = key reversal confirmation + // OB = institutional reversal zone + // Divergence captured in score elsewhere + if(tech == TECH_BOS_RETEST) return 1.50; + if(tech == TECH_OB) return 1.30; + if(tech == TECH_FVG) return 1.10; + if(tech == TECH_OTE) return 1.10; + if(tech == TECH_LIQ_SWEEP) return 1.20; + if(tech == TECH_TREND_CONT) return 0.50; + break; + + case SCEN_BULL_FLAG: + case SCEN_BEAR_FLAG: + // TC = trend continuation = ideal for flag breakout + // BOS = breakout confirmation + if(tech == TECH_TREND_CONT) return 1.40; + if(tech == TECH_BOS_RETEST) return 1.20; + if(tech == TECH_LIQ_SWEEP) return 1.10; + if(tech == TECH_FVG) return 0.80; + if(tech == TECH_OB) return 0.80; + if(tech == TECH_OTE) return 0.70; + break; + + case SCEN_COMPRESSION: + // BOS = primary breakout signal + // TC = continuation after breakout + if(tech == TECH_BOS_RETEST) return 1.40; + if(tech == TECH_TREND_CONT) return 1.20; + if(tech == TECH_LIQ_SWEEP) return 1.20; + if(tech == TECH_FVG) return 0.80; + if(tech == TECH_OTE) return 0.70; + break; + + case SCEN_FAILED_BREAKOUT: + // BOS failed = reversal setup + // OB/FVG at failed level = entry + if(tech == TECH_BOS_RETEST) return 1.30; + if(tech == TECH_OB) return 1.20; + if(tech == TECH_FVG) return 1.20; + if(tech == TECH_LIQ_SWEEP) return 1.10; + if(tech == TECH_TREND_CONT) return 0.60; + break; + + default: break; + } + return 1.0; // No adjustment for this technique/scenario combination +} + +//+------------------------------------------------------------------+ +//| FIX#379: APPLY CONTEXT MULTIPLIERS TO CANDIDATE SCORE | +//| Called after AddCandidate() — adjusts score based on context. | +//+------------------------------------------------------------------+ +void ApplyContextMultiplierToCandidate(int idx) +{ + if(!g_mktCtx.valid) return; + if(idx < 0 || idx >= g_candidateCount) return; + + // technique is ENUM_ENTRY_TECHNIQUE — read directly from array + ENUM_ENTRY_TECHNIQUE tech = g_candidates[idx].technique; + double mult = 1.0; + + switch(tech) + { + case TECH_FVG: mult = g_mktCtx.fvgScoreMult; break; + case TECH_OB: mult = g_mktCtx.obScoreMult; break; + case TECH_TREND_CONT: mult = g_mktCtx.tcScoreMult; break; + case TECH_BOS_RETEST: mult = g_mktCtx.bosScoreMult; break; + case TECH_OTE: mult = g_mktCtx.oteScoreMult; break; + case TECH_LIQ_SWEEP: case TECH_TBS: mult = g_mktCtx.liqScoreMult; break; + case TECH_BREAKER: mult = g_mktCtx.obScoreMult; break; + default: mult = 1.0; break; + } + + // ── FIX#502: Apply additional scenario weight multiplier ────────── + // Compounds with the existing context multiplier above. + // Scenarios reward the techniques that are most effective in that context. + if(g_scenarioProfile.isValid) + mult *= GetScenarioWeightMult(g_candidates[idx].scenario, tech); + + if(MathAbs(mult - 1.0) > 0.01) + { + int oldScore = g_candidates[idx].totalScore; + int newScore = (int)MathRound(oldScore * mult); + if(g_verboseLog) + PrintFormat("[FIX#379] Context mult x%.2f on tech=%d %s: totalScore %d -> %d (phase=%s scenario=%d)", + mult, (int)tech, + g_candidates[idx].isBullish ? "BUY" : "SELL", + oldScore, newScore, g_mktCtx.phaseDescription, + (int)g_candidates[idx].scenario); + g_candidates[idx].totalScore = newScore; + } + // * FIX#383: Multi-zone confluence bonus — added AFTER context multiplier + int confBonus = ComputeZoneConfluenceBonus(idx); + if(confBonus > 0) + g_candidates[idx].totalScore += confBonus; +} + +//+------------------------------------------------------------------+ +//| FIX#380: STRUCTURAL TP FOR ALL TECHNIQUES | +//| Upgrades TP1/TP2 of a candidate to nearest structural level. | +//| Replaces pure ATR-based TP when a better structural target | +//| is available. Falls back to ATR if no structural level found. | +//+------------------------------------------------------------------+ +void UpgradeCandidateTPsToStructural(int idx) +{ + if(idx < 0 || idx >= g_candidateCount) return; + // FIX: MQL5 does not support references to array elements. + // Copy fields locally, write back only what changes. + double entryPrice = g_candidates[idx].entryPrice; + double stopLoss = g_candidates[idx].stopLoss; + bool isBullish = g_candidates[idx].isBullish; + ENUM_ENTRY_TECHNIQUE technique = g_candidates[idx].technique; + + double atr = g_cachedATR; + if(atr <= 0) return; + + double slDist = MathAbs(entryPrice - stopLoss); + if(slDist <= 0) return; + + double minTP1Dist = slDist * (AutoOpt_Enabled ? g_autoOptParams.min_rr : EA_MinRR); + + // Build a temporary CandidateSignal to pass into FindNearestStructuralTP (which takes const ref) + CandidateSignal tmp; + tmp = g_candidates[idx]; // shallow copy — safe, no dynamic memory + + double structTP1 = FindNearestStructuralTP(tmp, atr, minTP1Dist); + if(structTP1 > 0) + { + double structDist = MathAbs(structTP1 - entryPrice); + double currentDist = MathAbs(g_candidates[idx].tp1 - entryPrice); + + if(structDist > currentDist * 1.05) + { + if(g_verboseLog) + PrintFormat("[FIX#380] Structural TP1 upgrade: %.5f -> %.5f (+%.1f%% RR) on tech=%d %s", + g_candidates[idx].tp1, structTP1, + (structDist / currentDist - 1.0) * 100.0, + (int)technique, isBullish ? "BUY" : "SELL"); + g_candidates[idx].tp1 = structTP1; + tmp.tp1 = structTP1; + + double minTP2Dist = structDist + atr * 0.5; + double structTP2 = FindNearestStructuralTP(tmp, atr, minTP2Dist); + if(structTP2 > 0) + { + g_candidates[idx].tp2 = structTP2; + tmp.tp2 = structTP2; + } + + double structTP3 = FindStructuralTP3(tmp, atr); + if(structTP3 > 0 && MathAbs(structTP3 - entryPrice) > MathAbs(g_candidates[idx].tp2 - entryPrice)) + g_candidates[idx].tp3 = structTP3; + } + } +} + +//+------------------------------------------------------------------+ +//| FIX#381: ZONE-AWARE SMART EXIT 2.0 | +//| Checks whether the entry zone (OB/FVG/BOS/etc) has been | +//| violated AGAINST the trade. If so, closes immediately | +//| regardless of current RR — the reason for entry is gone. | +//| Runs every 5s inside ManageFullHybrid alongside SmartExit_FIX41.| + +//+------------------------------------------------------------------+ +//| FIX#382: STRUCTURE-BASED TRAILING STOP | +//| Trails SL behind the most recent confirmed swing low (BUY) or | +//| swing high (SELL) from STRUCT_Array — not behind ATR×mult. | +//| Called from ManageFullHybrid after TP1 is hit. | +//+------------------------------------------------------------------+ + + +//+------------------------------------------------------------------+ +//| Get Current Session Type for SL Adjustment | +//+------------------------------------------------------------------+ +ENUM_SESSION_SL_TYPE GetCurrentSessionSL() +{ + if(!SessionSL_Enabled) + return SESSION_SL_DEAD_ZONE; + MqlDateTime dt; + TimeToStruct(TimeGMT(), dt); // * FIX#413b: TimeGMT() — comment says GMT, code must match + int hour = dt.hour; + // Asian: 23:00-08:00 + // London: 07:00-16:00 + // NY: 12:00-21:00 + bool isAsian = (hour >= 23 || hour < 8); + bool isLondon = (hour >= 7 && hour < 16); + bool isNY = (hour >= 12 && hour < 21); + // [OK] FIX: Check ALL overlaps generically (Asian/London + London/NY) + if((isAsian && isLondon) || (isLondon && isNY)) + return SESSION_SL_OVERLAP; + else if(isNY) + return SESSION_SL_NY; + else if(isLondon) + return SESSION_SL_LONDON; + else if(isAsian) + return SESSION_SL_ASIAN; + else + return SESSION_SL_DEAD_ZONE; +} +//+------------------------------------------------------------------+ +//| Get Session SL Multiplier | +//+------------------------------------------------------------------+ +double GetSessionSLMultiplier() +{ + if(!SessionSL_Enabled) + return 1.0; + // * v9.16 FIX#46b: Pair-aware SessionSL multipliers + // Problem: Asian mult=0.8 for ALL pairs, but: + // Gold Asian = quiet (0.8 correct), USDJPY Asian = ACTIVE (0.8 wrong -> too tight -> SL hits) + // Indices Asian = almost dead (0.7 better), Crypto = 24h (1.0) + // Fix: Adjust base multiplier per pair category + session combo + string pairCat = g_autoOptParams.pair_category; + double baseMult = 1.0; + switch(g_currentSessionSL) + { + case SESSION_SL_ASIAN: + { + // Base from input + baseMult = SessionSL_AsianMult; // 0.8 + // Pair adjustments + if(pairCat == "Major") + { + // JPY pairs are ACTIVE during Asian (Tokyo session) + string sym = _Symbol; + if(StringFind(sym, "JPY") >= 0 || StringFind(sym, "AUD") >= 0 || StringFind(sym, "NZD") >= 0) + baseMult = 1.0; // Normal SL -- Asian is THEIR session + // EUR/GBP/CHF pairs quiet during Asian -> keep tighter + // baseMult stays 0.8 + } + else if(pairCat == "Cross" || pairCat == "VolatileCross") + { + if(StringFind(_Symbol, "JPY") >= 0) + baseMult = 1.05; // JPY crosses active during Asian + else + baseMult = 0.75; // Non-JPY crosses very quiet + } + else if(pairCat == "Metal" || pairCat == "Commodity") + { + baseMult = 0.75; // Gold/Silver very quiet Asian + } + else if(pairCat == "Index") + { + baseMult = 0.70; // Indices near-dead Asian (except Nikkei) + if(StringFind(_Symbol, "JP") >= 0 || StringFind(_Symbol, "NIK") >= 0) + baseMult = 1.0; // Nikkei active during Asian + } + else if(pairCat == "Energy") + { + baseMult = 0.70; // Oil quiet Asian + } + else if(pairCat == "Crypto") + { + baseMult = 1.0; // Crypto 24h -> no session effect + } + else if(pairCat == "Exotic") + { + baseMult = 0.70; // Exotics dead Asian + } + break; + } + case SESSION_SL_LONDON: + { + baseMult = SessionSL_LondonMult; // 1.0 + // London is EUR/GBP home -> wider SL for them + if(pairCat == "Major" || pairCat == "Cross") + { + if(StringFind(_Symbol, "EUR") >= 0 || StringFind(_Symbol, "GBP") >= 0 || StringFind(_Symbol, "CHF") >= 0) + baseMult = 1.05; // Slightly wider -- their peak session + } + else if(pairCat == "Metal") + { + baseMult = 1.05; // Gold active London + } + else if(pairCat == "Index") + { + if(StringFind(_Symbol, "DAX") >= 0 || StringFind(_Symbol, "UK") >= 0 || StringFind(_Symbol, "STOXX") >= 0 || StringFind(_Symbol, "GER") >= 0) + baseMult = 1.10; // EU indices peak + else + baseMult = 0.95; // US indices pre-market -> quieter + } + break; + } + case SESSION_SL_NY: + { + baseMult = SessionSL_NYMult; // 1.1 + if(pairCat == "Index") + { + baseMult = 1.15; // US indices peak volatility + } + else if(pairCat == "Metal" || pairCat == "Energy") + { + baseMult = 1.10; // Gold/Oil active NY + } + else if(pairCat == "Crypto") + { + baseMult = 1.0; // Crypto 24h + } + break; + } + case SESSION_SL_OVERLAP: + { + baseMult = SessionSL_OverlapMult; // 1.1 + // Overlap = highest volatility for most assets + if(pairCat == "Metal" || pairCat == "Energy") + baseMult = 1.15; + else if(pairCat == "VolatileCross") + baseMult = 1.20; // GBPJPY etc. explode during overlap + else if(pairCat == "Index") + baseMult = 1.15; + break; + } + default: + baseMult = 1.0; + break; + } + return baseMult; +} +//+------------------------------------------------------------------+ +//| Update Session Info | +//+------------------------------------------------------------------+ +void UpdateSessionInfo() +{ + // Update only once per minute for performance + if(TimeCurrent() - g_lastSessionCheck < 60) + return; + g_currentSessionSL = GetCurrentSessionSL(); + g_sessionSLMultiplier = GetSessionSLMultiplier(); + g_lastSessionCheck = TimeCurrent(); + // Display session info on chart + if(SessionSL_ShowInfo) + { + string sessionName = ""; + switch(g_currentSessionSL) + { + case SESSION_SL_ASIAN: sessionName = "Asian"; break; + case SESSION_SL_LONDON: sessionName = "London"; break; + case SESSION_SL_NY: sessionName = "NY"; break; + case SESSION_SL_OVERLAP: sessionName = "OVERLAP"; break; + default: sessionName = "Dead Zone"; break; + } + string label = StringFormat("Session: %s | SL x%.2f", + sessionName, g_sessionSLMultiplier); + ObjectCreate(0, "SessionSL_Label", OBJ_LABEL, 0, 0, 0); + ObjectSetInteger(0, "SessionSL_Label", OBJPROP_CORNER, CORNER_RIGHT_UPPER); + ObjectSetInteger(0, "SessionSL_Label", OBJPROP_XDISTANCE, 10); + ObjectSetInteger(0, "SessionSL_Label", OBJPROP_YDISTANCE, 80); + ObjectSetInteger(0, "SessionSL_Label", OBJPROP_COLOR, SessionSL_LabelColor); + ObjectSetInteger(0, "SessionSL_Label", OBJPROP_FONTSIZE, 10); + ObjectSetString(0, "SessionSL_Label", OBJPROP_TEXT, label); + } +} +// =================================================================== +// * v7.2: REMOVED EA_BuildSignal() and EA_CalculateScore() +// These were DEAD CODE -- never called from the EA pipeline. +// EA_BuildSignal recalculated TPs with blind ATR multipliers, +// destroying SmartAdjustTPs swing-based targeting. +// EA_CalculateScore was only called from EA_BuildSignal. +// Actual pipeline: EA_CheckSignals -> BuildCandidateSLTP -> +// SmartAdjustTPs -> AddCandidate (RunConfirmationCascade) -> +// SelectBestCandidate -> EvaluateSmartEntry -> EA_ExecuteTrade +// =================================================================== +//+------------------------------------------------------------------+ +//| Execute Trade | +//+------------------------------------------------------------------+ +// ── FIX#502: SCENARIO-AWARE SL/TP ADJUSTMENT ───────────────────────── +// Called after EA_CheckSignals() produces a valid signal, before execution. +// Adjusts g_ea_signal.stopLoss / tp1 / tp2 / tp3 based on the scenario +// geometry from g_scenarioProfile.slMethod and g_scenarioProfile.tpMethod. +// +// FALLBACK RULE: If computed SL/TP is invalid (0, wrong side, or worse +// than existing), keeps the original technique-derived values. +// This means existing FVG/OB/OTE SL logic is preserved when scenario +// geometry cannot improve on it. +void AdjustSLTPForScenario() +{ + if(!g_ea_signal.isValid || !g_scenarioProfile.isValid) return; + if(g_ea_signal.entryPrice <= 0 || g_ea_signal.stopLoss <= 0) return; + + bool isBuy = g_ea_signal.isBullish; + double entry = g_ea_signal.entryPrice; + double curSL = g_ea_signal.stopLoss; + double atr = g_cachedATR; + if(atr <= 0) return; + + double newSL = 0; + + // ── Compute new SL from scenario method ────────────────────────── + switch(g_scenarioProfile.slMethod) + { + case SL_CONSOLIDATION: + // SL beyond the consolidation range (rangeHigh/rangeLow ± 1×ATR) + // Consolidation = the range before the breakout + if(g_regimeData.rangeHigh > g_regimeData.rangeLow) + { + newSL = isBuy ? g_regimeData.rangeLow - atr + : g_regimeData.rangeHigh + atr; + } + break; + + case SL_FIB78: + // SL beyond Fib 78.6% of the last swing — pullback invalidation + // Proxy: use OTE zone bottom/top ± 0.5 ATR + { + int n = ArraySize(OTE_Array); + for(int i = 0; i < n; i++) + { + if(!OTE_Array[i].isValid || !OTE_Array[i].active) continue; + if(OTE_Array[i].isBullish != isBuy) continue; + newSL = isBuy ? OTE_Array[i].level786 - atr * 0.5 + : OTE_Array[i].level786 + atr * 0.5; + break; + } + } + break; + + case SL_WICK: + // SL beyond the fakeout wick ± 0.5 ATR + // Wick = price poked outside range boundary + if(g_regimeData.rangeHigh > g_regimeData.rangeLow) + { + newSL = isBuy ? g_regimeData.rangeLow - atr * 0.5 + : g_regimeData.rangeHigh + atr * 0.5; + } + break; + + case SL_FLAG: + // SL beyond the flag boundary (tight consolidation inside trend) + // Flag boundary ≈ rangeHigh/rangeLow of the consolidation + if(g_regimeData.rangeHigh > g_regimeData.rangeLow) + { + newSL = isBuy ? g_regimeData.rangeLow - atr + : g_regimeData.rangeHigh + atr; + } + break; + + case SL_STRUCTURE: + // SL beyond the last swing point (LH for SELL, HL for BUY) + // Use g_swingHighs/g_swingLows arrays + { + int swingCount = ArraySize(g_swingLows); + if(isBuy && swingCount > 0) + { + double lastHL = g_swingLows[0]; + for(int i = 0; i < MathMin(swingCount, 5); i++) + if(g_swingLows[i] > 0) { lastHL = g_swingLows[i]; break; } + newSL = lastHL - atr; + } + int swingHCount = ArraySize(g_swingHighs); + if(!isBuy && swingHCount > 0) + { + double lastLH = g_swingHighs[0]; + for(int i = 0; i < MathMin(swingHCount, 5); i++) + if(g_swingHighs[i] > 0) { lastLH = g_swingHighs[i]; break; } + newSL = lastLH + atr; + } + } + break; + + case SL_ATR: + default: + // Generic ATR-based SL — keep technique-derived value + newSL = 0; + break; + } + + // ── Validate new SL — only apply if better than current ────────── + // "Better" = further from entry (more room = less likely to be stopped prematurely) + // AND correct side (SL must be beyond entry, not between entry and TP) + bool slValid = false; + if(newSL > 0) + { + bool correctSide = isBuy ? (newSL < entry) : (newSL > entry); + double newDist = MathAbs(entry - newSL); + double curDist = MathAbs(entry - curSL); + // Accept if: correct side AND not wildly wider than current (max 2× wider) + // This prevents scenario SL from overriding a tight well-placed structure SL + slValid = correctSide && newDist > 0 && newDist <= curDist * 2.0; + } + + if(slValid) + { + double oldSL = g_ea_signal.stopLoss; + g_ea_signal.stopLoss = newSL; + + // Recompute TP targets based on scenario TP method and new SL distance + double slDist = MathAbs(entry - newSL); + double newTP1 = 0, newTP2 = 0, newTP3 = 0; + + switch(g_scenarioProfile.tpMethod) + { + case TP_RANGE_PROJ: + // Target = range width projected from breakout/entry point + if(g_regimeData.rangeHigh > g_regimeData.rangeLow) + { + double rangeH = g_regimeData.rangeHigh - g_regimeData.rangeLow; + newTP1 = isBuy ? entry + rangeH : entry - rangeH; + newTP2 = isBuy ? entry + rangeH * 1.5 : entry - rangeH * 1.5; + newTP3 = isBuy ? entry + rangeH * 2.0 : entry - rangeH * 2.0; + } + break; + + case TP_PREV_HIGHLOW: + // Target = nearest structural high/low beyond entry + { + double bestTP1 = 0; + if(isBuy) + { + int n = ArraySize(g_swingHighs); + for(int i = 0; i < MathMin(n, 10); i++) + if(g_swingHighs[i] > entry + slDist * 0.5) + { bestTP1 = g_swingHighs[i]; break; } + } + else + { + int n = ArraySize(g_swingLows); + for(int i = 0; i < MathMin(n, 10); i++) + if(g_swingLows[i] > 0 && g_swingLows[i] < entry - slDist * 0.5) + { bestTP1 = g_swingLows[i]; break; } + } + if(bestTP1 > 0) + { + newTP1 = bestTP1; + newTP2 = isBuy ? entry + slDist * g_autoOptParams.tp2_rr + : entry - slDist * g_autoOptParams.tp2_rr; + newTP3 = isBuy ? entry + slDist * g_autoOptParams.tp3_rr + : entry - slDist * g_autoOptParams.tp3_rr; + } + } + break; + + case TP_FIB_EXT: + // Target = Fibonacci extensions 127.2% / 161.8% / 200% + newTP1 = isBuy ? entry + slDist * 1.272 : entry - slDist * 1.272; + newTP2 = isBuy ? entry + slDist * 1.618 : entry - slDist * 1.618; + newTP3 = isBuy ? entry + slDist * 2.000 : entry - slDist * 2.000; + break; + + case TP_FLAGPOLE: + // Target = use fixed RR from g_workingMinRiskReward (flagpole projection + // requires bar-by-bar measurement not available here) + newTP1 = isBuy ? entry + slDist * g_workingMinRiskReward + : entry - slDist * g_workingMinRiskReward; + newTP2 = isBuy ? entry + slDist * g_autoOptParams.tp2_rr + : entry - slDist * g_autoOptParams.tp2_rr; + newTP3 = isBuy ? entry + slDist * g_autoOptParams.tp3_rr + : entry - slDist * g_autoOptParams.tp3_rr; + break; + + case TP_FIXED_RR: + default: + // Keep existing TP — just rescale to new SL distance + newTP1 = isBuy ? entry + slDist * g_workingMinRiskReward + : entry - slDist * g_workingMinRiskReward; + newTP2 = isBuy ? entry + slDist * g_autoOptParams.tp2_rr + : entry - slDist * g_autoOptParams.tp2_rr; + newTP3 = isBuy ? entry + slDist * g_autoOptParams.tp3_rr + : entry - slDist * g_autoOptParams.tp3_rr; + break; + } + + // Apply new TPs only if valid (correct side and meaningful distance) + if(newTP1 > 0) + { + bool tp1Side = isBuy ? (newTP1 > entry) : (newTP1 < entry); + if(tp1Side) + { + g_ea_signal.tp1 = newTP1; + if(newTP2 > 0) g_ea_signal.tp2 = newTP2; + if(newTP3 > 0) g_ea_signal.tp3 = newTP3; + } + } + + if(g_verboseLog) + PrintFormat("[FIX#502] SL/TP adjusted | scenario=%s slMethod=%d tpMethod=%d | SL: %.5f→%.5f | TP1: %.5f→%.5f", + g_scenarioProfile.description, + (int)g_scenarioProfile.slMethod, (int)g_scenarioProfile.tpMethod, + oldSL, g_ea_signal.stopLoss, + g_ea_signal.tp1, newTP1 > 0 ? newTP1 : g_ea_signal.tp1); + } +} + +void EA_ExecuteTrade() +{ + // * FIX#161: Direction filter -- EA_AllowBuy / EA_AllowSell + if(!EA_AllowBuy && g_ea_signal.isBullish) + { + if(g_verboseLog) + Print("[DIR_FILTER] BUY signal blocked (EA_AllowBuy=false)"); + return; + } + if(!EA_AllowSell && !g_ea_signal.isBullish) + { + if(g_verboseLog) + Print("[DIR_FILTER] SELL signal blocked (EA_AllowSell=false)"); + return; + } + g_ea_symbol.Name(_Symbol); + g_ea_symbol.RefreshRates(); + // Calculate lot size - * v7.0: Use pair-adapted risk + double total_lots = EA_FixedLotSize; + if(total_lots == 0) + { + // * v7.9 FIX BUG#2: Use g_smartEntry.recommendedLots when the full PositionSize system + // (Kelly Criterion, Regime Multiplier, Win/Loss Streak, Compounding, etc.) has run and + // produced a valid result. Previously this was 100% dead code -- the result was calculated + // but NEVER read, and EA_ExecuteTrade always did its own simple calculation. + // Now: if PosSize_Enabled AND g_posSizeValid AND SmartEntry ran -> use the full result. + // This activates Kelly, Regime, Streak and Compounding position sizing correctly. + if(PosSize_Enabled && g_posSizeValid && SmartEntry_Enabled && g_smartEntry.recommendedLots > 0) + { + total_lots = g_smartEntry.recommendedLots; + if(g_verboseLog) + Print("* v7.9 LotCalc [PosSize/Kelly]: RecommendedLots=", DoubleToString(total_lots, 4), + " | AdjRisk=", DoubleToString(g_smartEntry.recommendedRisk, 2), "%", + " | Multiplier=", DoubleToString(g_smartEntry.positionMultiplier, 3), + " | Kelly=", DoubleToString(g_smartEntry.kellyFraction, 3)); + } + else + { + // * FIX#P1b: Cap balance at BacktestInitialBalance (mirrors FIX#P1a in CalculatePositionSize). + // Without this cap the fallback path (when PosSize is disabled or not yet valid) still + // calculates lots on compounded equity — same Kelly compounding crash risk. + double _rawBal = AccountInfoDouble(ACCOUNT_BALANCE); + double balance = (g_effectiveBIB > 0 && _rawBal > g_effectiveBIB) + ? g_effectiveBIB : _rawBal; + // * Use pair-adapted risk percent if available + // * v10.29 FIX#317 → FIX#317b: start from g_workingRiskCeiling (= EA_RiskPercent για ALL TFs) + double riskPct = g_workingRiskCeiling; + // FIX#164: Profile risk only in AutoOpt mode; manual always uses working ceiling + if(AutoOpt_Enabled && g_gates.computed) + riskPct = MathMin(g_workingRiskCeiling, g_autoOptParams.risk_pct); + // * Apply SmartEntry adaptive sizing + if(SmartEntry_Enabled && SmartEntry_AdaptiveSize) + { + double posMult = GetAdaptivePositionMultiplier(g_smartEntry.finalConfidence); // * FIX v7.3: Current trade confidence + riskPct *= posMult; + } + // * FIX#108: Score-aware risk ceiling in fallback lot calculation. + // BEFORE: riskCeiling = EA_RiskPercent ALWAYS -> A+ score boost (x1.40) was killed. + // A+ trade: 3% x 1.40 = 4.2% -> capped to 3% -> 0 reward for excellent score. + // D trade: 3% x 0.80 = 2.4% -> penalty worked fine (asymmetric = unfair). + // AFTER: ceiling matches the score tier, same as PosSize/Kelly path and AggRiskCap block. + // A+ (>=90): ceiling = EA_AggRiskCap_APlus (default 5%) -> 3% x 1.40 = 4.2% -> allowed + // A (>=72): ceiling = EA_AggRiskCap_A (default 4%) -> 3% x 1.30 = 3.9% -> allowed + // B/C/D: ceiling = EA_RiskPercent (original safe behavior, unchanged) + int _fallbackScore = (int)g_ea_signal.score; + double riskCeiling; + string tierLabel = ""; + // * FIX#317b: base ceiling = g_workingRiskCeiling (= EA_RiskPercent, user-controlled) + // Mirror the main CalculatePositionSize ceiling: pair table risk[tf] × 1.50 + // g_autoOptParams.risk_pct holds cfg.risk[tf] (set by ApplyPairTFProfile). + double _fallbackPairRisk = (AutoOpt_Enabled && g_autoOptParams.risk_pct > 0) + ? g_autoOptParams.risk_pct : g_workingRiskCeiling; + if(_fallbackScore >= 90) { riskCeiling = _fallbackPairRisk * 1.50; tierLabel = "A+"; } + else if(_fallbackScore >= 72) { riskCeiling = _fallbackPairRisk * 1.30; tierLabel = "A"; } + else if(_fallbackScore >= 55) { riskCeiling = _fallbackPairRisk * 1.10; tierLabel = "B"; } + else { riskCeiling = _fallbackPairRisk; tierLabel = "C/D"; } + riskCeiling = MathMin(riskCeiling, g_workingRiskCeiling * 1.50); // never > 1.5× base + double riskFloor = AutoOpt_Enabled ? AutoOpt_MinRiskOverride : 0.1; + riskPct = MathMax(riskFloor, MathMin(riskCeiling, riskPct)); + if(g_verboseLog) + PrintFormat("* v7.9 RiskCalc: ceiling=%.2f%% | AfterAutoOpt=%.2f%% | EA_input=%.2f%% | Source=%s", + riskCeiling, riskPct, EA_RiskPercent, + g_gates.computed ? "PairThresholds" : "FIX#317_WorkingCeiling"); + double risk_money = balance * riskPct / 100.0; + double sl_points = MathAbs(g_ea_signal.entryPrice - g_ea_signal.stopLoss) / g_ea_symbol.Point(); + double tick_value = g_ea_symbol.TickValue(); + // * v7.5c FIX: USE OrderCalcProfit() INSTEAD OF TickValue() + // TickValue() returns $0.01 for US500 but REAL value is $1.00 (100x error!) + // This caused 7 lots where 0.07 was correct -> instant stop-out + // OrderCalcProfit() correctly accounts for contract size, currency conversion, etc. + ENUM_ORDER_TYPE calc_ord_type = g_ea_signal.isBullish ? ORDER_TYPE_BUY : ORDER_TYPE_SELL; + double calc_open = g_ea_signal.entryPrice; + double calc_close = g_ea_signal.stopLoss; + double loss_at_sl = 0; + bool calcOK = OrderCalcProfit(calc_ord_type, _Symbol, 1.0, calc_open, calc_close, loss_at_sl); + double loss_per_lot = MathAbs(loss_at_sl); + if(calcOK && loss_per_lot > 0) + { + total_lots = risk_money / loss_per_lot; + Print("* LotCalc [OrderCalcProfit]: Balance=", DoubleToString(balance, 0), + " | Risk%=", DoubleToString(riskPct, 2), + " | RiskMoney=$", DoubleToString(risk_money, 2), + " | SL_pts=", DoubleToString(sl_points, 1), + " | LossPerLot=$", DoubleToString(loss_per_lot, 2), + " | TickVal=$", DoubleToString(tick_value, 6), + " | RawLots=", DoubleToString(total_lots, 4)); + } + else if(sl_points > 0 && tick_value > 0) + { + // * v8.0 FIX: Index/CFD safety check on TickValue fallback + // For US100, XAUUSD, US500 etc: TickValue from SYMBOL_TRADE_TICK_VALUE returns $0.01 + // but the real value is $1.00 (100x error!) -- OrderCalcProfit handles this correctly. + // If we are here, OrderCalcProfit FAILED -> abort for indices instead of using wrong TickVal. + ENUM_SYMBOL_CALC_MODE calcMode = (ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_CALC_MODE); + bool isCFD = (calcMode == SYMBOL_CALC_MODE_CFD || calcMode == SYMBOL_CALC_MODE_CFDINDEX || calcMode == SYMBOL_CALC_MODE_CFDLEVERAGE); + if(isCFD && tick_value < 0.1) + { + // Dangerous: TickValue is suspiciously small for a CFD/Index -- 100x error risk + Print("* v8.0 ABORT: TickValue fallback UNSAFE for CFD/Index (TickVal=$", DoubleToString(tick_value, 6), + "). OrderCalcProfit failed -- cannot safely size position. Skipping trade."); + return; + } + // Fallback to tick_value method (forex pairs where OrderCalcProfit might fail) + total_lots = risk_money / (sl_points * tick_value); + Print("* LotCalc [TickValue fallback]: Balance=", DoubleToString(balance, 0), + " | Risk%=", DoubleToString(riskPct, 2), + " | RiskMoney=$", DoubleToString(risk_money, 2), + " | SL_pts=", DoubleToString(sl_points, 1), + " | TickVal=", DoubleToString(tick_value, 6), + " | RawLots=", DoubleToString(total_lots, 2)); + } + else + { + // [OK] FIX: If risk calculation fails, abort trade instead of using LotsMin + Print("* EA_ExecuteTrade ABORTED: Cannot calculate lot size (SL_points=", + DoubleToString(sl_points, 2), " TickValue=", DoubleToString(tick_value, 6), ")"); + return; + } + if(g_verboseLog) + Print("* Position Size: Risk=", DoubleToString(riskPct, 2), + "% | RiskMoney=", DoubleToString(risk_money, 2), + " | Lots=", DoubleToString(total_lots, 2)); + } // end else (PosSize/Kelly not used) + } // end if(total_lots == 0) + // Normalize + double lot_step = g_ea_symbol.LotsStep(); + total_lots = MathFloor(total_lots / lot_step) * lot_step; + total_lots = MathMax(g_ea_symbol.LotsMin(), MathMin(total_lots, g_ea_symbol.LotsMax())); + // * v7.5c FIX: MARGIN SAFETY CHECK + // Without this, tight SL on indices (21 points on US100) -> huge lots -> instant stop-out + // Example: $10k balance, 1% risk=$100, SL=21.65pts, TickVal=$0.01 -> 462 lots -> BLOWUP + // Fix: Check actual margin needed and cap at 30% of free margin max + { + double margin_needed = 0; + double free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); + double entry_price = g_ea_signal.isBullish ? g_ea_symbol.Ask() : g_ea_symbol.Bid(); + ENUM_ORDER_TYPE ord_type = g_ea_signal.isBullish ? ORDER_TYPE_BUY : ORDER_TYPE_SELL; + // * v9.08: Detect instrument type for cap decisions + ENUM_SYMBOL_CALC_MODE calcModeEQ = (ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_CALC_MODE); + bool isCFD_EQ = (calcModeEQ == SYMBOL_CALC_MODE_CFD || calcModeEQ == SYMBOL_CALC_MODE_CFDINDEX || calcModeEQ == SYMBOL_CALC_MODE_CFDLEVERAGE); + // * v8.0 FIX: multi_tp_factor removed -- was dead code (always 1.0 in both branches) + // For multi-TP: lots already represent per-position size, margin check uses total_lots directly + if(OrderCalcMargin(ord_type, _Symbol, total_lots, entry_price, margin_needed)) + { + // * v9.08 FIX#13: Margin cap 30%->50% for Forex (30% limited risk to ~2% when user wants 5%) + // CFD/Indices keep 30% (higher contract values, more dangerous) + double marginPct = isCFD_EQ ? 0.30 : 0.50; + double max_margin = free_margin * marginPct; + if(margin_needed > max_margin && max_margin > 0) + { + double scale = max_margin / margin_needed; + total_lots = MathFloor((total_lots * scale) / lot_step) * lot_step; + total_lots = MathMax(g_ea_symbol.LotsMin(), total_lots); + if(g_verboseLog) + Print("* v7.5c MARGIN CAP: Lots reduced from ", DoubleToString(total_lots / scale, 2), + " to ", DoubleToString(total_lots, 2), + " | Margin needed: $", DoubleToString(margin_needed, 0), + " > 30% free: $", DoubleToString(max_margin, 0)); + } + } + // * v9.08 FIX#13: EQUITY CAP -- Only for CFD/Indices (where TickValue is unreliable) + // Old: 5% equity cap for ALL instruments -> EURUSD capped at 0.42 lots on $10K + // -> Actual risk: 0.42 * 65pts * $1 = $27 = 0.27% (NOT 5%!) + // -> No compounding: lots stuck at 0.42 regardless of balance changes + // -> Evidence: 141 EQUITY CAP hits, every single trade capped + // Fix: Skip equity cap for Forex (margin cap at 50% is sufficient safety). + // Keep for CFD/indices where TickValue bugs could cause 100x oversizing. + if(isCFD_EQ) + { + // * Additional safety: Cap lots at 5% of equity / contract value + // This prevents catastrophic sizing even if TickValue is wrong + double equity = AccountInfoDouble(ACCOUNT_EQUITY); + double contract_value = entry_price * g_ea_symbol.ContractSize(); + if(contract_value > 0) + { + double max_lots_by_equity = (equity * 0.05) / (contract_value / 100.0); // 5% of equity with leverage + max_lots_by_equity = MathFloor(max_lots_by_equity / lot_step) * lot_step; + if(total_lots > max_lots_by_equity && max_lots_by_equity >= g_ea_symbol.LotsMin()) + { + if(g_verboseLog) + Print("* v7.5c EQUITY CAP [CFD]: Lots reduced from ", DoubleToString(total_lots, 2), + " to ", DoubleToString(max_lots_by_equity, 2), + " | Max 5% equity exposure"); + total_lots = max_lots_by_equity; + } + } + } + else if(g_verboseLog) + { + Print("* v9.08 EQUITY CAP SKIP [Forex]: ", DoubleToString(total_lots, 2), + " lots -- margin cap sufficient for Forex pairs"); + } + } + g_ea_trade.SetExpertMagicNumber(EA_MagicNumber); + // * FIX#469b: Live-specific trade object configuration. + // SetDeviationInPoints: allow up to 3 pips slippage on execution. + // Without this, MT5 uses broker default (often 0) → requotes on every fast move. + // 3 pips = reasonable for XAUUSD/EURUSD. Tighter pairs may need less. + // SetAsyncMode(false): synchronous execution — waits for broker confirmation. + // On multi-instance (M5+H4+D1 simultaneously), async can cause order overlap. + // Sync ensures each order completes before the next is sent. + if(!MQLInfoInteger(MQL_TESTER)) + { + g_ea_trade.SetDeviationInPoints(30); // 3 pips slippage tolerance + g_ea_trade.SetAsyncMode(false); // Sync: wait for confirmation + g_ea_trade.SetTypeFilling(ORDER_FILLING_FOK); // Fill or Kill — no partial fills + } + // * v9.27 FIX#96: Score-aware aggregate risk cap (was hardcoded 2.5% for ALL trades) + // A+ (score>=90): EA_AggRiskCap_APlus=5% | A (>=72): EA_AggRiskCap_A=4% | Others: EA_AggRiskCap_Default=2.5% + { + double balance = AccountInfoDouble(ACCOUNT_BALANCE); + // * FIX#P1b: Cap agg risk cap balance at BacktestInitialBalance (mirrors FIX#P1a) + if(g_effectiveBIB > 0 && balance > g_effectiveBIB) + balance = g_effectiveBIB; + int tradeScore = (int)g_ea_signal.score; + double maxRiskPct = (tradeScore >= 90) ? EA_AggRiskCap_APlus / 100.0 : + (tradeScore >= 72) ? EA_AggRiskCap_A / 100.0 : + EA_AggRiskCap_Default / 100.0; + double maxRiskDollar = balance * maxRiskPct; + double slPoints = MathAbs(g_ea_signal.entryPrice - g_ea_signal.stopLoss) / _Point; + double tickVal = g_ea_symbol.TickValue(); + if(tickVal <= 0) tickVal = 1.0; + double totalDollarRisk = total_lots * slPoints * tickVal; + if(totalDollarRisk > maxRiskDollar && maxRiskDollar > 0) + { + double scale = maxRiskDollar / totalDollarRisk; + total_lots = MathFloor((total_lots * scale) / lot_step) * lot_step; + total_lots = MathMax(g_ea_symbol.LotsMin(), total_lots); + if(g_verboseLog) + Print("* v9.11 FIX#26 RISK CAP: Aggregate $", DoubleToString(totalDollarRisk, 0), + " > 2.5% ($", DoubleToString(maxRiskDollar, 0), + ") -> lots scaled to ", DoubleToString(total_lots, 2)); + } + } + // * v6.39: Initialize modify error tracking + ZeroMemory(g_modifyStats); + bool success = false; + int trades_opened = 0; + // * v7.5c: Auto-disable MultipleTP when lots too small for meaningful split + // If total_lots < 4x min lot, splitting creates 3 x min_lot = no compounding benefit + // Better to use single position with full lots + bool useMultiTP = EA_UseMultipleTP; + double minLot = g_ea_symbol.LotsMin(); + if(useMultiTP && total_lots < minLot * 4) + { + useMultiTP = false; + Print("* v7.5c Auto Single-TP: total_lots=", DoubleToString(total_lots, 4), + " < ", DoubleToString(minLot * 4, 4), " (4xminLot) -> using single position for better compounding"); + } + // * v9.23 FIX#63: WEAK_TREND Regime Filter -- force single position + // Analysis: 80% of trades in WEAK_TREND. 3-pos WR=52.6% (random!), avg=-$19.79. + // Single position WR=93.8%, avg=+$13.18. Multi-pos in weak trend = systematic loss. + // Root cause: WEAK_TREND has frequent retracements -> trail closes TP2/TP3 prematurely + // while all 3 positions hit SL simultaneously on losses. + if(useMultiTP) + { + // --- FIX#192 (v9.48) -- REGIME FILTER CORRECTED: + // FIX#156 blocked TREND_UP/DOWN from Multi-TP → every TC BUY in uptrend became + // a binary single-position trade with 100-110p SL and no partial-close buffer. + // TREND is exactly the regime where TP2/TP3 can be reached (sustained directional move). + // New rule: block Multi-TP ONLY in CHOPPY + VOLATILE (no directional momentum). + // WEAK_TREND: configurable via InpMultiTP_AllowWeakTrend (default=true). + // CHOPPY/VOLATILE stay single-pos because they lack directional momentum for TP2/TP3. --- + ENUM_MARKET_REGIME curRegime = g_regimeData.regime; + bool forcesSingle = (curRegime == REGIME_CHOPPY || + curRegime == REGIME_VOLATILE); + // WEAK_TREND: allow Multi-TP (directional bias exists, just weaker) + // Previously blocked by FIX#63 — re-enabled by FIX#192 + if(forcesSingle) + { + useMultiTP = false; + Print("* FIX#192 REGIME FILTER: Forcing single position | regime=", + EnumToString(curRegime), + " | CHOPPY/VOLATILE = no directional momentum for TP2/TP3"); + } + } + // * FIX#304: per-TF max positions cap. + // M15 EURUSD: max_positions=1 — 3-deal groups caused -$1,617 in EA2026 backtest. + // cap=1 forces single position (full lot, no split), preserving same $ risk. + if(useMultiTP && g_autoOptParams.max_positions_per_signal == 1) + { + useMultiTP = false; + if(g_verboseLog) + PrintFormat("[FIX#304] MAX POSITIONS CAP=1: single position forced (TF=%s)", + EnumToString(_Period)); + } + // [OK] MULTIPLE TP LOGIC + if(useMultiTP) + { + // Υπολογισμός lot sizes για κάθε TP + // * FIX#MERGE_C3: LOTS MUST MATCH TP PERCENTAGES. + // BUG: Equal 33%/33%/33% split but AddMultiTPEntry tracks tp1Lots=50%, tp2Lots=25%, tp3Lots=25%. + // On TP1 close, CloseTranchePosition found a 33% position but tracker expected 50% → mismatch. + // Result: TP2/TP3 showed +0.0R across all 9 hits combined because wrong lot sizes were closed. + // FIX: Open each tranche proportional to its TP close percentage from g_workingTP1/2/3_Pct. + double pct1 = (g_workingTP1_Pct > 0) ? g_workingTP1_Pct / 100.0 : 0.50; + double pct2 = (g_workingTP2_Pct > 0) ? g_workingTP2_Pct / 100.0 : 0.25; + double pct3 = (g_workingTP3_Pct > 0) ? g_workingTP3_Pct / 100.0 : 0.25; + double lot1 = MathFloor((total_lots * pct1) / lot_step) * lot_step; + double lot2 = MathFloor((total_lots * pct2) / lot_step) * lot_step; + double lot3 = MathFloor((total_lots * pct3) / lot_step) * lot_step; + lot1 = MathMax(g_ea_symbol.LotsMin(), lot1); + lot2 = MathMax(g_ea_symbol.LotsMin(), lot2); + lot3 = MathMax(g_ea_symbol.LotsMin(), lot3); + // [OK] FIX: Overflow protection - ensure sum doesn't exceed total_lots + double lot_sum = lot1 + lot2 + lot3; + if(lot_sum > total_lots) + { + // If even minimum lots exceed total, reduce to 2 or 1 position + double min3 = g_ea_symbol.LotsMin() * 3; + double min2 = g_ea_symbol.LotsMin() * 2; + if(total_lots >= min3) + { + // Proportionally scale down + double scale = total_lots / lot_sum; + lot1 = MathFloor((lot1 * scale) / lot_step) * lot_step; + lot2 = MathFloor((lot2 * scale) / lot_step) * lot_step; + lot3 = MathFloor((lot3 * scale) / lot_step) * lot_step; + lot1 = MathMax(g_ea_symbol.LotsMin(), lot1); + lot2 = MathMax(g_ea_symbol.LotsMin(), lot2); + lot3 = MathMax(g_ea_symbol.LotsMin(), lot3); + // Final check: if still over, reduce lot3 + if(lot1 + lot2 + lot3 > total_lots) + lot3 = MathFloor((total_lots - lot1 - lot2) / lot_step) * lot_step; + if(lot3 < g_ea_symbol.LotsMin()) lot3 = 0; + } + else if(total_lots >= min2) + { + // Only 2 positions + lot1 = g_ea_symbol.LotsMin(); + lot2 = MathFloor((total_lots - lot1) / lot_step) * lot_step; + lot2 = MathMax(g_ea_symbol.LotsMin(), lot2); + lot3 = 0; + } + else + { + // Single position only + lot1 = total_lots; + lot2 = 0; + lot3 = 0; + } + if(g_verboseLog) + Print("* Multi-TP lot overflow corrected: ", DoubleToString(lot_sum, 2), + " -> ", DoubleToString(lot1 + lot2 + lot3, 2), " (max=", DoubleToString(total_lots, 2), ")"); + } + string comment_base = EA_TradeComment + "_" + g_ea_signal.type; + // * v9.16: TP2/TP3 open with TP=0 (NO fixed TP) -- trail-only tranches + // TP1 keeps fixed TP as "bonus" target, but also trails from 0.3R + // Cascading SL ensures TP2/TP3 NEVER lose after TP1 hits + // * v9.23 FIX#64: TP2/TP3 NOW have fixed TPs (g_ea_signal.tp2/tp3) + // Evidence: 0 TP2 hits, 0 TP3 hits in 64 trades when TPs were 0. + // Real TP prices already calculated by BuildCandidateSLTP/AutoOpt. + // Trail remains active as fallback, but fixed TP is the primary target. + // Open positions + if(g_ea_signal.isBullish) + { + // * FIX#469c: Live retry on requote/reject. + // In live trading, fast markets cause requotes (10004) or temporary rejects (10006/10007). + // Without retry, valid signals are silently skipped. + // Max 3 retries with 100ms pause — enough for broker to refresh quote. + // In tester: no retry needed (no real latency). + // Position 1 - TP1 (fixed TP + trail) + if(lot1 >= g_ea_symbol.LotsMin()) + { + for(int _r1=0; _r1<(g_liveMode?3:1); _r1++) + { + if(g_ea_trade.Buy(lot1, _Symbol, g_ea_symbol.Ask(), + g_ea_signal.stopLoss, g_ea_signal.tp1, + comment_base + "_TP1")) + { trades_opened++; break; } + if(g_liveMode) { int _rc=(int)g_ea_trade.ResultRetcode(); + if(_rc!=10004 && _rc!=10006 && _rc!=10007) break; + Sleep(100); g_ea_symbol.RefreshRates(); } + } + } + // Position 2 - TP2 + if(lot2 >= g_ea_symbol.LotsMin()) + { + for(int _r2=0; _r2<(g_liveMode?3:1); _r2++) + { + if(g_ea_trade.Buy(lot2, _Symbol, g_ea_symbol.Ask(), + g_ea_signal.stopLoss, g_ea_signal.tp2, + comment_base + "_TP2")) + { trades_opened++; break; } + if(g_liveMode) { int _rc=(int)g_ea_trade.ResultRetcode(); + if(_rc!=10004 && _rc!=10006 && _rc!=10007) break; + Sleep(100); g_ea_symbol.RefreshRates(); } + } + } + // Position 3 - TP3 + if(lot3 >= g_ea_symbol.LotsMin()) + { + for(int _r3=0; _r3<(g_liveMode?3:1); _r3++) + { + if(g_ea_trade.Buy(lot3, _Symbol, g_ea_symbol.Ask(), + g_ea_signal.stopLoss, g_ea_signal.tp3, + comment_base + "_TP3")) + { trades_opened++; break; } + if(g_liveMode) { int _rc=(int)g_ea_trade.ResultRetcode(); + if(_rc!=10004 && _rc!=10006 && _rc!=10007) break; + Sleep(100); g_ea_symbol.RefreshRates(); } + } + } + } + else // Bearish + { + // Position 1 - TP1 SELL + if(lot1 >= g_ea_symbol.LotsMin()) + { + for(int _rs1=0; _rs1<(g_liveMode?3:1); _rs1++) + { + if(g_ea_trade.Sell(lot1, _Symbol, g_ea_symbol.Bid(), + g_ea_signal.stopLoss, g_ea_signal.tp1, + comment_base + "_TP1")) + { trades_opened++; break; } + if(g_liveMode) { int _rc=(int)g_ea_trade.ResultRetcode(); + if(_rc!=10004 && _rc!=10006 && _rc!=10007) break; + Sleep(100); g_ea_symbol.RefreshRates(); } + } + } + // Position 2 - TP2 SELL + if(lot2 >= g_ea_symbol.LotsMin()) + { + for(int _rs2=0; _rs2<(g_liveMode?3:1); _rs2++) + { + if(g_ea_trade.Sell(lot2, _Symbol, g_ea_symbol.Bid(), + g_ea_signal.stopLoss, g_ea_signal.tp2, + comment_base + "_TP2")) + { trades_opened++; break; } + if(g_liveMode) { int _rc=(int)g_ea_trade.ResultRetcode(); + if(_rc!=10004 && _rc!=10006 && _rc!=10007) break; + Sleep(100); g_ea_symbol.RefreshRates(); } + } + } + // Position 3 - TP3 SELL + if(lot3 >= g_ea_symbol.LotsMin()) + { + for(int _rs3=0; _rs3<(g_liveMode?3:1); _rs3++) + { + if(g_ea_trade.Sell(lot3, _Symbol, g_ea_symbol.Bid(), + g_ea_signal.stopLoss, g_ea_signal.tp3, + comment_base + "_TP3")) + { trades_opened++; break; } + if(g_liveMode) { int _rc=(int)g_ea_trade.ResultRetcode(); + if(_rc!=10004 && _rc!=10006 && _rc!=10007) break; + Sleep(100); g_ea_symbol.RefreshRates(); } + } + } + } + success = (trades_opened > 0); + } + else // Single position + { + // Single position gets _TP1 suffix so peak trail and DOL updates work correctly. + string _spComment = EA_TradeComment + "_" + g_ea_signal.type + "_TP1"; + if(g_ea_signal.isBullish) + { + success = g_ea_trade.Buy(total_lots, _Symbol, g_ea_symbol.Ask(), + g_ea_signal.stopLoss, g_ea_signal.tp1, + _spComment); + } + else + { + success = g_ea_trade.Sell(total_lots, _Symbol, g_ea_symbol.Bid(), + g_ea_signal.stopLoss, g_ea_signal.tp1, + _spComment); + } + if(success) trades_opened = 1; + } + if(success) + { + // * v9.09 FIX#14b: Multi-TP positions are ONE signal -> count as 1 daily trade + g_ea_stats.trades += 1; + g_tradesThisDay += 1; + // * v9.34 FIX#132: Increment per-regime counter (used by regime throttle) + g_tradesThisRegimePeriod += 1; + // * FIX#5: Register trade with MultiTP tracker so statistics are correctly counted. + // Previously AddMultiTPEntry was only called in legacy CreateSignal branches (lines + // 15754/15860 etc.) but EA_ExecuteTrade is the REAL execution path -- MultiTP never + // received any entries through here, resulting in 0/1 recorded vs 170 actual trades. + // * v9.11 FIX#18: Pass ACTUAL order TPs so tracker matches real positions. + // Before: tracker recalculated from InpTP_RR (R:R-based) -> different from ATR-based orders! + if(EA_UseMultipleTP) + { + // * v9.46 FIX#189: Pass actual MT5 position ticket so FIX#184 (perTrade_Trail_RR) + // and FIX#100 lookups work correctly in ProfitGuardTrail / SmartExit. + // ROOT CAUSE: was passing 0 → g_multiTPEntries[x].ticket stored as 0 → + // ticket-based lookup in ProfitGuardTrail never matched → fell back to global + // EA_Trail_Activation_RR=0.3R → trail fired at 0.63R instead of 0.99R (FIX#100 value). + // FIX: g_ea_trade.ResultOrder() returns the ticket of the last opened position. + ulong _posTicketFIX189 = g_ea_trade.ResultOrder(); + // Fallback: scan positions for matching entry price + direction + magic + if(_posTicketFIX189 == 0) + { + for(int _ps = PositionsTotal()-1; _ps >= 0; _ps--) + { + ulong _pk = PositionGetTicket(_ps); + if(_pk == 0) continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; + if((long)PositionGetInteger(POSITION_MAGIC) != EA_MagicNumber) continue; + bool _sd = (g_ea_signal.isBullish && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) || + (!g_ea_signal.isBullish && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL); + if(_sd && MathAbs(PositionGetDouble(POSITION_PRICE_OPEN) - g_ea_signal.entryPrice) < g_cachedATR * 0.1) + { _posTicketFIX189 = _pk; break; } + } + } + int _dir = g_ea_signal.isBullish ? 1 : -1; + AddMultiTPEntry(g_ea_signal.entryPrice, g_ea_signal.stopLoss, _dir, total_lots, (long)_posTicketFIX189, + g_ea_signal.tp1, g_ea_signal.tp2, g_ea_signal.tp3); + // * v9.31 FIX#104c: Mark counter-trend trades for early cut monitoring + if(g_ea_signal.isCounterTrend) + { + int last_mtp = ArraySize(g_multiTPEntries)-1; + for(int _mf=last_mtp; _mf>=0; _mf--) + { + if(g_multiTPEntries[_mf].active && + MathAbs(g_multiTPEntries[_mf].entryPrice - g_ea_signal.entryPrice) < g_pipValue*3) + { + g_multiTPEntries[_mf].isCounterTrend = true; + g_multiTPEntries[_mf].entryConfirmTime = TimeCurrent(); + break; + } + } + } + } + Print("Trade(s) opened: ", g_ea_signal.type, + " | Direction: ", g_ea_signal.isBullish ? "BUY" : "SELL", + " | Score: ", g_ea_signal.score, + " | Positions: ", trades_opened, + " | DailyCount: ", g_ea_stats.trades, "/", EA_MaxDailyTrades); + } + else + { + Print("Failed to open trade. Error: ", GetLastError()); + } +} +//+------------------------------------------------------------------+ +//| Manage Positions | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| * v6.33: Track Trade Results from Deal History | +//| Scans closed deals and updates g_historicalWinRate | +//| * v9.13 FIX#23c v2: Per-entry-group buffering with position ID | +//| tracking. Deals are grouped by their DEAL_POSITION_ID -- all | +//| partial closes from the same MT5 position go to the same group. | +//| A group flushes when ALL its tracked positions are closed. | +//| Safe even with EA_MaxOpenTrades > 1 (concurrent entries). | +//+------------------------------------------------------------------+ +// * v9.13 FIX#23c v2: Entry group buffer -- groups deals by entry event +struct EntryGroupBuffer +{ + double totalPnL; // Sum of netProfit across all partial closes + double totalRMultiple; // Sum of R-multiples + int dealCount; // Number of deals in this group + datetime firstDealTime; // Time of first deal (for timeout) + ulong positionIDs[6]; // Position IDs contributing to this group (max 6 legs) + int posIDCount; // Number of tracked position IDs + bool active; // Is this group waiting for flush? + // * v9.49 FIX#204: Peak RR tracking — highest RR reached while trade was open. + // ROOT CAUSE: AutoOpt learned only WIN/LOSS but never knew HOW FAR price moved. + // A trade that reached +2.5R then reversed to SL was classified identically to + // one that hit SL immediately. AutoOpt could not distinguish "TP3 was reachable" + // from "trade never had a chance". This caused AutoOpt to keep TP targets conservatively + // low because it saw losses from trades that were actually high-quality setups. + // Fix: track peakRR per group, pass to UpdateHistoricalPerformance, store separately. + double peakRR; // Highest R:R reached by any position in this group while open + // * FIX#469b: entryPrice for robust peakRR propagation fallback. + // Lets live peakRR update match entryGroup by price when positionID lookup fails. + double entryPrice; // Entry price of first deal in this group +}; +// * Max 4 concurrent entry groups (MaxOpenTrades=1 means usually 1, but safe to 4) +#define MAX_ENTRY_GROUPS 4 +static EntryGroupBuffer g_entryGroups[MAX_ENTRY_GROUPS]; +static bool g_entryGroupsInitialized = false; +void EA_UpdateTradeResults() +{ + static datetime lastCheckTime = 0; + static int lastDealsTotal = 0; + // One-time init + if(!g_entryGroupsInitialized) + { + for(int g = 0; g < MAX_ENTRY_GROUPS; g++) + { + g_entryGroups[g].totalPnL = 0; + g_entryGroups[g].totalRMultiple = 0; + g_entryGroups[g].dealCount = 0; + g_entryGroups[g].firstDealTime = 0; + g_entryGroups[g].posIDCount = 0; + g_entryGroups[g].active = false; + } + g_entryGroupsInitialized = true; + } + // Only check every 5 seconds (not every tick) + if(TimeCurrent() - lastCheckTime < 5) return; + lastCheckTime = TimeCurrent(); + // Check if there are new deals + // * v8.01 FIX BUG#1: 90-day window instead of full history scan + datetime histStart = TimeCurrent() - (datetime)(90 * 86400); + if(!HistorySelect(histStart, TimeCurrent())) return; + int dealsTotal = HistoryDealsTotal(); + if(dealsTotal != lastDealsTotal) + { + // Scan new deals since last check + for(int i = lastDealsTotal; i < dealsTotal; i++) + { + ulong dealTicket = HistoryDealGetTicket(i); + if(dealTicket == 0) continue; + long dealMagic = HistoryDealGetInteger(dealTicket, DEAL_MAGIC); + if(dealMagic != EA_MagicNumber) continue; + long dealEntry = HistoryDealGetInteger(dealTicket, DEAL_ENTRY); + if(dealEntry != DEAL_ENTRY_OUT && dealEntry != DEAL_ENTRY_INOUT) continue; + string dealSymbol = HistoryDealGetString(dealTicket, DEAL_SYMBOL); + if(dealSymbol != _Symbol) continue; + double dealProfit = HistoryDealGetDouble(dealTicket, DEAL_PROFIT); + double dealSwap = HistoryDealGetDouble(dealTicket, DEAL_SWAP); + double dealCommission = HistoryDealGetDouble(dealTicket, DEAL_COMMISSION); + double netProfit = dealProfit + dealSwap + dealCommission; + // * FIX#385 requires dealPosID and dealTime — declare them here before use + ulong dealPosID = (ulong)HistoryDealGetInteger(dealTicket, DEAL_POSITION_ID); + datetime dealTime = (datetime)HistoryDealGetInteger(dealTicket, DEAL_TIME); + double rMultiple = 0; + double dealVolume = HistoryDealGetDouble(dealTicket, DEAL_VOLUME); + // * FIX#385: Use sl_dist_cached (ATR at entry) instead of live g_cachedATR. + // BUG: g_cachedATR at close time ≠ ATR at entry → distorts R-multiple fed to Kelly. + // FIX: scan g_multiTPEntries for matching position to get the cached SL distance. + // Fallback: live ATR × SL mult (old behaviour) if no match found. + if(dealVolume > 0) + { + double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); + double slDistPoints = 0; + // Try to find matching MultiTP entry by position ID or proximity + for(int _rr = 0; _rr < ArraySize(g_multiTPEntries); _rr++) + { + if(g_multiTPEntries[_rr].sl_dist_cached > 0 && + (g_multiTPEntries[_rr].ticket == dealPosID || + MathAbs((long)(g_multiTPEntries[_rr].entryTime - dealTime)) < 120)) + { + slDistPoints = g_multiTPEntries[_rr].sl_dist_cached / _Point; + break; + } + } + // Fallback to live ATR estimate if no match + if(slDistPoints <= 0 && g_cachedATR > 0) + slDistPoints = GetActiveSLMult() * g_cachedATR / _Point; + double riskMoney = slDistPoints * tickValue * dealVolume; + if(riskMoney > 0) + rMultiple = netProfit / riskMoney; + } + // * v9.13 FIX#23c v2: dealPosID and dealTime already declared above (FIX#385) + // Find existing group that contains this position ID, or a group + // opened within 60 seconds (Multi-TP legs open near-simultaneously) + int groupIdx = -1; + for(int g = 0; g < MAX_ENTRY_GROUPS; g++) + { + if(!g_entryGroups[g].active) continue; + // Check if this position ID is already tracked in this group + for(int p = 0; p < g_entryGroups[g].posIDCount; p++) + { + if(g_entryGroups[g].positionIDs[p] == dealPosID) + { + groupIdx = g; + break; + } + } + if(groupIdx >= 0) break; + // Check time proximity: deals within 60s of first deal = same entry event + if(g_entryGroups[g].firstDealTime > 0 && + MathAbs((long)(dealTime - g_entryGroups[g].firstDealTime)) < 60) + { + groupIdx = g; + break; + } + } + // No existing group found -> allocate new one + if(groupIdx < 0) + { + for(int g = 0; g < MAX_ENTRY_GROUPS; g++) + { + if(!g_entryGroups[g].active) + { + groupIdx = g; + g_entryGroups[g].active = true; + g_entryGroups[g].totalPnL = 0; + g_entryGroups[g].totalRMultiple = 0; + g_entryGroups[g].dealCount = 0; + g_entryGroups[g].firstDealTime = dealTime; + g_entryGroups[g].posIDCount = 0; + g_entryGroups[g].peakRR = 0.0; // * FIX#204 + // * FIX#470: Seed entryPrice from DEAL_ENTRY_IN so FIX#469b live-update + // fallback in ManagePositions can match by price. Without this, entryPrice=0 + // in every normally-allocated group → FIX#469b match always fails → + // g_entryGroups[].peakRR never written live → PeakRR: 0.00 on every close. + // Also seed peakRR immediately from g_multiTPEntries (live-tracked value): + // at close time the position is already gone but multiTPEntries still holds + // the highest RR reached. This guarantees a non-zero peakRR even if the + // live propagation path failed every tick. + { + // Get entry price from the DEAL_ENTRY_IN deal for this position + int _hTotalFx = HistoryDealsTotal(); + for(int _hdFx = 0; _hdFx < _hTotalFx; _hdFx++) + { + ulong _hTktFx = HistoryDealGetTicket(_hdFx); + if(_hTktFx == 0) continue; + if((ulong)HistoryDealGetInteger(_hTktFx, DEAL_POSITION_ID) == dealPosID && + HistoryDealGetInteger(_hTktFx, DEAL_ENTRY) == DEAL_ENTRY_IN) + { + g_entryGroups[g].entryPrice = HistoryDealGetDouble(_hTktFx, DEAL_PRICE); + break; + } + } + // Seed peakRR from multiTPEntries (live-tracked, best value available) + int _mtpSz = ArraySize(g_multiTPEntries); + for(int _mtFx = 0; _mtFx < _mtpSz; _mtFx++) + { + bool _tickMatch = ((ulong)g_multiTPEntries[_mtFx].ticket == dealPosID); + bool _timeMatch = (MathAbs((long)(g_multiTPEntries[_mtFx].entryTime - dealTime)) < 300); + bool _priceMatch = (g_entryGroups[g].entryPrice > 0 && + MathAbs(g_multiTPEntries[_mtFx].entryPrice - g_entryGroups[g].entryPrice) < g_pipValue * 5); + if(_tickMatch || _timeMatch || _priceMatch) + { + if(g_multiTPEntries[_mtFx].peakRR > g_entryGroups[g].peakRR) + g_entryGroups[g].peakRR = g_multiTPEntries[_mtFx].peakRR; + } + } + } + break; + } + } + // All groups full -> flush oldest (safety valve) + if(groupIdx < 0) + { + groupIdx = 0; + datetime oldest = g_entryGroups[0].firstDealTime; + for(int g = 1; g < MAX_ENTRY_GROUPS; g++) + { + if(g_entryGroups[g].firstDealTime < oldest) + { + oldest = g_entryGroups[g].firstDealTime; + groupIdx = g; + } + } + // Force-flush the oldest group + if(g_entryGroups[groupIdx].dealCount > 0) + { + bool fWin = (g_entryGroups[groupIdx].totalPnL > 0); + double fR = g_entryGroups[groupIdx].totalRMultiple / MathMax(1, g_entryGroups[groupIdx].dealCount); + UpdateHistoricalPerformance(fWin, fR, g_entryGroups[groupIdx].peakRR); // * FIX#204 + Print("* v9.13 FIX#23c: FORCE-FLUSH oldest group | ", g_entryGroups[groupIdx].dealCount, + " deals | Net: ", DoubleToString(g_entryGroups[groupIdx].totalPnL, 2), + " | ", fWin ? "WIN" : "LOSS"); + } + g_entryGroups[groupIdx].active = true; + g_entryGroups[groupIdx].totalPnL = 0; + g_entryGroups[groupIdx].totalRMultiple = 0; + g_entryGroups[groupIdx].dealCount = 0; + g_entryGroups[groupIdx].firstDealTime = dealTime; + g_entryGroups[groupIdx].posIDCount = 0; + g_entryGroups[groupIdx].peakRR = 0.0; // * FIX#204 + // * FIX#469b: capture deal entry price for fallback matching + g_entryGroups[groupIdx].entryPrice = HistoryDealGetDouble(dealTicket, DEAL_PRICE); + } + } + // Add deal to group + g_entryGroups[groupIdx].totalPnL += netProfit; + g_entryGroups[groupIdx].totalRMultiple += rMultiple; + g_entryGroups[groupIdx].dealCount++; + // * v9.49 FIX#204: Capture peakRR from MultiTPEntry registry for this position. + // The peakRR field in g_multiTPEntries[_pk].peakRR is updated every bar by ManagePositions + // (FIX#150). When the deal closes, read the final peak and store in the group. + // This ensures AutoOpt learns HOW FAR the trade moved regardless of whether it won or lost. + // * FIX#456: g_entryGroups[groupIdx].peakRR is now updated live on every tick + // in EA_ManagePositions() by propagating directly from g_multiTPEntries[_mtp_idx].peakRR + // via positionID match. No post-close lookup needed — value is already correct here. + // Track position ID if not already in this group + bool posIDFound = false; + for(int p = 0; p < g_entryGroups[groupIdx].posIDCount; p++) + { + if(g_entryGroups[groupIdx].positionIDs[p] == dealPosID) + { + posIDFound = true; + break; + } + } + if(!posIDFound && g_entryGroups[groupIdx].posIDCount < 6) + { + g_entryGroups[groupIdx].positionIDs[g_entryGroups[groupIdx].posIDCount] = dealPosID; + g_entryGroups[groupIdx].posIDCount++; + } + if(g_verboseLog) + Print("* v9.13 Deal buffered -> group ", groupIdx, + " | PosID=", dealPosID, + " | Profit: ", DoubleToString(netProfit, 2), + " | GroupDeals: ", g_entryGroups[groupIdx].dealCount, + " | GroupNet: ", DoubleToString(g_entryGroups[groupIdx].totalPnL, 2)); + // * FIX#112: EARLY-CLOSE CASCADE TRIGGER + // Problem: When TP1 tranche closes via TRAIL (not TP price hit), OnTP1Hit() + // is never called → tp1Hit stays FALSE → cascade SL never fires for TP2/TP3. + // Evidence: Jan02 18:05 BUY trade - #4(TP1) trailed out at 1.17489 (profit), + // tp1Price=1.17582 never reached → #5 and #6 kept original SL=1.17358 + // for 46 minutes → SL hit → -$143.38 + // Fix: Scan deal comment for "_TP1". If TP1 tranche closed at profit via + // any mechanism (trail/smartexit/adverse), find matching MultiTP entry, + // set tp1Hit=true, and immediately apply cascade SL to all remaining positions. + // This mirrors exactly what OnTP1Hit() does but triggered by deal history, + // not by price level check. + if(InpEnableMultiTP && netProfit > 0) + { + string dComment = HistoryDealGetString(dealTicket, DEAL_COMMENT); + bool dealIsTP1 = (StringFind(dComment, "_TP1") >= 0); + if(dealIsTP1) + { + // * FIX#112b: Match via DEAL_POSITION_ID → HistoryDeal entry price + // DEAL_PRICE for an OUT deal is the close price, NOT the entry. + // To get entry price, look up the IN deal with the same position ID. + ulong closePosID = (ulong)HistoryDealGetInteger(dealTicket, DEAL_POSITION_ID); + double dealEntryPrice = 0; + // Scan history for IN deal with same position ID + int histTotal = HistoryDealsTotal(); + for(int _hd = 0; _hd < histTotal; _hd++) + { + ulong hTkt = HistoryDealGetTicket(_hd); + if(HistoryDealGetInteger(hTkt, DEAL_POSITION_ID) == closePosID && + HistoryDealGetInteger(hTkt, DEAL_ENTRY) == DEAL_ENTRY_IN) + { + dealEntryPrice = HistoryDealGetDouble(hTkt, DEAL_PRICE); + break; + } + } + if(dealEntryPrice == 0) dealEntryPrice = HistoryDealGetDouble(dealTicket, DEAL_PRICE); // fallback + for(int _m = 0; _m < ArraySize(g_multiTPEntries); _m++) + { + if(!g_multiTPEntries[_m].active && !g_multiTPEntries[_m].tp1Hit) continue; + if(g_multiTPEntries[_m].tp1Hit) continue; // already set + if(MathAbs(g_multiTPEntries[_m].tp1Price - 0) < _Point) continue; + // Match by entry price (IN deal price vs recorded entry) + if(MathAbs(g_multiTPEntries[_m].entryPrice - dealEntryPrice) > g_pipValue * 3) + continue; + // Set tp1Hit so cascade fires next ManagePositions tick + g_multiTPEntries[_m].tp1Hit = true; + g_multiTPEntries[_m].tp1HitTime = dealTime; + Print("* FIX#112: TP1 EARLY-CLOSE DETECTED | ID=", g_multiTPEntries[_m].id, + " | TP1 closed via trail/exit at profit (not TP price)", + " | tp1Hit=true → cascade SL will fire next bar", + " | Comment=", dComment); + break; + } + } + } + } + lastDealsTotal = dealsTotal; + } + // * v9.13 FIX#23c v2: Check each group -- flush when ALL its positions are closed + for(int g = 0; g < MAX_ENTRY_GROUPS; g++) + { + if(!g_entryGroups[g].active || g_entryGroups[g].dealCount == 0) continue; + // Check if ANY tracked position in this group is still open + bool groupHasOpenPos = false; + for(int pid = 0; pid < g_entryGroups[g].posIDCount; pid++) + { + // Scan open positions for this position ID + for(int p = PositionsTotal() - 1; p >= 0; p--) + { + ulong pTicket = PositionGetTicket(p); + if(pTicket == 0) continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; + if(PositionGetInteger(POSITION_MAGIC) != EA_MagicNumber) continue; + // MT5: position ticket IS the position ID for netting accounts, + // or matches DEAL_POSITION_ID for hedging accounts + if(pTicket == g_entryGroups[g].positionIDs[pid] || + (ulong)PositionGetInteger(POSITION_IDENTIFIER) == g_entryGroups[g].positionIDs[pid]) + { + groupHasOpenPos = true; + break; + } + } + if(groupHasOpenPos) break; + } + // Timeout safety: force flush stale groups. + // * FIX#469a: Was 86400s (24h). D1 trades routinely last 2-5 days → group flushed + // WHILE position still open → active=false → live peakRR update skipped → + // peakRR=0.00 at every trade close. Fix: 30-day timeout for D1, 3-day for others. + int _flushTimeout = (_Period >= PERIOD_D1) ? 86400 * 30 : 86400 * 3; + bool timedOut = (TimeCurrent() - g_entryGroups[g].firstDealTime > _flushTimeout); + if(!groupHasOpenPos || timedOut) + { + bool isWin = (g_entryGroups[g].totalPnL > 0); + double avgR = g_entryGroups[g].totalRMultiple / MathMax(1, g_entryGroups[g].dealCount); + UpdateHistoricalPerformance(isWin, avgR, g_entryGroups[g].peakRR); // * FIX#204: pass peakRR + // * FIX#448b: Update totalProfitPips/totalLossPips from R-multiple × ATR proxy. + // ROOT: FIX#23c path never called UpdatePerformanceFromTrade → pips stayed 0 forever. + // Performance summary showed "Net Pips: 0.0" and Expectancy: 0. Not a trading bug + // but corrupts the performance display and AutoOpt statistics. + // FIX: approximate pips as avgR × (ATR in pips). ATR is in price units; divide by pipValue. + { + double _pipsSE = (g_cachedATR > 0 && g_pipValue > 0) ? avgR * (g_cachedATR / g_pipValue) : 0; + if(isWin) g_perfData.totalProfitPips += MathAbs(_pipsSE); + else g_perfData.totalLossPips += MathAbs(_pipsSE); + } + // * FIX#STATS_ZERO: Also update g_perfData counters (totalTrades/wins/losses). + // PROBLEM: FIX#23c called only UpdateHistoricalPerformance() which updates win-rate arrays + // but NOT g_perfData.totalTrades. At deinit: "Total Trades: 0" even after 5 entry groups. + // The AddTradeRecord path (which calls UpdatePerformanceFromTrade → totalTrades++) was never + // called from the FIX#23c group close handler. + g_perfData.totalTrades++; + if(isWin) g_perfData.totalWins++; else g_perfData.totalLosses++; + g_backtestResults.totalTrades++; + if(isWin) g_backtestResults.winningTrades++; else g_backtestResults.losingTrades++; + g_backtestResults.netProfit += g_entryGroups[g].totalPnL; + // * FIX#448a: grossProfit/grossLoss were never updated → Profit Factor = 0 always. + if(isWin) g_backtestResults.grossProfit += g_entryGroups[g].totalPnL; + else g_backtestResults.grossLoss += MathAbs(g_entryGroups[g].totalPnL); + Print("* v9.13 FIX#23c: Entry group ", g, " closed", + timedOut ? " (TIMEOUT)" : "", + " | ", g_entryGroups[g].dealCount, " deals", + " | ", g_entryGroups[g].posIDCount, " positions", + " | Net: ", DoubleToString(g_entryGroups[g].totalPnL, 2), + " | Result: ", isWin ? "WIN" : "LOSS", + " | PeakRR: ", DoubleToString(g_entryGroups[g].peakRR, 2), // * FIX#204 + " | WinRate: ", DoubleToString(g_historicalWinRate * 100, 1), "%"); + // Reset group + g_entryGroups[g].active = false; + g_entryGroups[g].totalPnL = 0; + g_entryGroups[g].totalRMultiple = 0; + g_entryGroups[g].dealCount = 0; + g_entryGroups[g].firstDealTime = 0; + g_entryGroups[g].posIDCount = 0; + g_entryGroups[g].peakRR = 0.0; // * FIX#204 + } + } +} +//+------------------------------------------------------------------+ +//| * v9.31 FIX#116: ForceFlushAllEntryGroups() | +//| Flush all un-closed entry groups so trade counter is correct. | +//| Called at OnDeinit and OnTester to ensure trades are counted. | +//+------------------------------------------------------------------+ +void ForceFlushAllEntryGroups() +{ + for(int g = 0; g < MAX_ENTRY_GROUPS; g++) + { + if(!g_entryGroups[g].active || g_entryGroups[g].dealCount == 0) continue; + bool isWin = (g_entryGroups[g].totalPnL > 0); + double avgR = g_entryGroups[g].totalRMultiple / MathMax(1, g_entryGroups[g].dealCount); + UpdateHistoricalPerformance(isWin, avgR, g_entryGroups[g].peakRR); // * FIX#204 + // * FIX#STATS_ZERO: same fix as group-close handler + g_perfData.totalTrades++; + if(isWin) g_perfData.totalWins++; else g_perfData.totalLosses++; + g_backtestResults.totalTrades++; + if(isWin) g_backtestResults.winningTrades++; else g_backtestResults.losingTrades++; + g_backtestResults.netProfit += g_entryGroups[g].totalPnL; + // * FIX#448a: grossProfit/grossLoss — same fix as main close path. + if(isWin) g_backtestResults.grossProfit += g_entryGroups[g].totalPnL; + else g_backtestResults.grossLoss += MathAbs(g_entryGroups[g].totalPnL); + if(g_verboseLog) + PrintFormat("[FIX#116] ForceFlush group %d | %d deals | Net=%.2f | %s | PeakRR=%.2f", + g, g_entryGroups[g].dealCount, + g_entryGroups[g].totalPnL, isWin ? "WIN" : "LOSS", + g_entryGroups[g].peakRR); // * FIX#204 + g_entryGroups[g].active = false; + g_entryGroups[g].totalPnL = 0; + g_entryGroups[g].totalRMultiple = 0; + g_entryGroups[g].dealCount = 0; + g_entryGroups[g].firstDealTime = 0; + g_entryGroups[g].posIDCount = 0; + g_entryGroups[g].peakRR = 0.0; // * FIX#204 + } +} +//+------------------------------------------------------------------+ +//| * v7.5c: SMART EXIT -- Reversal Detection System | +//| Detects early reversal signals and closes with smaller profit | +//| instead of waiting for full TP and risking a loss. | +//| Returns: number of reversal signals detected (0-8) | +//+------------------------------------------------------------------+ +int SmartExit_Check(bool isBuy, double current_rr, double open_price, double current_price, double atr) +{ + if(!EA_SmartExit_Enable) return 0; + if(atr <= 0) return 0; + // * v7.5c: Timeframe-adaptive thresholds (auto-optimization for all pairs) + // * v9.03: Use working vars (AutoOpt-adjusted when ON, input when OFF) + double minProfitRR = g_workingSmartExit_MinRR; + // TF-aware minProfitRR scaling (AutoOpt OFF only — when ON, working vars already adjusted) + if(!AutoOpt_Enabled || !g_autoOptInitialized) + { + switch(g_marketSnap.tf_category) + { + case TF_CAT_SCALP: minProfitRR = MathMax(0.8, g_workingSmartExit_MinRR * 2.5); break; + case TF_CAT_INTRADAY: minProfitRR = MathMax(1.1, g_workingSmartExit_MinRR); break; + case TF_CAT_INTRASWING: minProfitRR = g_workingSmartExit_MinRR * 1.2; break; + case TF_CAT_SWING: minProfitRR = g_workingSmartExit_MinRR * 1.5; break; + case TF_CAT_POSITION: minProfitRR = g_workingSmartExit_MinRR * 2.0; break; + } + } + + // Absolute minimum floor — pair table se_minrr[tf] is the real threshold per TF. + // This hardcoded floor is only a safety net: never close below 0.30R no matter what. + // H1: se_minrr[2]=0.45R → effective floor=max(0.45, 0.30)=0.45R (pair table wins) + // Tier 1 trail-tighten can still activate at 2 cats below this floor. + if(minProfitRR < 0.30) minProfitRR = 0.30; + if(current_rr <= 0) return 0; // never close at a loss + + // * v7.5d FIX: Use CATEGORIES -- each category contributes MAX 1 signal + // This prevents 1 candle + RSI from triggering 5 signals + // Need reversal from N DIFFERENT sources, not N signals from same source + bool cat_RSI = false; // Category 1: RSI exhaustion + bool cat_CandlePattern = false; // Category 2: Engulfing / Large candle / Wick rejection + bool cat_Momentum = false; // Category 3: Consecutive opposite candles + bool cat_MTF = false; // Category 4: Multi-timeframe direction flip + bool cat_Regime = false; // Category 5: Market regime against position + string signalNames = ""; + // =========================================================== + // CATEGORY 1: RSI Exhaustion (max 1 signal) + // =========================================================== + if(g_cachedRSI > 0) + { + // * FIX#421: Use g_workingSE_RSI_OB/OS from pair table (was hardcoded 75/25). + if(isBuy && g_cachedRSI > g_workingSE_RSI_OB) + { + cat_RSI = true; + signalNames += "RSI(" + DoubleToString(g_cachedRSI, 0) + ">OB" + DoubleToString(g_workingSE_RSI_OB,0) + ") "; + } + else if(!isBuy && g_cachedRSI < g_workingSE_RSI_OS) + { + cat_RSI = true; + signalNames += "RSI(" + DoubleToString(g_cachedRSI, 0) + "= 5 && + CopyOpen(_Symbol, _Period, 0, 5, c_open) >= 5 && + CopyHigh(_Symbol, _Period, 0, 5, c_high) >= 5 && + CopyLow(_Symbol, _Period, 0, 5, c_low) >= 5) + { + // --- MOMENTUM: Count consecutive opposite candles (bar 1,2,3) --- + int oppositeCount = 0; + for(int b = 1; b <= 3; b++) + { + if(isBuy && c_close[b] < c_open[b]) + oppositeCount++; + else if(!isBuy && c_close[b] > c_open[b]) + oppositeCount++; + else + break; + } + if(oppositeCount >= 2) + { + cat_Momentum = true; + signalNames += "Momentum(" + IntegerToString(oppositeCount) + "bars) "; + } + // --- CANDLE PATTERN: Check bar[1] for reversal pattern --- + double body1 = MathAbs(c_close[1] - c_open[1]); + double body2 = MathAbs(c_close[2] - c_open[2]); + double range1 = MathMax(c_high[1] - c_low[1], _Point); + // Engulfing + bool hasEngulf = false; + if(body1 > body2 * 1.5 && body1 > atr * 0.3) + { + bool bearishEngulf = (c_close[1] < c_open[1]) && (c_close[2] > c_open[2]); + bool bullishEngulf = (c_close[1] > c_open[1]) && (c_close[2] < c_open[2]); + if(isBuy && bearishEngulf) hasEngulf = true; + else if(!isBuy && bullishEngulf) hasEngulf = true; + } + // Large opposite candle (body > 0.6 ATR) * v7.5d: 0.5->0.6 + bool hasLargeCandle = false; + if(body1 > atr * 0.6) + { + if(isBuy && c_close[1] < c_open[1]) hasLargeCandle = true; + else if(!isBuy && c_close[1] > c_open[1]) hasLargeCandle = true; + } + // Wick rejection — FIX#513: 65%→58% for XAUUSD H1 reversal wick profile. + // XAUUSD H1 reversal wicks average 55-60% of range; 65% was missing exits. + // 58% stays above noise floor (50%) — 3-signal confirmation prevents over-exit. + bool hasWickReject = false; + double upperWick1 = c_high[1] - MathMax(c_open[1], c_close[1]); + double lowerWick1 = MathMin(c_open[1], c_close[1]) - c_low[1]; + if(isBuy && upperWick1 > range1 * 0.58) hasWickReject = true; + else if(!isBuy && lowerWick1 > range1 * 0.58) hasWickReject = true; + // * v7.5d: ANY of these patterns = 1 category signal (not 3!) + if(hasEngulf || hasLargeCandle || hasWickReject) + { + cat_CandlePattern = true; + if(hasEngulf) signalNames += "Engulf "; + else if(hasLargeCandle) signalNames += "LargeCandle "; + else signalNames += "WickReject "; + } + } + } + // =========================================================== + // CATEGORY 4: MTF Direction flipped against position (max 1 signal) + // =========================================================== + if(MTF_Enabled) + { + // FIX#505f: use corrected direction for SmartExit + if(isBuy && (g_mtfAnalysis.overallDirection == MTF_BEARISH || g_mtfAnalysis.overallDirection == MTF_STRONG_BEARISH)) + { + cat_MTF = true; + signalNames += "MTF_Against "; + } + else if(!isBuy && (g_mtfAnalysis.overallDirection == MTF_BULLISH || g_mtfAnalysis.overallDirection == MTF_STRONG_BULLISH)) + { + cat_MTF = true; + signalNames += "MTF_Against "; + } + } + // =========================================================== + // CATEGORY 5: Regime changed against position (max 1 signal) + // =========================================================== + if(g_regimeValid) + { + if(g_regimeData.regime == REGIME_CHOPPY) + { + cat_Regime = true; + signalNames += "Regime_Choppy "; + } + else if(isBuy && (g_regimeData.regime == REGIME_TREND_DOWN || g_regimeData.regime == REGIME_STRONG_TREND_DOWN)) + { + cat_Regime = true; + signalNames += "Regime_Against "; + } + else if(!isBuy && (g_regimeData.regime == REGIME_TREND_UP || g_regimeData.regime == REGIME_STRONG_TREND_UP)) + { + cat_Regime = true; + signalNames += "Regime_Against "; + } + } + // * v9.29 FIX#99c: CATEGORY 6 -- Confirmed opposing divergence (max 1 signal) + // SmartEntry blocks BEFORE entry (FIX#99b). SmartExit_Check needs it too: + // If a confirmed divergence appears AGAINST the position while in-trade -> early exit signal. + bool cat_Divergence = false; + if(Divergence_Enabled) + { + for(int _dv = 0; _dv < ArraySize(g_divergences); _dv++) + { + if(!g_divergences[_dv].active || !g_divergences[_dv].confirmed) continue; + // Only fresh divergences (<= 2 bars old) + if(TimeCurrent() - g_divergences[_dv].time2 > 2 * PeriodSeconds(_Period)) continue; + bool divBull = (g_divergences[_dv].type == DIV_REGULAR_BULLISH || g_divergences[_dv].type == DIV_HIDDEN_BULLISH); + bool divBear = (g_divergences[_dv].type == DIV_REGULAR_BEARISH || g_divergences[_dv].type == DIV_HIDDEN_BEARISH); + if(isBuy && divBear) { cat_Divergence = true; signalNames += "BearDiv "; break; } + if(!isBuy && divBull) { cat_Divergence = true; signalNames += "BullDiv "; break; } + } + } + // * v10.14 FIX#292: Structure flip as independent SmartExit category. + // Previously: structure (g_isBullishStructure) only fed into Regime (indirect, slow). + // Now: direct connection — if BOS/CHoCH flips against our trade AND we have + // enough profit (minProfitRR already checked above), it counts as a signal. + // This is the FASTEST signal: CHoCH fires at bar close, Regime lags 2-3 bars. + // Weight: counts as 1 full category (same as MTF or Divergence). + bool cat_Structure = false; + if(EnableStructure) + { + bool structureAgainst = isBuy ? !g_isBullishStructure : g_isBullishStructure; + if(structureAgainst) + { + cat_Structure = true; + signalNames += "Structure_CHoCH "; + } + } + // CATEGORY 8: DOL flipped against position (institutional delivery reversed) + bool cat_DOL = false; + if(g_dolValid && g_dolDirection != 0 && current_rr >= 0.20) + { + bool dolAgainst = (isBuy && g_dolDirection < 0) || (!isBuy && g_dolDirection > 0); + if(dolAgainst) { cat_DOL = true; signalNames += "DOL_Against "; } + } + + // CATEGORY 9: Opposing OB within 0.5 ATR (price at supply/demand zone) + bool cat_OpposingZone = false; + if(atr > 0) + { + // Opposing OB + for(int _oz = 0; _oz < MathMin(g_obCount, ArraySize(OB_Array)) && !cat_OpposingZone; _oz++) + { + if(!g_obs[_oz].active || g_obs[_oz].mitigated) continue; + if(g_obs[_oz].isBullish == isBuy) continue; + double mid = (g_obs[_oz].top + g_obs[_oz].bottom) * 0.5; + if(MathAbs(current_price - mid) < atr * 0.5) + { cat_OpposingZone = true; signalNames += "OppOB "; } + } + // Opposing FVG fill + if(!cat_OpposingZone) + { + for(int _fz = 0; _fz < MathMin(g_fvgCount, ArraySize(FVG_Array)) && !cat_OpposingZone; _fz++) + { + if(!g_fvgs[_fz].active) continue; + if(g_fvgs[_fz].isBullish == isBuy) continue; + if(current_price >= g_fvgs[_fz].bottom && current_price <= g_fvgs[_fz].top) + { cat_OpposingZone = true; signalNames += "OppFVG "; } + } + } + } + + // Count categories triggered + int categoryCount = 0; + if(cat_RSI) categoryCount++; + if(cat_CandlePattern) categoryCount++; + if(cat_Momentum) categoryCount++; + if(cat_MTF) categoryCount++; + if(cat_Regime) categoryCount++; + if(cat_Divergence) categoryCount++; + if(cat_Structure) categoryCount++; + if(cat_DOL) categoryCount++; + if(cat_OpposingZone) categoryCount++; + // * FIX#423: Write catCount to global so dispatch block can store it per-ticket. + // This is the cooperative channel: SmartExit_Check → FIX#403 peak trail. + g_lastSE_catCount = categoryCount; + + // ── 3-TIER RESPONSE ──────────────────────────────────────────── + // Tier 0 (0-1 cats): hold — normal trail + // Tier 1 (-1, 2 cats): tighten trail — caller applies ATR×0.8, no close + // Tier 2 (>0, 3+ cats AND rr >= minProfitRR): CLOSE the position + // Tier 2b (2 cats, rr >= se_override_rr): close at high profit via override + // Emergency tier (Structure+MTF, any rr>0): close even below floor + + if(categoryCount >= 2 && EA_SmartExit_Enable) + { + static datetime lastSELog = 0; + datetime now = TimeCurrent(); + if(now - lastSELog > 300) // max once per 5 min (called from trail every tick) + { + lastSELog = now; + PrintFormat("* SmartExit: %d/9 cats | R:R=%.2fR | floor=%.2fR | [%s]", + categoryCount, current_rr, minProfitRR, signalNames); + } + } + + // Tier 0: not enough signals to act + if(categoryCount < 2) return 0; + + // Tier 1: 2 categories → tighten trail only, don't close yet + if(categoryCount == 2 && current_rr < minProfitRR) + return -1; // caller tightens trail to ATR×0.8 + + // Tier 2: 3+ categories AND above floor → close + if(categoryCount >= 3 && current_rr >= minProfitRR) + return categoryCount; + + // Tier 2b: 2 cats at high RR (override threshold from pair table) + // e.g. EURUSD H1: se_override_rr=0.85R → 2 cats at 0.85R+ = close + // RSI exhaustion is a valid broader context signal (overbought/oversold = reversal context). + // RSI + DOL_Against = two independent reversal signals → close at override threshold. + // RSI + PA/Structure also valid. PA/Struct + Broader also valid. + if(categoryCount >= 2 && g_workingSE_OverrideRR > 0 && current_rr >= g_workingSE_OverrideRR) + { + bool hasPAorStruct = (cat_Momentum || cat_CandlePattern || cat_Structure); + bool hasBroader = (cat_MTF || cat_Regime || cat_Divergence || cat_DOL || cat_RSI); + if(hasPAorStruct && hasBroader) + { + PrintFormat("* SmartExit 2-CAT CLOSE: RR=%.2fR >= %.1fR threshold | [%s]", + current_rr, g_workingSE_OverrideRR, signalNames); + return categoryCount; + } + } + + // Tier 2b scalp: M5 — 2 cats with price-action+context is sufficient + if(g_marketSnap.tf_category == TF_CAT_SCALP && categoryCount >= 2 && current_rr >= minProfitRR) + { + bool hasPriceAction = (cat_Momentum || cat_CandlePattern); + bool hasBroaderCtx = (cat_MTF || cat_Regime); + if(hasPriceAction && hasBroaderCtx) + { + PrintFormat("* SmartExit SCALP 2-CAT CLOSE: RR=%.2fR | [%s]", current_rr, signalNames); + return categoryCount; + } + } + + // Emergency tier: Structure+MTF both reversed, any profit + if(current_rr > 0 && cat_Structure && cat_MTF) + { + static datetime _emgLastLog = 0; + if(TimeCurrent() - _emgLastLog >= 60) + { + PrintFormat("* SmartExit EMERGENCY: Structure+MTF reversed | RR=%.2fR | [%s]", + current_rr, signalNames); + _emgLastLog = TimeCurrent(); + } + return categoryCount; + } + + // 2 cats below floor → tighten trail + return -1; +} +//+------------------------------------------------------------------+ +//| EA_ManagePositions - ΔΙΟΡΘΩΜΕΝΗ ΕΚΔΟΣΗ | +//| [OK] FIX #2: Single comment read per position | +//| [OK] Replaced all posCommentProx/BE/Trail -> posComment | +//| [OK] Replaced all isTP1_Prox/BE/Trail -> isTP1_Tranche | +//+------------------------------------------------------------------+ +void EA_ManagePositions() +{ + // * v9.59 FIX#228: CRITICAL — If any DD limit is active, do NOT manage positions. + // Previous bug: After g_totalDDLimitReached=true, EA_ManagePositions() continued + // to run every tick, allowing open positions to keep bleeding (XAGUSD -115% blowup). + // The ExpertRemove() in CheckWeeklyTotalDrawdownLimit only removes EA on NEXT tick; + // until that happens (or if EA_StopOnDrawdown=false), positions must be halted. + // * FIX#294b: Reset SmartExit warning each ManagePositions cycle. + if(g_totalDDLimitReached || g_weeklyDDLimitReached || g_dailyDDLimitReached) + { + if(g_totalDDLimitReached && EA_StopOnDrawdown) + { + // Force close all remaining positions — safety net for the tick between + // ExpertRemove() call and actual EA removal + for(int _dd = PositionsTotal() - 1; _dd >= 0; _dd--) + { + if(g_ea_position.SelectByIndex(_dd)) + if(g_ea_position.Symbol() == _Symbol && g_ea_position.Magic() == EA_MagicNumber) + g_ea_trade.PositionClose(g_ea_position.Ticket()); + } + } + return; // Block all position management when DD limit hit + } + g_ea_symbol.Name(_Symbol); + g_ea_symbol.RefreshRates(); + // =============================================================== + // * FULL HYBRID: Auto-switch between Option D and Option E + // Checks market regime and selects appropriate management strategy + // =============================================================== + if(EA_UseFullHybrid) + ManageFullHybrid(); + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + if(!g_ea_position.SelectByIndex(i)) continue; + if(g_ea_position.Symbol() != _Symbol) continue; + if(g_ea_position.Magic() != EA_MagicNumber) continue; + // =============================================================== + // [OK] ΔΙΟΡΘΩΣΗ #2: Διάβασε το comment ΜΙΑ ΦΟΡΑ μόνο + // =============================================================== + string posComment = g_ea_position.Comment(); + bool isTP1_Tranche = (StringFind(posComment, "_TP1") >= 0); + bool isTP2_Tranche = (StringFind(posComment, "_TP2") >= 0); + bool isTP3_Tranche = (StringFind(posComment, "_TP3") >= 0); + double current_price = g_ea_position.Type() == POSITION_TYPE_BUY ? + g_ea_symbol.Bid() : g_ea_symbol.Ask(); + double open_price = g_ea_position.PriceOpen(); + double current_sl = g_ea_position.StopLoss(); + double current_tp = g_ea_position.TakeProfit(); + // [OK] CALCULATE CURRENT R:R + // * v9.24 FIX#70: Use ORIGINAL SL (from g_multiTPEntries.stopLoss) for RR calculation. + // After BE move, current_sl = entry+2p -> sl_distance = 2p -> RR inflated to 2.90 on 5.8p profit. + // SmartExit threshold 1.1R passed immediately -> trade closed at 0.46R instead of running to TP. + // Fix: find the original SL from g_multiTPEntries by ticket -> sl_distance = true risk. + // Find MultiTP entry for this position: original SL + peakRR tracking + // Scan ALL entries (active and inactive) — inactive may still have open positions + // if position was temporarily lost and redetected via entry price fallback. + ulong posTicket = g_ea_position.Ticket(); + double original_sl = current_sl; + int _mtp_idx = -1; + double _mtpTol = (g_cachedATR > 0) ? g_cachedATR * 0.10 : g_pipValue * 5; + for(int mtp = 0; mtp < ArraySize(g_multiTPEntries); mtp++) + { + // Primary: ticket match (exact) + if((ulong)g_multiTPEntries[mtp].ticket == posTicket) + { + original_sl = g_multiTPEntries[mtp].stopLoss; + _mtp_idx = mtp; + if(!g_multiTPEntries[mtp].active) g_multiTPEntries[mtp].active = true; + break; + } + // Fallback: entry price + direction (TP2/TP3 tranches have different tickets) + bool _dirMatch = (g_ea_position.PositionType() == POSITION_TYPE_BUY && g_multiTPEntries[mtp].direction == 1) || + (g_ea_position.PositionType() == POSITION_TYPE_SELL && g_multiTPEntries[mtp].direction == -1); + if(_dirMatch && MathAbs(g_multiTPEntries[mtp].entryPrice - open_price) < _mtpTol) + { + original_sl = g_multiTPEntries[mtp].stopLoss; + _mtp_idx = mtp; + if(!g_multiTPEntries[mtp].active) g_multiTPEntries[mtp].active = true; + break; + } + } + double sl_distance = MathAbs(open_price - original_sl); + double profit_distance = 0; + if(g_ea_position.Type() == POSITION_TYPE_BUY) + profit_distance = current_price - open_price; + else + profit_distance = open_price - current_price; + double current_rr = 0; + if(sl_distance > 0) + current_rr = profit_distance / sl_distance; + // * v7.5b FIX: Cap R:R at 10.0 max (safety guard) + if(current_rr > 10.0) + current_rr = 10.0; + // Get ATR + double atr_buffer[]; + ArraySetAsSeries(atr_buffer, true); + double atr = g_cachedATR; + if(g_atrHandle != INVALID_HANDLE && CopyBuffer(g_atrHandle, 0, 0, 1, atr_buffer) > 0) + atr = atr_buffer[0]; + // [OK] FIX: ATR fallback if both cached and handle fail + if(atr <= 0) + atr = g_ea_symbol.Point() * 100; // Reasonable default fallback + // * FIX#150: Update peakRR for this position (highest RR ever reached since entry) + // Used by SmartExit to detect reversal-from-peak and close before giving back too much profit. + // * FIX#447: Use _mtp_idx (resolved above via entryPrice fallback) — covers TP2/TP3 tranches. + // * FIX#456: ARCHITECTURAL — also update g_entryGroups[].peakRR live here every tick. + // PROBLEM: g_entryGroups[].peakRR was populated at deal-close via a fragile time-based + // lookup (entryTime vs lastDealTime < 120s). On D1 trades (hours between entry and exit) + // the delta >> 120s → match always failed → peakRR=0.00 on every FIX#23c print → AutoOpt + // never learned how far price moved → TP targets calibrated too conservatively. + // Architectural fix: push peakRR to entryGroups LIVE on every tick, same location as + // multiTPEntries update. At close time the value is already correct — no lookup needed. + if(current_rr > 0 && _mtp_idx >= 0) + { + if(current_rr > g_multiTPEntries[_mtp_idx].peakRR) + g_multiTPEntries[_mtp_idx].peakRR = current_rr; + // * FIX#456: Propagate live to the matching entryGroup (matched by positionID = posTicket) + // * FIX#468: Race condition — on the very first tick after position open, posTicket may + // not yet be in positionIDs[] (EA_UpdateTradeResults hasn't run yet for this deal). + // Fix: if positionID match fails, fall back to entryPrice match (same tolerance as + // _mtp_idx fallback above). This ensures the first-tick peak is never silently lost. + bool _egMatched = false; + for(int _eg = 0; _eg < MAX_ENTRY_GROUPS; _eg++) + { + if(!g_entryGroups[_eg].active) continue; + // Primary: positionID match (normal path after first deal processed) + for(int _ep = 0; _ep < g_entryGroups[_eg].posIDCount; _ep++) + { + if(g_entryGroups[_eg].positionIDs[_ep] == posTicket) + { + if(current_rr > g_entryGroups[_eg].peakRR) + g_entryGroups[_eg].peakRR = current_rr; + _egMatched = true; + break; + } + } + if(_egMatched) break; + } + // * FIX#469b: Replace time-proximity fallback with entryPrice match. + // ROOT: old fallback used |firstDealTime - POSITION_TIME| <= 5s. + // In D1 OHLC simulation this window is frequently missed → _egMatched + // stays false → g_entryGroups.peakRR never written → PeakRR: 0.00. + // Fix: match by entryPrice (same ATR tolerance as _mtp_idx resolution). + if(!_egMatched) + { + double _egTol = (g_cachedATR > 0) ? g_cachedATR * 0.10 : g_pipValue * 5; + for(int _eg = 0; _eg < MAX_ENTRY_GROUPS; _eg++) + { + if(!g_entryGroups[_eg].active) continue; + if(g_entryGroups[_eg].entryPrice > 0 && + MathAbs(g_entryGroups[_eg].entryPrice - open_price) < _egTol) + { + if(current_rr > g_entryGroups[_eg].peakRR) + g_entryGroups[_eg].peakRR = current_rr; + _egMatched = true; + break; + } + } + } + // * v9.27 FIX#97: Declare posIsBuy here (outer scope) so Proximity Close can use it too + bool posIsBuy = (g_ea_position.Type() == POSITION_TYPE_BUY); + // =========================================================== + // * v7.5c: SMART EXIT -- Close early on reversal signals + // Detects when price is about to reverse and closes with + // smaller profit instead of waiting for full TP + // * FIX #4: PROXIMITY CLOSE - Close at 92% of TP distance + // If price gets very close to TP but stalls, secure profit + // * v6.38 FIX ISSUE#4: TRANCHE-AWARE -- skip TP1 AND TP2 + // TP1: Let it hit TP exactly (bread and butter) + // TP2: At 4.5 ATR, 92% = 4.14 ATR -> closing loses 0.36 ATR needlessly + // Only apply proximity close to TP3 runners and single positions + // =========================================================== + // [OK] ΔΙΟΡΘΩΣΗ: Χρήση isTP1_Tranche, isTP2_Tranche (όχι isTP1_Prox, isTP2_Prox) + // * v9.27 FIX#97: TRIPLE DIRECTION GUARD -- prevent proximity close on losing trades + // BUG: SELL at 1.16224, TP=1.15932. Price went UP to 1.16519 (AGAINST trade, -29.5p). + // System measured |1.16519-1.16224|=29.5p vs tp_distance=29.2p -> 99.3% -> force closed for -$339. + // Root cause: profit_distance > 0 check was unreliable due to position type tracking issue. + // Fix: add EXPLICIT directional check that price is actually moving toward TP, not toward SL. + bool priceMovingTowardTP = posIsBuy ? (current_price > open_price && current_price < current_tp) + : (current_price < open_price && current_price > current_tp); + if(!isTP1_Tranche && !isTP2_Tranche && current_tp > 0 && profit_distance > 0 && priceMovingTowardTP) + { + double tp_distance = MathAbs(current_tp - open_price); + // * v9.27 FIX#97: Use DIRECTIONAL distance to TP (not raw profit_distance which can be wrong) + // For BUY: how far price moved FROM entry TOWARD TP (capped at tp_distance) + // For SELL: same -- ensures ratio is always 0.0..1.0 + double directedProgress = posIsBuy ? (current_price - open_price) : (open_price - current_price); + double proximity_ratio = (tp_distance > 0) ? MathMin(1.0, directedProgress / tp_distance) : 0.0; + // Close if price reached 92%+ of TP AND shows clear reversal + // * v6.32: 88%->92% (was closing too early, losing 12% of TP profit) + if(proximity_ratio >= EA_Proximity_Close_Start) // [OK] Configurable (was 0.92) + { + // Check if momentum is fading (price clearly reversing near TP) + bool momentumFading = false; + double close_buf[]; + ArraySetAsSeries(close_buf, true); + if(CopyClose(_Symbol, _Period, 0, 3, close_buf) >= 3) + { + if(g_ea_position.Type() == POSITION_TYPE_BUY) + { + // * v6.32 FIX: Changed || to && (was: ANY 1 bearish candle = close) + // Now requires BOTH last 2 candles bearish = actual reversal + momentumFading = (close_buf[0] < close_buf[1]) && (close_buf[1] < close_buf[2]); + } + else + { + // * v6.32 FIX: Same fix for shorts + momentumFading = (close_buf[0] > close_buf[1]) && (close_buf[1] > close_buf[2]); + } + } + // Also check RSI for exhaustion + if(g_cachedRSI > 0) + { + if(g_ea_position.Type() == POSITION_TYPE_BUY && g_cachedRSI > EA_RSI_Overbought) // [OK] Configurable (was 78) + momentumFading = true; + else if(g_ea_position.Type() == POSITION_TYPE_SELL && g_cachedRSI < EA_RSI_Oversold) // [OK] Configurable (was 22) + momentumFading = true; + } + // * v6.32: 95%->97% for forced close (let it reach TP naturally at 95-97%) + if(momentumFading || proximity_ratio >= EA_Proximity_Force_Close) // [OK] Configurable (was 0.97) + { + g_ea_trade.PositionClose(g_ea_position.Ticket()); + Print("* v9.27 Proximity Close: ", DoubleToString(proximity_ratio * 100, 1), + "% of TP reached | Profit: ", DoubleToString(directedProgress / g_pipValue, 1), " pips", + " | MomentumFading: ", momentumFading ? "YES" : "NO (95%+ forced)"); + continue; + } + } + } + // =========================================================== + // * v6.3: STRENGTH-WEIGHTED STRUCTURE CLOSE + // Close if price hits opposing OB, FVG, or Breaker Block + // Stronger structure -> earlier close, Weaker -> hold longer + // * v9.16 FIX#50b: SKIP TP1 and TP2 tranches! + // TP1 must hit its target or SL (bread & butter) + // TP2 must reach target -- structure close was killing it at 1.9R (target=3.0R) + // Only TP3 runners and single positions use structure close + // Evidence: 0 TP2 hits, 0 TP3 hits in backtest -- structure close killed ALL + // =========================================================== + if(current_rr >= 1.0 && atr > 0 && !isTP1_Tranche && !isTP2_Tranche) // * FIX#50b: +skip TP1/TP2 + { + bool hitStructure = false; + double structLevel = 0; + string structType = ""; + double structStrength = 0; // * v6.3: Track structure strength (0.0-1.0) + // 1. Opposing OBs (strongest signal) + for(int ob = 0; ob < ArraySize(OB_Array) && !hitStructure; ob++) + { + if(!OB_Array[ob].active || OB_Array[ob].mitigated) continue; + if(OB_Array[ob].strength < EA_OB_MinStrength_StructClose) continue; // [OK] Configurable (was 0.65) + if(g_ea_position.Type() == POSITION_TYPE_BUY && !OB_Array[ob].isBullish) + { + if(current_price >= OB_Array[ob].bottom && current_price <= OB_Array[ob].top && + OB_Array[ob].bottom > open_price) + { + hitStructure = true; + structLevel = OB_Array[ob].bottom; + structType = "Bearish OB"; + structStrength = OB_Array[ob].strength; + } + } + else if(g_ea_position.Type() == POSITION_TYPE_SELL && OB_Array[ob].isBullish) + { + if(current_price <= OB_Array[ob].top && current_price >= OB_Array[ob].bottom && + OB_Array[ob].top < open_price) + { + hitStructure = true; + structLevel = OB_Array[ob].top; + structType = "Bullish OB"; + structStrength = OB_Array[ob].strength; + } + } + } + // 2. Opposing FVGs (medium signal) + // * v6.32: Added quality filter - LOW quality FVGs should not trigger structure close + for(int fvg = 0; fvg < ArraySize(FVG_Array) && !hitStructure; fvg++) + { + if(FVG_Array[fvg].status != FVG_STATUS_ACTIVE) continue; + if(FVG_Array[fvg].quality < EA_FVG_MinQuality_StructClose) continue; // [OK] Configurable (was FVG_QUALITY_HIGH) + if(g_ea_position.Type() == POSITION_TYPE_BUY && !FVG_Array[fvg].isBullish) + { + if(current_price >= FVG_Array[fvg].bottom && current_price <= FVG_Array[fvg].top && + FVG_Array[fvg].bottom > open_price) + { + hitStructure = true; + structLevel = FVG_Array[fvg].bottom; + structType = "Bearish FVG"; + structStrength = (double)FVG_Array[fvg].quality / 3.0; // LOW=0, MEDIUM=0.33, HIGH=0.67, PREMIUM=1.0 + } + } + else if(g_ea_position.Type() == POSITION_TYPE_SELL && FVG_Array[fvg].isBullish) + { + if(current_price <= FVG_Array[fvg].top && current_price >= FVG_Array[fvg].bottom && + FVG_Array[fvg].top < open_price) + { + hitStructure = true; + structLevel = FVG_Array[fvg].top; + structType = "Bullish FVG"; + structStrength = (double)FVG_Array[fvg].quality / 3.0; + } + } + } + // 3. Opposing Breaker Blocks (strong signal) + for(int bb = 0; bb < ArraySize(BREAKER_Array) && !hitStructure; bb++) + { + if(!BREAKER_Array[bb].active || BREAKER_Array[bb].mitigated) continue; + if(BREAKER_Array[bb].strength < EA_Breaker_MinStrength_StructClose) continue; // [OK] Configurable (was 0.5) + if(g_ea_position.Type() == POSITION_TYPE_BUY && !BREAKER_Array[bb].isBullish) + { + if(current_price >= BREAKER_Array[bb].bottom && current_price <= BREAKER_Array[bb].top && + BREAKER_Array[bb].bottom > open_price) + { + hitStructure = true; + structLevel = BREAKER_Array[bb].bottom; + structType = "Bearish Breaker"; + structStrength = BREAKER_Array[bb].strength; + } + } + else if(g_ea_position.Type() == POSITION_TYPE_SELL && BREAKER_Array[bb].isBullish) + { + if(current_price <= BREAKER_Array[bb].top && current_price >= BREAKER_Array[bb].bottom && + BREAKER_Array[bb].top < open_price) + { + hitStructure = true; + structLevel = BREAKER_Array[bb].top; + structType = "Bullish Breaker"; + structStrength = BREAKER_Array[bb].strength; + } + } + } + // * v6.3: STRENGTH-WEIGHTED CLOSE THRESHOLD + // Strong structure (0.9) -> close at ~baseThreshold (early) + // Weak structure (0.3) -> close at ~baseThreshold+0.35R (hold longer) + if(hitStructure) + { + // * v6.32 PROFIT FIX: Structure Close was closing ALL 3 tranches! + // TP1 (50% lots) at 0.8R instead of 1.8R -> destroyed R:R advantage + // TP2 (30% lots) at 0.8R instead of 3.0R -> same problem + // TP3 (20% lots) at 0.8R instead of 4.5R -> this is the only one that should close + // + // FIX: Check position comment to identify tranche: + // _TP1: NEVER structure close (let it hit TP1 or SL) + // _TP2: Only structure close if R:R >= 1.5R (decent profit secured) + // _TP3: Structure close at regime threshold (runners adapt to structure) + // Single position (no multi-TP): use old behavior + // [OK] ΔΙΟΡΘΩΣΗ: Χρήση isTP1_Tranche, isTP2_Tranche, isTP3_Tranche + bool isMultiTP = (isTP1_Tranche || isTP2_Tranche || isTP3_Tranche); + // TP1 positions: NEVER close at structure - let them hit TP or SL + // This is the bread & butter of the system + if(isTP1_Tranche) + { + if(g_verboseLog) + Print("* Structure Close SKIPPED for TP1 tranche: ", structType, + " | R:R=", DoubleToString(current_rr, 2), + " | Let TP1 hit its target or SL"); + } + // TP2: only close if decent profit already secured + else if(isTP2_Tranche) + { + double tp2CloseThreshold = MathMax(EA_TP2_Structure_MinRR, EA_MinRR); // [OK] Configurable (was 1.5) + if(current_rr >= tp2CloseThreshold) + { + if(g_ea_trade.PositionClose(g_ea_position.Ticket())) + { + Print("* Structure Close TP2: ", structType, + " | Strength=", DoubleToString(structStrength, 2), + " | R:R=", DoubleToString(current_rr, 2), + " | Threshold=", DoubleToString(tp2CloseThreshold, 2), "R"); + } + continue; + } + } + // TP3 (runner) or single position: use regime-adaptive threshold + else + { + double baseThreshold = EA_Single_Position_BaseThreshold; // [OK] Configurable (was 1.2) + // * v6.38 FIX ISSUE#3: TP3 runners need MUCH higher thresholds + // Before: 1.0-1.6R -> runners closed at pathetic profits (6.0 ATR target never reached) + // Now: TP3 requires 2.5R base + strong structure (0.85+) + if(isTP3_Tranche) + { + baseThreshold = EA_TP3_Runner_BaseThreshold; // [OK] Configurable (was 2.5) + // Only react to VERY strong opposing structure for runners + if(structStrength < EA_TP3_Structure_MinStrength) // [OK] Configurable (was 0.85) + { + if(g_verboseLog) + Print("* v6.38 Structure Close SKIPPED for TP3 runner: ", structType, + " | Strength=", DoubleToString(structStrength, 2), + " < ", DoubleToString(EA_TP3_Structure_MinStrength, 2), " (too weak to close runner)"); + // Don't close -- continue to next position + } + else + { + // Strong structure: adaptive threshold + double structCloseThreshold = baseThreshold + (0.5 * (1.0 - structStrength)); + if(current_rr >= structCloseThreshold) + { + if(g_ea_trade.PositionClose(g_ea_position.Ticket())) + { + Print("* v6.38 Structure Close TP3 Runner: ", structType, + " | Level=", DoubleToString(structLevel, _Digits), + " | Strength=", DoubleToString(structStrength, 2), + " | R:R=", DoubleToString(current_rr, 2), + " | Threshold=", DoubleToString(structCloseThreshold, 2), "R"); + } + continue; + } + } + } + else if(!isMultiTP) // Single position (not multi-TP) + { + // Adaptive threshold based on structure strength + double structCloseThreshold = baseThreshold + (0.35 * (1.0 - structStrength)); + // Strong OB (0.9) -> close at 1.05R, Weak (0.3) -> 1.55R + if(current_rr >= structCloseThreshold) + { + if(g_ea_trade.PositionClose(g_ea_position.Ticket())) + { + Print("* Structure Close: ", structType, + " | Level=", DoubleToString(structLevel, _Digits), + " | Strength=", DoubleToString(structStrength, 2), + " | R:R=", DoubleToString(current_rr, 2), + " | Threshold=", DoubleToString(structCloseThreshold, 2), "R", + " | Regime=", EnumToString(g_regimeData.regime)); + } + continue; + } + } + } + } + } + // =========================================================== + // * v9.03 FIX#4: SL LADDER -- Lock profit at each TP level + // When TP1 position closed: move TP2+TP3 SL -> TP1 price + // When TP2 position closed: move TP3 SL -> TP2 price + // This ensures runner always has locked profit from previous TP. + // =========================================================== + if((isTP2_Tranche || isTP3_Tranche) && InpEnableMultiTP) + { + // Find matching MultiTP entry by entry price + direction + int posDir = (g_ea_position.Type() == POSITION_TYPE_BUY) ? 1 : -1; + for(int m = 0; m < ArraySize(g_multiTPEntries); m++) + { + if(!g_multiTPEntries[m].active && !g_multiTPEntries[m].tp1Hit) continue; + if(MathAbs(g_multiTPEntries[m].entryPrice - open_price) > g_pipValue * 2) continue; + if(g_multiTPEntries[m].direction != posDir) continue; + // -- TP2 tranche: if TP1 was hit -> SL at TP1 price with buffer -- + // * v9.16: Was 50% of TP1 distance. Now 100% = guaranteed profit at TP1 level. + // * v9.23 FIX#68: Add 25% buffer BELOW TP1 price so TP2 has breathing room. + // Without buffer: any retracement after TP1 immediately closes TP2 at TP1 = $0 extra. + // With buffer: TP2 can retrace 25% of TP1 profit before SL triggers. + if(isTP2_Tranche && g_multiTPEntries[m].tp1Hit) + { + // v9.23 FIX#68: Buffer = 25% of distance from entry to TP1 + double tp1Dist = MathAbs(g_multiTPEntries[m].tp1Price - g_multiTPEntries[m].entryPrice); + double cascadeBuffer = tp1Dist * 0.25; + double ladderSL; + if(posDir == 1) + ladderSL = g_multiTPEntries[m].tp1Price - cascadeBuffer; // 25% below TP1 + else + ladderSL = g_multiTPEntries[m].tp1Price + cascadeBuffer; // 25% above TP1 + // Safety: ensure ladder SL is still above entry (never lose after TP1) + if(posDir == 1) + ladderSL = MathMax(ladderSL, g_multiTPEntries[m].entryPrice + g_pipValue); + else + ladderSL = MathMin(ladderSL, g_multiTPEntries[m].entryPrice - g_pipValue); + bool shouldMove = false; + if(posDir == 1) + shouldMove = (ladderSL > current_sl + g_pipValue); + else + shouldMove = (ladderSL < current_sl - g_pipValue); + if(shouldMove) + { + if(SafePositionModify(g_ea_position.Ticket(), ladderSL, current_tp, "SL_Ladder_TP1_buf25pct")) + { + Print("* v9.23 FIX#68 SL LADDER: TP2 tranche SL -> TP1 -25% buffer | SL=", + DoubleToString(current_sl, _Digits), " -> ", DoubleToString(ladderSL, _Digits), + " | TP1=", DoubleToString(g_multiTPEntries[m].tp1Price, _Digits), + " | Buffer=", DoubleToString(cascadeBuffer/_Point * _Point / g_pipValue, 1), "p", + " | Ticket #", g_ea_position.Ticket()); + } + continue; + } + } + // -- TP3 tranche: cascading SL from TP levels with buffer -- + // * v9.16: TP2 hit -> SL at TP2 price (100%). TP1 only hit -> SL at TP1 price (100%). + // * v9.23 FIX#68: Add 25% buffer so TP3 runner has room after each TP cascade. + if(isTP3_Tranche) + { + double ladderSL = 0; + string ladderLevel = ""; + double entryP = g_multiTPEntries[m].entryPrice; + double cascBuf = 0; + if(g_multiTPEntries[m].tp2Hit) + { + // TP2 hit -> lock profit at TP2 level with buffer + double tp2Dist = MathAbs(g_multiTPEntries[m].tp2Price - entryP); + cascBuf = tp2Dist * 0.25; + ladderSL = (posDir == 1) + ? g_multiTPEntries[m].tp2Price - cascBuf + : g_multiTPEntries[m].tp2Price + cascBuf; + ladderLevel = "TP2(cascade+buf)"; + } + else if(g_multiTPEntries[m].tp1Hit) + { + // Only TP1 hit -> SL at TP1 price with buffer + double tp1Dist = MathAbs(g_multiTPEntries[m].tp1Price - entryP); + cascBuf = tp1Dist * 0.25; + ladderSL = (posDir == 1) + ? g_multiTPEntries[m].tp1Price - cascBuf + : g_multiTPEntries[m].tp1Price + cascBuf; + ladderLevel = "TP1(cascade+buf)"; + } + // Safety: ensure always above entry + if(ladderSL > 0) + { + if(posDir == 1) ladderSL = MathMax(ladderSL, entryP + g_pipValue); + else ladderSL = MathMin(ladderSL, entryP - g_pipValue); + } + if(ladderSL > 0) + { + bool shouldMove = false; + if(posDir == 1) + shouldMove = (ladderSL > current_sl + g_pipValue); + else + shouldMove = (ladderSL < current_sl - g_pipValue); + if(shouldMove) + { + if(SafePositionModify(g_ea_position.Ticket(), ladderSL, current_tp, "SL_Ladder_" + ladderLevel)) + { + Print("* v9.23 FIX#68 SL LADDER: TP3 tranche SL -> ", ladderLevel, + " | SL=", DoubleToString(current_sl, _Digits), " -> ", DoubleToString(ladderSL, _Digits), + " | Buffer=", DoubleToString(cascBuf/_Point * _Point / g_pipValue, 1), "p", + " | Ticket #", g_ea_position.Ticket()); + } + continue; + } + } + } + break; // Found matching entry, stop searching + } + } + if(isTP1_Tranche) + { + double tp1BEThreshold = EA_TP1_BE_Threshold; // * v6.39: configurable (was hardcoded 2.5) + // * v9.03 FIX#10: TF-aware TP1 BE -- scalp locks profit earlier, swing waits longer + if(AutoOpt_Enabled && g_autoOptInitialized) + { + switch(g_marketSnap.tf_category) + { + case TF_CAT_SCALP: tp1BEThreshold = MathMax(1.0, EA_TP1_BE_Threshold * 0.8); break; + case TF_CAT_INTRADAY: break; // keep input value + case TF_CAT_INTRASWING: tp1BEThreshold = EA_TP1_BE_Threshold * 1.1; break; // H1: slight increase + case TF_CAT_SWING: tp1BEThreshold = EA_TP1_BE_Threshold * 1.3; break; + case TF_CAT_POSITION: tp1BEThreshold = EA_TP1_BE_Threshold * 1.5; break; + } + } + if(EA_MoveToBreakEven && current_rr >= tp1BEThreshold) + { + bool should_be_tp1 = false; + if(g_ea_position.Type() == POSITION_TYPE_BUY) + should_be_tp1 = (current_sl < open_price); + else + should_be_tp1 = (current_sl > open_price); + if(should_be_tp1) + { + double be_sl_tp1 = open_price; + if(g_ea_position.Type() == POSITION_TYPE_BUY) + be_sl_tp1 += EA_BE_Buffer_Pips * g_pipValue; // * v6.39: configurable buffer + else + be_sl_tp1 -= EA_BE_Buffer_Pips * g_pipValue; // * v6.39: configurable buffer + if(SafePositionModify(g_ea_position.Ticket(), be_sl_tp1, g_ea_position.TakeProfit(), "TP1_Late_BE")) + { + Print("* v6.39 TP1 Late BE: Threshold=", DoubleToString(tp1BEThreshold, 1), + "R | Buffer=", DoubleToString(EA_BE_Buffer_Pips, 1), "p | Protecting 50% position | R:R=", DoubleToString(current_rr, 2), + " | Ticket #", g_ea_position.Ticket()); + } + continue; + } + } + // Below 2.5R: TP1 has no BE (let it develop toward target) + } + else + { + // * v9.30 FIX#100: Per-trade BE threshold -- look up from g_multiTPEntries + // Each trade stores its own BE_RR calculated at open from TP1/SL ratio. + // Falls back to global g_workingBE_RR if entry not found (safety). + double perTrade_BE = g_workingBE_RR; // fallback + for(int _m = 0; _m < ArraySize(g_multiTPEntries); _m++) + { + if(g_multiTPEntries[_m].active && + MathAbs(g_multiTPEntries[_m].entryPrice - open_price) < g_pipValue * 3 && + g_multiTPEntries[_m].perTrade_BE_RR > 0) + { + perTrade_BE = g_multiTPEntries[_m].perTrade_BE_RR; + break; + } + } + double beThreshold = perTrade_BE; + // CT trade: tighter BE threshold — entering against trend, protect faster + bool _pgCT = false; + for(int _mct = 0; _mct < ArraySize(g_multiTPEntries); _mct++) + { + if(!g_multiTPEntries[_mct].active) continue; + if(MathAbs(g_multiTPEntries[_mct].entryPrice - open_price) > g_pipValue * 5) continue; + _pgCT = g_multiTPEntries[_mct].isCounterTrend; + break; + } + if(_pgCT && beThreshold > 0) beThreshold = MathMax(0.35, beThreshold * 0.80); + // TP2/TP3 tranche handling + if(isTP2_Tranche) + { + // * v10.08 FIX#278: TP2 BE now reads g_workingBE_TP2_RR (AutoOpt-aware). + // OLD: beThreshold = EA_TP2_BE_Threshold (raw input, no AutoOpt scaling). + // FIX: g_workingBE_TP2_RR = MAX(input, tp2_rr*0.85) — computed in ApplyAutoOptToWorkingVars. + // H4: tp2_rr=2.4 → g_workingBE_TP2_RR = MAX(2.5, 2.04) = 2.5R (input wins, expected). + // If AutoOpt changes tp2_rr (different pair/TF), TP2 BE updates automatically. + beThreshold = g_workingBE_TP2_RR; + // * v9.03 FIX#10: TF-aware TP2 BE scaling (applied on top of g_workingBE_TP2_RR) + if(AutoOpt_Enabled && g_autoOptInitialized) + { + switch(g_marketSnap.tf_category) + { + case TF_CAT_SCALP: beThreshold = MathMax(1.5, g_workingBE_TP2_RR * 0.75); break; + case TF_CAT_INTRADAY: break; // keep g_workingBE_TP2_RR as-is + case TF_CAT_INTRASWING: beThreshold = g_workingBE_TP2_RR * 1.1; break; // H1 + case TF_CAT_SWING: beThreshold = g_workingBE_TP2_RR * 1.2; break; // H4 + case TF_CAT_POSITION: beThreshold = g_workingBE_TP2_RR * 1.4; break; // D1 + } + } + } + else + { + // TP3 or single position: regime-adaptive + if(g_regimeData.regime == REGIME_RANGING || g_regimeData.regime == REGIME_RANGING_TIGHT || + g_regimeData.regime == REGIME_RANGING_WIDE) + beThreshold = g_workingBE_Ranging_RR; // * v9.03: AutoOpt-adjusted + else if(!Regime_AdjustSL && !Regime_AdjustTP && !Regime_AdjustPosition) + { + // No regime adaptation - use defaults (beThreshold stays at g_workingBE_RR) + } + else if(g_regimeData.regime == REGIME_VOLATILE || g_regimeData.regime == REGIME_CHOPPY) + beThreshold = perTrade_BE * 1.20; // * v9.30: Volatile: 120% of per-trade BE (wider) + else if(g_regimeData.regime == REGIME_TRENDING || g_regimeData.regime == REGIME_TREND_UP || + g_regimeData.regime == REGIME_TREND_DOWN || g_regimeData.regime == REGIME_STRONG_TREND_UP || + g_regimeData.regime == REGIME_STRONG_TREND_DOWN) + beThreshold = g_workingBE_RR; // * v9.03: Trending: delayed BE = let winners run + } + if(EA_MoveToBreakEven && current_rr >= beThreshold) + { + bool should_move_to_be = false; + if(g_ea_position.Type() == POSITION_TYPE_BUY) + should_move_to_be = (current_sl < open_price); + else + should_move_to_be = (current_sl > open_price); + if(should_move_to_be) + { + // * v9.35 FIX#135a: ProfitLock_BE lockDist corrected. + // BUG: lockDist = MathMax(bufferDist=2pts, riskDist=50-300pts) = riskDist. + // On H4 riskDist can be 150-300pts -> be_sl = entry+300pts = current_price + // -> SL fires IMMEDIATELY the moment price reaches beThreshold. + // Confirmed in smartEA log5: 3 trades (Aug 7/11) lost entry+1R with RR=1.04-1.05. + // On H4 the effect is even more pronounced (wider SL = bigger riskDist). + // FIX: lockDist = bufferDist only (EA_BE_Buffer_Pips=2p). + // SL moves to entry+2pts = proper breakeven lock, not to current price. + double bufferDist = EA_BE_Buffer_Pips * g_pipValue; + double lockDist = bufferDist; // * v9.35 FIX#135a: was MathMax(bufferDist, riskDist*1.0) + double be_sl = open_price; + if(g_ea_position.Type() == POSITION_TYPE_BUY) + be_sl += lockDist; + else + be_sl -= lockDist; + if(SafePositionModify(g_ea_position.Ticket(), be_sl, g_ea_position.TakeProfit(), "ProfitLock_BE")) + { + Print("* v9.35 FIX#135a Profit Lock: Threshold=", DoubleToString(beThreshold, 1), + "R | SL->BE+", DoubleToString(EA_BE_Buffer_Pips, 1), "p", + " | Regime=", EnumToString(g_regimeData.regime), + " | R:R=", DoubleToString(current_rr, 2), + " | Ticket #", g_ea_position.Ticket()); + } + continue; + } + } + } // * v6.38: Close else block (TP2/TP3 adaptive BE -- TP1 handled above with late BE) + // =========================================================== + // * v9.23 FIX#65/66: UNIVERSAL PER-TRANCHE TRAILING + // ALL tranches trail from EA_Trail_Activation_RR (1.2R) + // TP1: 1.5xATR (wider) + fixed TP as primary target + // TP2: 1.8xATR (medium) -- fixed TP2 + trail as fallback + // TP3: 2.5xATR (loose) -- fixed TP3 + loose trail runner + // Cascading SL: TP1 hit -> SL floor = TP1-25% buffer for TP2/TP3 + // Hard entry floor: trail NEVER locks below entry (FIX#69) + // =========================================================== + // * v9.30 FIX#100: Per-trade Trail threshold + // * FIX#110b: Use g_workingFIX41_TrailStart as fallback (AutoOpt+TF-aware) + // instead of raw EA_Trail_Activation_RR (fixed 0.3R for ALL TFs). + // g_workingFIX41_TrailStart = max(EA_Trail_Activation_RR, breakeven_rr) + // which AutoOpt sets TF-aware: M5=~0.6R, H1=~0.8R, H4=~1.0R + // Result: M5 trail no longer fires at 0.3R (= 1.5pip noise territory) + // Trail start RR: read from multiTPEntry (set at open from pair table). + // Fallback: EA_Trail_Activation_RR (direct user input — deterministic). + // Never use g_workingFIX41_TrailStart: it is AutoOpt-computed and varies per run. + double perTrade_Trail = EA_Trail_Activation_RR; + for(int _mt = 0; _mt < ArraySize(g_multiTPEntries); _mt++) + { + if(g_multiTPEntries[_mt].active && + MathAbs(g_multiTPEntries[_mt].entryPrice - open_price) < g_pipValue * 3 && + g_multiTPEntries[_mt].perTrade_Trail_RR > 0) + { + perTrade_Trail = g_multiTPEntries[_mt].perTrade_Trail_RR; + break; + } + } + double trailStartRR = perTrade_Trail; // * v9.30: per-trade dynamic + double trailDistMult = EA_Trail_TP3_ATR; // * v9.23: Default = TP3 (loosest) + bool allow_trailing = true; + // * v9.16: Per-tranche trail distance (UNIVERSAL -- not AutoOpt controlled) + if(isTP1_Tranche) + trailDistMult = EA_Trail_TP1_ATR; // 1.5xATR -- wider buffer for M15 noise + else if(isTP2_Tranche) + trailDistMult = EA_Trail_TP2_ATR; // 1.8xATR -- medium, swing capture + else if(isTP3_Tranche) + trailDistMult = EA_Trail_TP3_ATR; // 2.5xATR -- loose, let runner run + if(EA_UseTrailing && allow_trailing && atr > 0) + { + double entry_price = g_ea_position.PriceOpen(); + double sl_distance = MathAbs(entry_price - current_sl); + if(sl_distance <= 0) sl_distance = atr; // fallback + bool isBuy = (g_ea_position.Type() == POSITION_TYPE_BUY); + // ======================================================= + // * v9.16 FIX#40: PROFIT-GUARANTEED PROGRESSIVE TRAIL + // Step 1: Calculate profit floor based on R:R milestones + // Step 2: Calculate normal trail SL + // Step 3: Final SL = MAX(profitFloor, trailSL) -- never below floor + // ======================================================= + // -- Step 1: Progressive Profit Floor -- + double profitFloorSL = 0; + bool floorActive = false; + string floorLabel = ""; + if(EA_Trail_GuaranteeProfit && current_rr >= EA_Trail_BE_Trigger) + { + double lockPips = 0; + if(current_rr >= EA_Trail_Lock3_RR) + { + lockPips = 1.2 * sl_distance / g_pipValue; // Lock +1.2R + floorLabel = "LOCK3(+1.2R)"; + } + else if(current_rr >= EA_Trail_Lock2_RR) + { + lockPips = 0.7 * sl_distance / g_pipValue; // Lock +0.7R + floorLabel = "LOCK2(+0.7R)"; + } + else if(current_rr >= EA_Trail_Lock1_RR) + { + lockPips = 0.3 * sl_distance / g_pipValue; // Lock +0.3R + floorLabel = "LOCK1(+0.3R)"; + } + else + { + lockPips = EA_Trail_BE_LockPips; // Breakeven + buffer + floorLabel = "BE"; + } + // Ensure minimum profit + lockPips = MathMax(lockPips, EA_Trail_MinProfitPips); + if(isBuy) + profitFloorSL = entry_price + lockPips * g_pipValue; + else + profitFloorSL = entry_price - lockPips * g_pipValue; + floorActive = true; + } + // -- Step 2: Normal Trail SL (only if current_rr >= trailStartRR) -- + double trailSL = 0; + bool trailActive = false; + if(current_rr >= trailStartRR) + { + // * v9.36 FIX#139: Trail distance uses ATR cached at entry, NOT live ATR. + // Live ATR changes every bar (shrinks in quiet markets, inflates on spikes). + // Using live ATR means trail_dist varies → inconsistent protection. + // CORRECT: trail_dist = fixed multiple of ATR that was valid when we entered. + // Also: trail_dist is capped at 0.5 × sl_dist_cached so on wide-SL trades + // (e.g. 15p SL) the trail doesn't need 7.4p excursion before protecting. + double atr_for_trail = atr; // fallback: live ATR + double sl_dist_for_trail = sl_distance; // fallback: current sl_distance + ulong _trail_ticket = g_ea_position.Ticket(); + for(int _mtp_t = 0; _mtp_t < ArraySize(g_multiTPEntries); _mtp_t++) + { + if(g_multiTPEntries[_mtp_t].active && + (ulong)g_multiTPEntries[_mtp_t].ticket == _trail_ticket && + g_multiTPEntries[_mtp_t].atr_at_entry > 0) + { + atr_for_trail = g_multiTPEntries[_mtp_t].atr_at_entry; + sl_dist_for_trail = g_multiTPEntries[_mtp_t].sl_dist_cached; + break; + } + } + // Trail dist = ATR-based, but capped at 50% of original SL + // so on wide-SL trades the trail still provides meaningful protection. + double trail_dist_atr = trailDistMult * atr_for_trail; + double trail_dist_cap = sl_dist_for_trail * 0.50; // max 50% of original SL + double trail_dist = MathMin(trail_dist_atr, trail_dist_cap); + // Safety floor: at least 1 ATR (prevents zero/negative trail on exotic calcs) + if(trail_dist < atr_for_trail * 0.5) trail_dist = atr_for_trail * 0.5; + if(isBuy) + trailSL = current_price - trail_dist; + else + trailSL = current_price + trail_dist; + trailActive = true; + } + // * v9.39 FIX#158: TP-PROPORTIONAL REAL-TIME TRAIL + // ----------------------------------------------------------------------- + // PROBLEM: ATR-trail ignores the trade's own target levels. A trade with + // TP1=+8p and a trade with TP1=+24p use identical trail distances, but + // the tight TP trade closes before reaching TP1 while the wide one over- + // gives back profit. Result: trail never adapts to WHERE price is relative + // to the targets, only to ATR magnitude. + // + // FIX: Compute trail distance as a fraction of the distance to the NEXT + // TP level. As price moves through zones (Entry→TP1, TP1→TP2, TP2→TP3), + // the trail automatically tightens proportionally — faster near TP zones, + // looser in the run-up. This is the "TP-aware real-time trail". + // + // Additionally: if g_isBullishStructure flips against the trade (CHoCH), + // tighten immediately to 0.5× ATR (emergency protection mode). + // + // Activation: price crosses 35% of entry→TP1 distance (conservative start + // so it does NOT interfere with the initial trade run-up phase). + // ----------------------------------------------------------------------- + { + double _tp158_tp1 = 0, _tp158_tp2 = 0, _tp158_tp3 = 0; + double _tp158_atr = atr; + // Look up TP levels from registry (by entry price proximity for runner-ticket safety) + ulong _t158_tk = g_ea_position.Ticket(); + double _t158_bestDist = g_pipValue * 5.0; + for(int _m158 = 0; _m158 < ArraySize(g_multiTPEntries); _m158++) + { + if(!g_multiTPEntries[_m158].active) continue; + // Match by ticket first, fallback by entry price (FIX#155 consistency) + bool _ticketMatch = ((ulong)g_multiTPEntries[_m158].ticket == _t158_tk); + double _eDist = MathAbs(g_multiTPEntries[_m158].entryPrice - entry_price); + bool _priceMatch = (_eDist < _t158_bestDist); + if(_ticketMatch || _priceMatch) + { + _tp158_tp1 = g_multiTPEntries[_m158].tp1Price; + _tp158_tp2 = g_multiTPEntries[_m158].tp2Price; + _tp158_tp3 = g_multiTPEntries[_m158].tp3Price; + if(g_multiTPEntries[_m158].atr_at_entry > g_pipValue) + _tp158_atr = g_multiTPEntries[_m158].atr_at_entry; + if(_ticketMatch || _eDist < _t158_bestDist) + _t158_bestDist = _eDist; + if(_ticketMatch) break; // exact match wins + } + } + if(_tp158_tp1 > 0 && _tp158_atr > g_pipValue) + { + double _tp158_tp1Dist = MathAbs(_tp158_tp1 - entry_price); // entry→TP1 distance + // * v9.47 FIX#191: Unified trail activation gate. + // PROBLEM: FIX#158 activated at 35% of TP1 distance (0.63R on H4), + // completely ignoring perTrade_Trail_RR (0.99R from FIX#100). + // Result: trail fired at 0.63R with 22p dist → H4 bar pullback (30-45p) + // stopped trades at 0.95R instead of letting them run to TP1 (1.80R). + // FIX: activation = MAX(perTrade_Trail_RR × sl_dist, tp1_dist × 0.35) + // → H4: MAX(0.99×43.7p, 0.35×78.7p) = 43.3p = 0.99R ← per-trade wins + // → M15: MAX(0.72×8p, 0.35×15p) = 5.8p ≈ 0.72R ← unchanged + // --- FIX#193 (v9.48): Trail activation gate raised for H4. + // Old: 0.35xTP1 = 0.63R on H4 → swept by normal 30-45p bar pullback. + // New: TF-aware fraction — H4=0.55xTP1 (≈0.99R), D1=0.60, H1=0.40, M5/M15=0.35. + // --- FIX#193b (v9.48b): AutoOpt guard RESTORED. Design: + // AutoOpt ON + initialized → tf_category switch (full auto) + // AutoOpt ON + NOT yet init → _Period fallback (correct TF values during init) + // AutoOpt OFF → 0.35 default (user sets manually via inputs) --- + double _tp1ActivFrac = 0.35; + if(AutoOpt_Enabled && g_autoOptInitialized) + { + switch(g_marketSnap.tf_category) + { + case TF_CAT_SWING: _tp1ActivFrac = 0.55; break; // H4 + case TF_CAT_POSITION: _tp1ActivFrac = 0.60; break; // D1 + case TF_CAT_INTRASWING: _tp1ActivFrac = 0.30; break; // H1 FIX#504: 0.40→0.30 (trail activates earlier) + default: _tp1ActivFrac = 0.35; break; // M5/M15 + } + } + else if(AutoOpt_Enabled && !g_autoOptInitialized) + { + // AutoOpt ON but not yet initialized → _Period fallback + // Prevents H4 trades during init window from using 0.35 default + switch(_Period) + { + case PERIOD_H4: + case PERIOD_H3: _tp1ActivFrac = 0.55; break; + case PERIOD_D1: + case PERIOD_W1: _tp1ActivFrac = 0.60; break; + case PERIOD_H1: + case PERIOD_H2: _tp1ActivFrac = 0.30; break; // FIX#504 + default: _tp1ActivFrac = 0.35; break; + } + } + // AutoOpt OFF: stays 0.35 — user controls manually via inputs + double _perTradeActivation = perTrade_Trail * sl_distance; + double _tp1Activation = _tp158_tp1Dist * _tp1ActivFrac; + double _activationDist = MathMax(_perTradeActivation, _tp1Activation); + double _priceMove = isBuy ? (current_price - entry_price) : (entry_price - current_price); + if(_priceMove >= _activationDist && _tp158_tp1Dist > g_pipValue) + { + double _tp158_trailDist = 0; + string _tp158_zone = ""; + // Determine which zone price is in and set trail accordingly + double _tp158_tp2Dist = (_tp158_tp2 > 0) ? MathAbs(_tp158_tp2 - entry_price) : _tp158_tp1Dist * 1.6; + double _tp158_tp3Dist = (_tp158_tp3 > 0) ? MathAbs(_tp158_tp3 - entry_price) : _tp158_tp1Dist * 2.2; + bool _pastTP1 = isBuy ? (current_price > _tp158_tp1) : (current_price < _tp158_tp1); + bool _pastTP2 = isBuy ? (current_price > _tp158_tp2 && _tp158_tp2 > 0) + : (current_price < _tp158_tp2 && _tp158_tp2 > 0); + if(_pastTP2) + { + // Zone C (past TP2): very tight — protect TP2 profit + double _tp3Span = MathAbs(_tp158_tp3Dist - _tp158_tp2Dist); + _tp158_trailDist = MathMax(_tp3Span * 0.35, _tp158_atr * 0.6); + _tp158_zone = "ZoneC>TP2"; + } + else if(_pastTP1) + { + // Zone B (TP1 → TP2): medium trail — let runner breathe but protect gains + double _tp2Span = MathAbs(_tp158_tp2Dist - _tp158_tp1Dist); + _tp158_trailDist = MathMax(_tp2Span * 0.40, _tp158_atr * 0.8); + _tp158_zone = "ZoneB>TP1"; + } + else + { + // Zone A (Entry → TP1): trail = max(28% of TP1 dist, TF-aware ATR mult) + // * v9.47 FIX#191: H4/D1 need wider Zone A floor (ATR×1.2) to survive + // 30-45p bar pullbacks. M5/M15 keep ATR×0.7 (tight is correct there). + double _zoneA_atrMult = 0.7; + if(AutoOpt_Enabled && g_autoOptInitialized) + { + switch(g_marketSnap.tf_category) + { + case TF_CAT_SWING: _zoneA_atrMult = 1.5; break; // H4 FIX#291: 1.2→1.5 (24p→30p @ ATR=20p, covers typical H4 spike) + case TF_CAT_POSITION: _zoneA_atrMult = 1.5; break; // D1 + case TF_CAT_INTRASWING: _zoneA_atrMult = 0.6; break; // H1 FIX#504: 0.9→0.6 (tighter trail) + default: _zoneA_atrMult = 0.7; break; // M5/M15 + } + } + else if(AutoOpt_Enabled && !g_autoOptInitialized) + { + switch(_Period) + { + case PERIOD_H4: + case PERIOD_H3: _zoneA_atrMult = 1.5; break; // FIX#291: 1.2→1.5 + case PERIOD_D1: + case PERIOD_W1: _zoneA_atrMult = 1.5; break; + case PERIOD_H1: + case PERIOD_H2: _zoneA_atrMult = 0.6; break; // FIX#504 + default: _zoneA_atrMult = 0.7; break; + } + } + // AutoOpt OFF: stays 0.7 — user controls manually + // --- FIX#193b (v9.48): Zone A trail dist fraction raised for H4. + // Old 0.28xTP1 = 22p on H4 < bar pullback 30-45p → premature close. + // TF-aware: H4=0.55, D1=0.60, H1=0.40, M5/M15=0.28. + // Same 3-tier guard as _tp1ActivFrac above. --- + double _zoneA_tp1Frac = 0.28; + if(AutoOpt_Enabled && g_autoOptInitialized) + { + switch(g_marketSnap.tf_category) + { + case TF_CAT_SWING: _zoneA_tp1Frac = 0.55; break; // H4 + case TF_CAT_POSITION: _zoneA_tp1Frac = 0.60; break; // D1 + case TF_CAT_INTRASWING: _zoneA_tp1Frac = 0.25; break; // H1 FIX#504: 0.40→0.25 (locks 0.40R at 0.9R peak) + default: _zoneA_tp1Frac = 0.28; break; // M5/M15 + } + } + else if(AutoOpt_Enabled && !g_autoOptInitialized) + { + switch(_Period) + { + case PERIOD_H4: + case PERIOD_H3: _zoneA_tp1Frac = 0.55; break; + case PERIOD_D1: + case PERIOD_W1: _zoneA_tp1Frac = 0.60; break; + case PERIOD_H1: + case PERIOD_H2: _zoneA_tp1Frac = 0.25; break; // FIX#504 + default: _zoneA_tp1Frac = 0.28; break; + } + } + // AutoOpt OFF: stays 0.28 — user controls manually + _tp158_trailDist = MathMax(_tp158_tp1Dist * _zoneA_tp1Frac, _tp158_atr * _zoneA_atrMult); + _tp158_zone = "ZoneA _tp158_tp1Dist * 0.30) + { + _tp158_trailDist = MathMin(_tp158_trailDist, _tp158_atr * 0.8); + _tp158_zone = "CHoCH_TIGHT"; + } + // Cooperative SmartExit trail tightening — direct call, no global flag. + // If SmartExit_Check returns 0 (not enough signals to close) but + // g_lastSE_catCount > 0 (at least 1 category active), pre-tighten trail. + // This runs per-position, per-tick — no race condition with ManageMultiTP. + { + int _seCheck = SmartExit_Check(isBuy, current_rr, entry_price, current_price, _tp158_atr); + // -1 = tighten trail (2 cats warning), >0 = should close (handled below) + if(_seCheck == -1 || (_seCheck == 0 && g_lastSE_catCount >= 2 && + _priceMove > _tp158_tp1Dist * 0.25)) + { + _tp158_trailDist = MathMin(_tp158_trailDist, _tp158_atr * 0.8); + _tp158_zone = "SE_WARN_TIGHT"; + } + } + // Compute candidate SL from TP-proportional trail + double _tp158_sl = isBuy ? (current_price - _tp158_trailDist) + : (current_price + _tp158_trailDist); + // Only apply if more protective than ATR trail + bool _tp158_better = !trailActive || + (isBuy ? (_tp158_sl > trailSL) : + (_tp158_sl < trailSL)); + if(_tp158_better) + { + trailSL = _tp158_sl; + trailActive = true; + if(g_verboseLog) + PrintFormat("* FIX#158 TP-Trail [%s]: dist=%.1fp | SL->%.5f | RR=%.2f | CHoCH=%d", + _tp158_zone, _tp158_trailDist / g_pipValue, _tp158_sl, + current_rr, (int)_structureAgainst); + } + } + } + } + // -- End FIX#158 -- + // -- Step 3: Merge -- take the BEST (most protective) SL -- + double new_sl = 0; + string slSource = ""; + if(floorActive && trailActive) + { + if(isBuy) + new_sl = MathMax(profitFloorSL, trailSL); + else + new_sl = MathMin(profitFloorSL, trailSL); + slSource = (new_sl == profitFloorSL) ? floorLabel : "TRAIL"; + } + else if(floorActive) + { + new_sl = profitFloorSL; + slSource = floorLabel; + } + else if(trailActive) + { + new_sl = trailSL; + slSource = "TRAIL"; + } + else + continue; // nothing to do + // -- Step 4: Only modify if new SL is BETTER than current -- + bool should_modify = false; + if(isBuy) + should_modify = (new_sl > current_sl) && (new_sl < current_price); + else + should_modify = (new_sl < current_sl) && (new_sl > current_price); + // * v9.23 FIX#69: Hard entry floor -- trail NEVER locks SL below entry (buy) + // or above entry (sell). Prevents "false profit" exits below entry. + // Evidence: 49 trail modifications with Locked < 0 (avg -3.4 pips). + // These created positions that "won" intraday but closed at a LOSS. + if(should_modify && EA_Trail_GuaranteeProfit) + { + double entryFloor = isBuy + ? entry_price // buy: SL must be at or above entry + : entry_price; // sell: SL must be at or below entry + if(isBuy && new_sl < entryFloor) + { + // Trail would lock below entry -- reject modification + // (will be picked up again once price moves high enough) + should_modify = false; + if(g_verboseLog) + Print("* v9.23 FIX#69 ENTRY FLOOR: Trail rejected | SL=", + DoubleToString(new_sl, _Digits), " < entry=", + DoubleToString(entry_price, _Digits), + " | would lock at ", DoubleToString((new_sl - entry_price)/_Point * _Point / g_pipValue, 1), + "p (negative) | Ticket #", g_ea_position.Ticket()); + } + else if(!isBuy && new_sl > entryFloor) + { + should_modify = false; + if(g_verboseLog) + Print("* v9.23 FIX#69 ENTRY FLOOR: Trail rejected | SL=", + DoubleToString(new_sl, _Digits), " > entry=", + DoubleToString(entry_price, _Digits), + " | would lock above entry | Ticket #", g_ea_position.Ticket()); + } + } + if(should_modify) + { + // * v7.4 FIX: Use g_workingMaxSpreadPips and correct pip conversion + double modSpreadPips = (double)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * g_point / g_pipValue; + if(!EA_CheckSpreadOnModify || modSpreadPips <= g_workingMaxSpreadPips) + if(SafePositionModify(g_ea_position.Ticket(), new_sl, g_ea_position.TakeProfit(), "ProfitGuard_Trail")) + { + double lockedPips = isBuy ? (new_sl - entry_price) / g_pipValue : (entry_price - new_sl) / g_pipValue; + Print("* v9.22 FIX#40 Trail [ACTIVE]: ", slSource, + " | R:R=", DoubleToString(current_rr, 2), + " | SL=", DoubleToString(current_sl, _Digits), "->", DoubleToString(new_sl, _Digits), + " | Locked=", DoubleToString(lockedPips, 1), "p profit", + " | Regime=", EnumToString(g_regimeData.regime), + " | Ticket #", g_ea_position.Ticket()); + } + } + } + } +} +// end: for(int i = PositionsTotal()) +} +// end EA_ManagePositions +//+------------------------------------------------------------------+ +//| Regime-adaptive position management - auto-switches between | +//| Option D (Safe/Risk Ladder) and Option E (Aggressive/Trailing) | +//| based on real-time market regime detection | +//+------------------------------------------------------------------+ +void ManageFullHybrid() +{ + // Safety checks + if(!EA_UseFullHybrid) return; + if(!EA_UseMultipleTP) return; // Hybrid requires multi-TP system + // =============================================================== + // STEP 1: UPDATE TRACKERS - Scan open positions + // =============================================================== + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + if(!g_ea_position.SelectByIndex(i)) continue; + if(g_ea_position.Symbol() != _Symbol) continue; + if(g_ea_position.Magic() != EA_MagicNumber) continue; + string posComment = g_ea_position.Comment(); + // Extract base comment (remove _TP1, _TP2, _TP3 suffix) + string baseComment = ""; + int tpPos = StringFind(posComment, "_TP"); + if(tpPos > 0) + baseComment = StringSubstr(posComment, 0, tpPos); + else + continue; // Not a multi-TP position + // Find or create tracker for this trade + int trackerIdx = -1; + for(int t = 0; t < g_hybridTrackerCount; t++) + { + if(g_hybridTrackers[t].baseComment == baseComment) + { + trackerIdx = t; + break; + } + } + if(trackerIdx < 0) + { + // Create new tracker + if(g_hybridTrackerCount >= MAX_HYBRID_TRACKERS) + { + // Cleanup old trackers (remove ones older than 24 hours) + datetime cutoff = TimeCurrent() - (24 * 3600); + for(int t = g_hybridTrackerCount - 1; t >= 0; t--) + { + if(g_hybridTrackers[t].tp1CloseTime > 0 && + g_hybridTrackers[t].tp1CloseTime < cutoff) + { + // Remove this tracker by shifting array + for(int s = t; s < g_hybridTrackerCount - 1; s++) + g_hybridTrackers[s] = g_hybridTrackers[s + 1]; + g_hybridTrackerCount--; + } + } + } + if(g_hybridTrackerCount < MAX_HYBRID_TRACKERS) + { + trackerIdx = g_hybridTrackerCount; + g_hybridTrackerCount++; + // Initialize new tracker + g_hybridTrackers[trackerIdx].baseComment = baseComment; + g_hybridTrackers[trackerIdx].tp1CloseTime = 0; + g_hybridTrackers[trackerIdx].tp2CloseTime = 0; + g_hybridTrackers[trackerIdx].tp1Processed = false; + g_hybridTrackers[trackerIdx].tp2Processed = false; + g_hybridTrackers[trackerIdx].entryPrice = g_ea_position.PriceOpen(); + g_hybridTrackers[trackerIdx].originalSL = g_ea_position.StopLoss(); + g_hybridTrackers[trackerIdx].currentMode = HYBRID_MODE_SAFE; + g_hybridTrackers[trackerIdx].trailingActive = false; + g_hybridTrackers[trackerIdx].lastModeCheck = 0; + g_hybridTrackers[trackerIdx].regimeAtTP1 = REGIME_UNKNOWN; + g_hybridTrackers[trackerIdx].regimeAtTP2 = REGIME_UNKNOWN; + g_hybridTrackers[trackerIdx].regimeStrengthTP1 = 0.0; + g_hybridTrackers[trackerIdx].regimeStrengthTP2 = 0.0; + } + } + } + // =============================================================== + // STEP 2: DETECT CLOSED TPs via Deal History + // * v8.01 FIX BUG#2: HistorySelect was called every tick -> heavy performance hit! + // Throttle to once every 30 seconds -- TP detections do not need tick precision + // =============================================================== + static datetime lastHybridHistCheck = 0; + if(TimeCurrent() - lastHybridHistCheck >= 30) + { + lastHybridHistCheck = TimeCurrent(); + if(!HistorySelect(TimeCurrent() - (24 * 3600), TimeCurrent())) return; + for(int d = HistoryDealsTotal() - 1; d >= 0; d--) + { + ulong dealTicket = HistoryDealGetTicket(d); + if(dealTicket == 0) continue; + if(HistoryDealGetInteger(dealTicket, DEAL_MAGIC) != EA_MagicNumber) continue; + if(HistoryDealGetString(dealTicket, DEAL_SYMBOL) != _Symbol) continue; + if(HistoryDealGetInteger(dealTicket, DEAL_ENTRY) != DEAL_ENTRY_OUT) continue; + string dealComment = HistoryDealGetString(dealTicket, DEAL_COMMENT); + datetime dealTime = (datetime)HistoryDealGetInteger(dealTicket, DEAL_TIME); + // Check if this is a TP1 or TP2 close + bool isTP1 = (StringFind(dealComment, "_TP1") >= 0); + bool isTP2 = (StringFind(dealComment, "_TP2") >= 0); + if(!isTP1 && !isTP2) continue; + // Extract base comment + string baseComment = ""; + int tpPos = StringFind(dealComment, "_TP"); + if(tpPos > 0) + baseComment = StringSubstr(dealComment, 0, tpPos); + else + continue; + // Update tracker + for(int t = 0; t < g_hybridTrackerCount; t++) + { + if(g_hybridTrackers[t].baseComment == baseComment) + { + if(isTP1 && g_hybridTrackers[t].tp1CloseTime == 0) + { + g_hybridTrackers[t].tp1CloseTime = dealTime; + g_hybridTrackers[t].regimeAtTP1 = g_regimeData.regime; + g_hybridTrackers[t].regimeStrengthTP1 = g_regimeData.trendStrength; + if(g_verboseLog) + Print("* HYBRID: TP1 closed | Regime=", EnumToString(g_regimeData.regime), + " | Strength=", DoubleToString(g_regimeData.trendStrength, 2), + " | Trade=", baseComment); + } + else if(isTP2 && g_hybridTrackers[t].tp2CloseTime == 0) + { + g_hybridTrackers[t].tp2CloseTime = dealTime; + g_hybridTrackers[t].regimeAtTP2 = g_regimeData.regime; + g_hybridTrackers[t].regimeStrengthTP2 = g_regimeData.trendStrength; + if(g_verboseLog) + Print("* HYBRID: TP2 closed | Regime=", EnumToString(g_regimeData.regime), + " | Strength=", DoubleToString(g_regimeData.trendStrength, 2), + " | Trade=", baseComment); + } + break; + } + } + } + } // end throttle block (30-sec HistorySelect gate) + // =============================================================== + // STEP 3: APPLY HYBRID LOGIC - Process each tracker + // =============================================================== + for(int t = 0; t < g_hybridTrackerCount; t++) + { + // -------------------------------------------------------- + // STAGE 1: TP1 CLOSED -> DECIDE MODE + // -------------------------------------------------------- + if(g_hybridTrackers[t].tp1CloseTime > 0 && !g_hybridTrackers[t].tp1Processed) + { + // Determine if market is trending + bool isTrending = IsTrendingRegime(g_hybridTrackers[t].regimeAtTP1, + g_hybridTrackers[t].regimeStrengthTP1); + // Set management mode based on regime + if(isTrending) + { + // AGGRESSIVE MODE: Activate trailing + g_hybridTrackers[t].currentMode = HYBRID_MODE_AGGRESSIVE; + ApplyAggressiveMode(t); + if(g_verboseLog) + Print("* HYBRID STAGE 1: AGGRESSIVE mode activated | Trade=", + g_hybridTrackers[t].baseComment); + } + else + { + // SAFE MODE: Lock at +1R + g_hybridTrackers[t].currentMode = HYBRID_MODE_SAFE; + ApplySafeMode(t, 1.0); + if(g_verboseLog) + Print("* HYBRID STAGE 1: SAFE mode activated | Trade=", + g_hybridTrackers[t].baseComment); + } + g_hybridTrackers[t].tp1Processed = true; + } + // -------------------------------------------------------- + // STAGE 2: TP2 CLOSED -> UPDATE MODE IF NEEDED + // -------------------------------------------------------- + if(g_hybridTrackers[t].tp2CloseTime > 0 && !g_hybridTrackers[t].tp2Processed) + { + bool isTrending = IsTrendingRegime(g_hybridTrackers[t].regimeAtTP2, + g_hybridTrackers[t].regimeStrengthTP2); + if(g_hybridTrackers[t].currentMode == HYBRID_MODE_SAFE && isTrending) + { + // Mode switch: SAFE -> AGGRESSIVE + g_hybridTrackers[t].currentMode = HYBRID_MODE_AGGRESSIVE; + ApplyAggressiveMode(t); + if(g_verboseLog) + Print("* HYBRID STAGE 2: Mode switch SAFE->AGGRESSIVE | Trade=", + g_hybridTrackers[t].baseComment); + } + else if(g_hybridTrackers[t].currentMode == HYBRID_MODE_SAFE) + { + // Still safe mode -> Lock at +2R + ApplySafeMode(t, 2.0); + if(g_verboseLog) + Print("* HYBRID STAGE 2: SAFE mode continues | Lock +2R | Trade=", + g_hybridTrackers[t].baseComment); + } + // If already AGGRESSIVE, trailing continues automatically + g_hybridTrackers[t].tp2Processed = true; + } + // -------------------------------------------------------- + // CONTINUOUS: UPDATE TRAILING IF AGGRESSIVE + // -------------------------------------------------------- + if(g_hybridTrackers[t].currentMode == HYBRID_MODE_AGGRESSIVE && + g_hybridTrackers[t].trailingActive) + { + // Update trailing SL every tick + UpdateTrailingSL(t); + // Optional: Check if regime changed (every 60 seconds) + if(EA_Hybrid_AllowModeSwitch && + (TimeCurrent() - g_hybridTrackers[t].lastModeCheck) >= 60) + { + bool stillTrending = IsTrendingRegime(g_regimeData.regime, + g_regimeData.trendStrength); + if(!stillTrending) + { + // Regime changed to ranging -> Lock current SL and stop trailing + g_hybridTrackers[t].currentMode = HYBRID_MODE_SAFE; + g_hybridTrackers[t].trailingActive = false; + if(g_verboseLog) + Print("* HYBRID: Mode switch AGGRESSIVE->SAFE (regime changed to ranging) | Trade=", + g_hybridTrackers[t].baseComment); + } + g_hybridTrackers[t].lastModeCheck = TimeCurrent(); + } + } + } +} +//+------------------------------------------------------------------+ +//| * HELPER: Check if regime is trending | +//+------------------------------------------------------------------+ +bool IsTrendingRegime(ENUM_MARKET_REGIME regime, double strength) +{ + // Check if regime type indicates trending market + bool regimeMatch = (regime == REGIME_TRENDING || + regime == REGIME_TREND_UP || + regime == REGIME_TREND_DOWN || + regime == REGIME_STRONG_TREND_UP || + regime == REGIME_STRONG_TREND_DOWN || + regime == REGIME_BREAKOUT); + // Check if trend strength meets minimum threshold + // strength = g_regimeData.trendStrength (0-100 scale) + // EA_Hybrid_TrendThreshold = 0.0-1.0 scale + // Must normalize: 0-100 -> 0.0-1.0 + double strengthNormalized = strength / 100.0; + bool strongEnough = (strengthNormalized >= EA_Hybrid_TrendThreshold); + // [v6.41] ADX filter for hybrid regime confirmation + if(EA_Hybrid_UseADX && EA_Hybrid_MinADX > 0 && g_cachedADX < EA_Hybrid_MinADX) + strongEnough = false; + // Return true only if BOTH conditions met: + // 1. Regime type is trending + // 2. Trend strength is above threshold + return (regimeMatch && strongEnough); +} +//+------------------------------------------------------------------+ +//| * HELPER: Apply Safe Mode (Option D - Risk Ladder) | +//+------------------------------------------------------------------+ +void ApplySafeMode(int trackerIdx, double rMultiple) +{ + double originalRisk = MathAbs(g_hybridTrackers[trackerIdx].entryPrice - + g_hybridTrackers[trackerIdx].originalSL); + double lockDistance = rMultiple * originalRisk; + // Find TP2 and TP3 positions + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + if(!g_ea_position.SelectByIndex(i)) continue; + if(g_ea_position.Symbol() != _Symbol) continue; + if(g_ea_position.Magic() != EA_MagicNumber) continue; + string posComment = g_ea_position.Comment(); + bool isTP2 = (StringFind(posComment, g_hybridTrackers[trackerIdx].baseComment + "_TP2") >= 0); + bool isTP3 = (StringFind(posComment, g_hybridTrackers[trackerIdx].baseComment + "_TP3") >= 0); + if(!isTP2 && !isTP3) continue; + double currentSL = g_ea_position.StopLoss(); + double newSL = 0; + if(g_ea_position.Type() == POSITION_TYPE_BUY) + { + newSL = g_hybridTrackers[trackerIdx].entryPrice + lockDistance; + if(newSL <= currentSL) continue; // Only move SL up + } + else + { + newSL = g_hybridTrackers[trackerIdx].entryPrice - lockDistance; + if(newSL >= currentSL) continue; // Only move SL down + } + // Apply new SL + if(SafePositionModify(g_ea_position.Ticket(), newSL, g_ea_position.TakeProfit(), "Hybrid_Safe")) + { + string tranche = isTP2 ? "TP2" : "TP3"; + Print("* HYBRID SAFE MODE: ", tranche, " locked at +", + DoubleToString(rMultiple, 1), "R (Entry+", + DoubleToString(lockDistance / g_pipValue, 1), "p) | Ticket #", + g_ea_position.Ticket()); + } + } +} +//+------------------------------------------------------------------+ +//| * HELPER: Apply Aggressive Mode (Option E - Trailing) | +//+------------------------------------------------------------------+ +void ApplyAggressiveMode(int trackerIdx) +{ + // Move to BE first, then activate trailing + double beDistance = EA_BE_Buffer_Pips * g_pipValue; + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + if(!g_ea_position.SelectByIndex(i)) continue; + if(g_ea_position.Symbol() != _Symbol) continue; + if(g_ea_position.Magic() != EA_MagicNumber) continue; + string posComment = g_ea_position.Comment(); + bool isTP2 = (StringFind(posComment, g_hybridTrackers[trackerIdx].baseComment + "_TP2") >= 0); + bool isTP3 = (StringFind(posComment, g_hybridTrackers[trackerIdx].baseComment + "_TP3") >= 0); + if(!isTP2 && !isTP3) continue; + double currentSL = g_ea_position.StopLoss(); + double newSL = 0; + if(g_ea_position.Type() == POSITION_TYPE_BUY) + { + newSL = g_hybridTrackers[trackerIdx].entryPrice + beDistance; + if(newSL <= currentSL) continue; // Only move SL up + } + else + { + newSL = g_hybridTrackers[trackerIdx].entryPrice - beDistance; + if(newSL >= currentSL) continue; // Only move SL down + } + // Apply BE + if(SafePositionModify(g_ea_position.Ticket(), newSL, g_ea_position.TakeProfit(), "Hybrid_Aggressive_BE")) + { + string tranche = isTP2 ? "TP2" : "TP3"; + Print("* HYBRID AGGRESSIVE MODE: ", tranche, " moved to BE+", + DoubleToString(EA_BE_Buffer_Pips, 1), "p | Trailing ACTIVE | Ticket #", + g_ea_position.Ticket()); + } + } + // Mark trailing as active + g_hybridTrackers[trackerIdx].trailingActive = true; +} +//+------------------------------------------------------------------+ +//| * HELPER: Update Trailing SL | +//+------------------------------------------------------------------+ +void UpdateTrailingSL(int trackerIdx) +{ + // Get ATR for trailing distance + double atr = g_cachedATR; + if(atr <= 0) atr = g_ea_symbol.Point() * 100; // Fallback + // Determine trailing distance based on regime + // * v9.03 FIX#10: TF-aware scaling -- M5 needs tighter trail, H4 needs wider + double tfScale = 1.0; + if(AutoOpt_Enabled && g_autoOptInitialized) + { + switch(g_marketSnap.tf_category) + { + case TF_CAT_SCALP: tfScale = 0.7; break; // M5: tight trail + case TF_CAT_INTRADAY: tfScale = 0.9; break; // M15: slightly tighter + case TF_CAT_INTRASWING: tfScale = 1.05; break; // H1: between M15 and H4 + case TF_CAT_SWING: tfScale = 1.2; break; // H4: wider + case TF_CAT_POSITION: tfScale = 1.5; break; // D1: much wider + } + } + double trailDist = EA_Hybrid_TrailDist_Trend * atr * tfScale; + if(g_regimeData.regime == REGIME_BREAKOUT) + trailDist = EA_Hybrid_TrailDist_Breakout * atr * tfScale; + // Update SL for TP2 and TP3 positions + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + if(!g_ea_position.SelectByIndex(i)) continue; + if(g_ea_position.Symbol() != _Symbol) continue; + if(g_ea_position.Magic() != EA_MagicNumber) continue; + string posComment = g_ea_position.Comment(); + bool isTP2 = (StringFind(posComment, g_hybridTrackers[trackerIdx].baseComment + "_TP2") >= 0); + bool isTP3 = (StringFind(posComment, g_hybridTrackers[trackerIdx].baseComment + "_TP3") >= 0); + if(!isTP2 && !isTP3) continue; + // Get current price + double currentPrice = g_ea_position.Type() == POSITION_TYPE_BUY ? + SymbolInfoDouble(_Symbol, SYMBOL_BID) : + SymbolInfoDouble(_Symbol, SYMBOL_ASK); + double currentSL = g_ea_position.StopLoss(); + double newSL = 0; + if(g_ea_position.Type() == POSITION_TYPE_BUY) + { + newSL = currentPrice - trailDist; + // Only trail up, and not beyond current price + if(newSL <= currentSL || newSL >= currentPrice) continue; + } + else + { + newSL = currentPrice + trailDist; + // Only trail down, and not beyond current price + if(newSL >= currentSL || newSL <= currentPrice) continue; + } + // Apply trailing SL + if(SafePositionModify(g_ea_position.Ticket(), newSL, g_ea_position.TakeProfit(), "Hybrid_Trail")) + { + double lockPips = MathAbs(newSL - g_hybridTrackers[trackerIdx].entryPrice) / g_pipValue; + string tranche = isTP2 ? "TP2" : "TP3"; + Print("* HYBRID TRAILING: ", tranche, " SL updated | New SL=", + DoubleToString(newSL, _Digits), + " | Locked=+", DoubleToString(lockPips, 1), "p", + " | Trail Dist=", DoubleToString(trailDist / g_pipValue, 1), "p", + " | Ticket #", g_ea_position.Ticket()); + } + } +} +//+------------------------------------------------------------------+ +//| Helper Functions | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| * v6.39 IMPROVEMENT #2: SAFE POSITION MODIFY WITH VALIDATION | +//+------------------------------------------------------------------+ +struct ModifyErrorStats { + int totalAttempts; + int totalFailures; + int consecutiveFailures; + int lastErrorCode; + datetime lastErrorTime; +}; +ModifyErrorStats g_modifyStats; +string GetTradeErrorDescription(int error) +{ + switch(error) + { + case 10004: return "Requote"; + case 10006: return "Request rejected"; + case 10007: return "Request canceled"; + case 10010: return "Only part executed"; + case 10011: return "Request processing error"; + case 10012: return "Request canceled by timeout"; + case 10013: return "Invalid request"; + case 10014: return "Invalid volume"; + case 10015: return "Invalid price"; + case 10016: return "Invalid stops"; + case 10017: return "Trade disabled"; + case 10018: return "Market closed"; + case 10019: return "Not enough money"; + case 10020: return "Prices changed"; + case 10021: return "No quotes"; + case 10024: return "Too many requests"; + case 10025: return "No changes in request"; + case 10026: return "Autotrading disabled"; + case 10029: return "Order/position frozen"; + case 10030: return "Invalid fill type"; + case 10031: return "No connection"; + default: return "Error " + IntegerToString(error); + } +} +//+------------------------------------------------------------------+ +//| ValidateSLTP - ΔΙΟΡΘΩΜΕΝΗ ΕΚΔΟΣΗ | +//| [OK] FIX #1: Correct SELL SL validation logic | +//| Location: Replace around line 35771 | +//+------------------------------------------------------------------+ +bool ValidateSLTP(ulong ticket, double sl, double tp) +{ + if(!g_ea_position.SelectByTicket(ticket)) + return false; + // * v9.31 FIX#115A: SL==TP always invalid — silent return, no log spam + if(sl > 0 && tp > 0 && MathAbs(sl - tp) < _Point * 2) + return false; + // * v9.37 FIX#148: Rate-limit [WARN] prints inside ValidateSLTP. + // BUG: Called every tick x N positions → hundreds of [WARN] lines per trade. + // FIX: Static timer per ticket — same ticket logs max once per 5 seconds. + static ulong _vsl_last_ticket = 0; + static datetime _vsl_last_time = 0; + bool _vsl_can_log = (ticket != _vsl_last_ticket || (TimeCurrent() - _vsl_last_time) >= 5); + if(_vsl_can_log) { _vsl_last_ticket = ticket; _vsl_last_time = TimeCurrent(); } + double stopLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * _Point; + if(stopLevel < 1 * _Point) stopLevel = 1 * _Point; // minimum 1 point + double current_bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + double current_ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + if(g_ea_position.Type() == POSITION_TYPE_BUY) + { + // ================================================================ + // BUY POSITION VALIDATION + // ================================================================ + // SL validation for BUY: SL must be BELOW current price + if(sl > 0) + { + double sl_gap = current_bid - sl; // Gap between current price and SL + if(sl_gap < stopLevel) + { + if(_vsl_can_log) + Print("[WARN] ValidateSLTP: BUY SL too close | SL=", DoubleToString(sl, g_digits), + " | Bid=", DoubleToString(current_bid, g_digits), + " | Gap=", DoubleToString(sl_gap / _Point, 1), "pts", + " | MinStop=", DoubleToString(stopLevel / _Point, 1), "pts"); + return false; + } + } + // TP validation for BUY: TP must be ABOVE current price + if(tp > 0) + { + double tp_gap = tp - current_ask; // Gap between TP and current price + if(tp_gap < stopLevel) + { + if(_vsl_can_log) + Print("[WARN] ValidateSLTP: BUY TP too close | TP=", DoubleToString(tp, g_digits), + " | Ask=", DoubleToString(current_ask, g_digits), + " | Gap=", DoubleToString(tp_gap / _Point, 1), "pts", + " | MinStop=", DoubleToString(stopLevel / _Point, 1), "pts"); + return false; + } + } + } + else // POSITION_TYPE_SELL + { + // ================================================================ + // SELL POSITION VALIDATION + // ================================================================ + // [OK] FIX #1: ΔΙΟΡΘΩΣΗ - Correct SL validation for SELL positions + // SL validation for SELL: SL must be ABOVE current price + if(sl > 0) + { + // [OK] ΠΡΙΝ (ΛΑΘΟΣ): + // if(sl - current_ask < stopLevel) // [X] Wrong logic + // [OK] ΤΩΡΑ (ΣΩΣΤΟ): + double sl_gap = sl - current_ask; // Gap between SL and current price + if(sl_gap < stopLevel) + { + if(_vsl_can_log) + Print("[WARN] ValidateSLTP: SELL SL too close | SL=", DoubleToString(sl, g_digits), + " | Ask=", DoubleToString(current_ask, g_digits), + " | Gap=", DoubleToString(sl_gap / _Point, 1), "pts", + " | MinStop=", DoubleToString(stopLevel / _Point, 1), "pts"); + return false; + } + } + // TP validation for SELL: TP must be BELOW current price + if(tp > 0) + { + double tp_gap = current_bid - tp; // Gap between current price and TP + if(tp_gap < stopLevel) + { + if(_vsl_can_log) + Print("[WARN] ValidateSLTP: SELL TP too close | TP=", DoubleToString(tp, g_digits), + " | Bid=", DoubleToString(current_bid, g_digits), + " | Gap=", DoubleToString(tp_gap / _Point, 1), "pts", + " | MinStop=", DoubleToString(stopLevel / _Point, 1), "pts"); + return false; + } + } + } + return true; +} +bool SafePositionModify(ulong ticket, double sl, double tp, string context) +{ + g_modifyStats.totalAttempts++; + // Normalize prices + sl = NormalizeDouble(sl, g_digits); + tp = NormalizeDouble(tp, g_digits); + // --- FIX#195 (v9.48): FREEZE LEVEL guard in SafePositionModify. + // Error 10016 fires when the new SL is within the broker's freeze zone + // (typically = spread + 1-2p). In backtest tick-by-tick mode this causes + // hundreds of MODIFY FAILED per trade, slowing the test 6+ hours. + // Fix: check SYMBOL_TRADE_FREEZE_LEVEL before attempting modify. + // Use 1.5x freeze as min step to absorb spread fluctuation. --- + if(sl > 0 && g_ea_position.SelectByTicket(ticket)) + { + long freezeLvl = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL); + if(freezeLvl > 0) + { + double freezeDist = freezeLvl * _Point * 1.5; // 1.5x buffer + double curPrice = (g_ea_position.PositionType() == POSITION_TYPE_BUY) + ? SymbolInfoDouble(_Symbol, SYMBOL_BID) + : SymbolInfoDouble(_Symbol, SYMBOL_ASK); + if(MathAbs(sl - curPrice) < freezeDist) + { + if(g_verboseLog) + PrintFormat("* FIX#195 FREEZE SKIP [%s]: SL=%.5f dist=%.1fp < freeze*1.5=%.1fp", + context, sl, + MathAbs(sl - curPrice) / _Point, + freezeDist / _Point); + return false; // silent skip — no 10016 + } + } + // Also skip if new SL is no better than current SL (trail going backwards) + double curSL = g_ea_position.StopLoss(); + if(curSL > 0) + { + bool isBuyPos = (g_ea_position.PositionType() == POSITION_TYPE_BUY); + bool noImprovement = isBuyPos ? (sl <= curSL) : (sl >= curSL); + if(noImprovement) + return false; // silent skip — trail would move SL in wrong direction + } + } + // Validate before sending + if(!ValidateSLTP(ticket, sl, tp)) + { + // * v9.31 FIX#115B: Rate-limit log -- max 1 per 60s per context + // Prevents 90,000+ identical log lines when SL==TP in trail loop + static datetime s_lastBlockedLog = 0; + static string s_lastBlockedCtx = ""; + datetime _now = TimeCurrent(); + if(_now - s_lastBlockedLog > 60 || s_lastBlockedCtx != context) + { + s_lastBlockedLog = _now; + s_lastBlockedCtx = context; + Print("[WARN] SafePositionModify BLOCKED | ", context, + " | SL=", DoubleToString(sl, g_digits), + " | TP=", DoubleToString(tp, g_digits), + " | Ticket #", ticket); + } + return false; + } + // Attempt modify + if(g_ea_trade.PositionModify(ticket, sl, tp)) + { + g_modifyStats.consecutiveFailures = 0; + return true; + } + // Handle failure + int err = (int)g_ea_trade.ResultRetcode(); + if(err == 0) err = GetLastError(); + g_modifyStats.totalFailures++; + g_modifyStats.consecutiveFailures++; + g_modifyStats.lastErrorCode = err; + g_modifyStats.lastErrorTime = TimeCurrent(); + Print("[WARN] MODIFY FAILED | ", context, + " | Error: ", err, " (", GetTradeErrorDescription(err), ")", + " | Ticket #", ticket, + " | SL=", DoubleToString(sl, g_digits), + " | TP=", DoubleToString(tp, g_digits), + " | Consecutive fails: ", g_modifyStats.consecutiveFailures); + // Circuit breaker: alert on repeated failures + if(g_modifyStats.consecutiveFailures >= 10) + { + Alert("[STOP] 10 consecutive modify failures! Check connection/broker settings."); + g_modifyStats.consecutiveFailures = 0; // Reset to avoid spamming + } + return false; +} +bool EA_IsInKillzone() +{ + for(int i = 0; i < ArraySize(g_activeKillzones); i++) + { + if(g_activeKillzones[i].isActive) return true; + } + return false; +} +bool EA_IsTrendAligned() +{ + if(!EnableHTFConfirmation) return true; + ENUM_MTF_DIRECTION mtf = GetMTFDirection(); + if(g_ea_signal.isBullish) + return (mtf == MTF_BULLISH || mtf == MTF_STRONG_BULLISH); + else + return (mtf == MTF_BEARISH || mtf == MTF_STRONG_BEARISH); +} +#endif // COMPILE_AS_EA - EA functions +//+------------------------------------------------------------------+ +//| END OF EA FUNCTIONS | +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ \ No newline at end of file diff --git a/33-STAR-EA-ICT/STAR-EA-ICT.pdf b/33-STAR-EA-ICT/STAR-EA-ICT.pdf new file mode 100644 index 0000000..17754b0 Binary files /dev/null and b/33-STAR-EA-ICT/STAR-EA-ICT.pdf differ diff --git a/34-EA31337/EA31337.mq4 b/34-EA31337/EA31337.mq4 new file mode 100644 index 0000000..2e0500b --- /dev/null +++ b/34-EA31337/EA31337.mq4 @@ -0,0 +1,28 @@ +//+------------------------------------------------------------------+ +//| EA31337 - multi-strategy advanced trading robot. | +//| Copyright 2016-2023, EA31337 Ltd | +//| https://ea31337.github.io/ | +//+------------------------------------------------------------------+ + +// Main code. +#include "EA31337.mq5" + +// EA indicator resources. +#ifdef __resource__ +// Indicator resources. +// #resource INDI_ATR_MA_TREND_PATH + MQL_EXT // @todo: Not supported in MT4. +#resource INDI_EWO_OSC_PATH + MQL_EXT +#resource INDI_SVEBB_PATH + MQL_EXT +#resource INDI_TMA_CG_PATH + MQL_EXT +#resource INDI_TMA_TRUE_PATH + MQL_EXT +#resource INDI_SAWA_PATH + MQL_EXT +// #resource INDI_SUPERTREND_PATH + MQL_EXT // @todo: Not supported in MT4. +// Strategy resources (MQL4 workaround). +string MetaNewsData2018 = ""; +string MetaNewsData2019 = ""; +string MetaNewsData2020 = ""; +string MetaNewsData2021 = ""; +string MetaNewsData2022 = ""; +string MetaNewsData2023 = ""; +string MetaNewsData2024 = ""; +#endif diff --git a/34-EA31337/EA31337.pdf b/34-EA31337/EA31337.pdf new file mode 100644 index 0000000..804dfb7 Binary files /dev/null and b/34-EA31337/EA31337.pdf differ diff --git a/35-ICT-Imbalance/ICT-Imbalance.mq5 b/35-ICT-Imbalance/ICT-Imbalance.mq5 new file mode 100644 index 0000000..248d5b8 --- /dev/null +++ b/35-ICT-Imbalance/ICT-Imbalance.mq5 @@ -0,0 +1,373 @@ +#property version "1.20"; +#include ; +#include ; +CPositionInfo position; +CTrade ctrade; + +#define HR0800 28800 +#define HR0830 30600 +#define HR1300 46800 +#define HR1330 48600 +#define HR1900 68400 +#define HR2400 86400 +#define SECONDS uint + +// settings +input string RISK_MANAGEMENT_SETTINGS; +input double lot_size; +input int bars_look_back; +input bool one_trade_per_session; + +// global variables +ulong orderTicket = OrderGetTicket(1); + +double shortFirstBarLow; +double shortFirstBarHigh; +double shortThirdBarHigh; +double shortThirdBarLow; + +double longFirstBarLow; +double longFirstBarHigh; +double longThirdBarHigh; +double longThirdBarLow; + +bool isShortEntry = false; +bool isLongEntry = false; +bool londonReset = false; +bool newYorkReset = false; +bool longReEntry = false; +bool shortReEntry = false; +int longImbalanceStorage; +int shortImbalanceStorage; +int longOrShort = 3; + +datetime shortBeginningCandleTime; +datetime longBeginningCandleTime; + +// returns the time in GMT in seconds +SECONDS time(datetime when = 0) { + return SECONDS(when == 0 ? TimeCurrent() : when) % HR2400; +} +datetime date(datetime when = 0) { + return datetime((when == 0 ? TimeCurrent() : when) - time(when)); +} +bool isValidTime(SECONDS start, SECONDS end, datetime when = 0) { + SECONDS now = time(when); + return start < end ? start <= now && now < end : !isValidTime(end, start, when); +} + +// returns true if it finds a short imbalance +bool isImbalanceShort(bool sa = false) { + // set the chart to 1 min to get precise entries + ChartSetSymbolPeriod(0, NULL, PERIOD_M1); + + // store a reference to the first bar + shortBeginningCandleTime = TimeCurrent(); + + // store the high of the first bar + shortFirstBarLow = iLow(NULL, 0, 1); + shortFirstBarHigh = iHigh(NULL, 0, 1); + + // store the low of the third bar + shortThirdBarHigh = iHigh(NULL, 0, 3); + + return shortFirstBarLow > shortThirdBarHigh ? sa = true : sa = false; +} + +// returns true if it finds a long imbalance +bool isImbalanceLong(bool la = false) { + // set the chart to 1 min to get precise entries + ChartSetSymbolPeriod(0, NULL, PERIOD_M1); + + // store a reference to the first candle + longBeginningCandleTime = TimeCurrent(); + + // store the high of the first bar + longFirstBarHigh = iHigh(NULL, 0, 1); + + // store the low of the third bar + longThirdBarLow = iLow(NULL, 0, 3); + + return longFirstBarHigh < longThirdBarLow ? la = true : la = false; +} + +// returns true or false if the short imbalance has been entered +bool isImbalanaceShortEntered(bool sb = false) { + // set the chart to 1 min to get precise entries + ChartSetSymbolPeriod(0, NULL, PERIOD_M1); + return iClose(NULL, 0, 1) > shortThirdBarLow && iClose(NULL, 0, 3) < shortFirstBarHigh ? sb = true : sb = false; +} + +// returns true or false is the long imbalance has been entered +bool isImbalanceLongEntered(bool lb = false) { + // set the chart to 1 min to get precise entries + ChartSetSymbolPeriod(0, NULL, PERIOD_M1); + return iClose(NULL, 0, 1) < longThirdBarLow && iClose(NULL, 0, 3) > longFirstBarHigh ? lb = true : lb = false; +} + +// returns true if there is a long imbalance (for use in a for loop) +bool initialLongImbalance(bool ik = false, int firstInt = 1, int secondInt = 1) { + return iHigh(NULL, 0, firstInt) < iLow(NULL, 0, secondInt) ? ik = true : ik = false; +} + +// returns true if there is a short imbalance (for use in a for loop) +bool initialShortImbalance(bool on = false, int firstInt = 1, int secondInt = 1) { + return iLow(NULL, 0, firstInt) > iHigh(NULL, 0, secondInt) ? on = true : on = false; +} + +// as there are no imbalances, it checks the close of the first candle of the more recently closed session to the first of the new one +bool alternateStrategyShort(bool ass = false) { + return iClose(NULL, 0, 12) > iClose(NULL, 0, 1) ? ass = true : ass = false; +} + +// as there are no imbalances, it check the close of the first candle of the more recently closed session to the first of the new one +bool alternateStrategyLong(bool asl = false) { + return iClose(NULL, 0, 12) < iClose(NULL, 0, 1) ? asl = true : asl = false; +} + +// creates a stop order +void stopOrderAction(int x, double tbl, double fbl, double tbh, double fbh) +{ + // place a limit order at the first bar's low + MqlTradeRequest stopOrderRequest; + MqlTradeResult stopOrderResult; + + stopOrderRequest.symbol = Symbol(); + stopOrderRequest.order = orderTicket; + stopOrderRequest.volume = lot_size; + stopOrderRequest.deviation = 2; + stopOrderRequest.action = TRADE_ACTION_PENDING; + stopOrderRequest.type_filling = ENUM_ORDER_TYPE_FILLING::ORDER_FILLING_FOK; + + if(x == 1) { + stopOrderRequest.price = tbl; + stopOrderRequest.type = ORDER_TYPE_BUY_STOP; + stopOrderRequest.sl = fbl + 5 * Point(); + } + if(x == 2) { + stopOrderRequest.price = tbh; + stopOrderRequest.type = ORDER_TYPE_SELL_STOP; + stopOrderRequest.sl = fbh + 5 * Point(); + } + // send the order with the inputs above + OrderSend(stopOrderRequest, stopOrderResult); +} + +// makes order ticket for use in cancelling the stop order if it doesn't get triggered +void removeStopOrder() +{ + MqlTradeRequest removeOrderRequest; + MqlTradeResult removeOrderResult; + + removeOrderRequest.action = TRADE_ACTION_REMOVE; + removeOrderRequest.order = orderTicket; + + OrderSend(removeOrderRequest, removeOrderResult); +} + +// this function closes the current position +void closePosition() +{ + if(position.Symbol()==Symbol()) { + ctrade.PositionClose(position.Ticket()); + } +} + +// this function finds the entry for both New York and London trades +void entry() +{ + // go through each candle and see if there is an imbalance + for(int i = 12; i > 3; i--) { + int k = i - 2; + if(initialLongImbalance(false, i, k) == true) { + longImbalanceStorage = longImbalanceStorage + 1; + } else if(initialShortImbalance(false, i, k) == true) { + shortImbalanceStorage = shortImbalanceStorage + 1; + } + } + + // check to make sure there are imbalances, and if not do a different calculation for the same bias thing + if((longImbalanceStorage == shortImbalanceStorage) || (longImbalanceStorage == 0 && shortImbalanceStorage == 0)) { + // do the check the first candle of the end of the last session to the one before the session now + if(alternateStrategyLong() == true) { + longImbalanceStorage = longImbalanceStorage + 1; + } else if (alternateStrategyShort() == true) { + shortImbalanceStorage = shortImbalanceStorage + 1; + } + } + + // see if more short imbalances have been found and then find an entry in the opposite direction + if(longImbalanceStorage < shortImbalanceStorage) { + ChartSetSymbolPeriod(0, NULL, PERIOD_M5); + while(isValidTime(HR0800, HR0830) && isLongEntry == false) { + if(isImbalanceLong() == true && isImbalanceLongEntered() == true) { + printf("got an entry"); + longOrShort = 1; + stopOrderAction(longOrShort, longThirdBarLow, longFirstBarLow, longThirdBarHigh, longFirstBarHigh); + isLongEntry = true; + } + } + } else if(longImbalanceStorage > shortImbalanceStorage) { + ChartSetSymbolPeriod(0, NULL, PERIOD_M5); + // see if more long imbalances have been found and then find an entry in the opposite direction + while(isValidTime(HR0800, HR0830) && isShortEntry == false) { + if(isImbalanceShort() == true && isImbalanaceShortEntered() == true) { + printf("got an entry"); + longOrShort = 2; + stopOrderAction(longOrShort, shortThirdBarLow, shortFirstBarLow, shortThirdBarHigh, shortFirstBarHigh); + isShortEntry = true; + } + } + } + printf("reset variables"); + // reset these variables + shortImbalanceStorage = 0; + longImbalanceStorage = 0; + londonReset = true; + newYorkReset = true; +} + + +void reEntryLong() +{ + printf("if long entry"); + // remove entry if price doesn't enter it within 6 bars + if(iBarShift(Symbol(), 0, longBeginningCandleTime, true) == 6) { + printf("cancelled order after time"); + removeStopOrder(); + isLongEntry = false; + } + if(iLow(NULL, 0, 1) > iHigh(NULL, 0, 3)) { + printf("went opposite direction"); + closePosition(); + if(one_trade_per_session == false) { + if(isImbalanceShort() == true && isImbalanaceShortEntered() == true) { + longBeginningCandleTime = TimeCurrent(); + longOrShort = 2; + stopOrderAction(longOrShort, shortThirdBarLow, shortFirstBarLow, shortThirdBarHigh, shortFirstBarLow); + longReEntry = true; + } + } else { + isLongEntry = false; + } + } +} + +void reEntryShort() +{ + printf("if short entry"); + if(iBarShift(Symbol(), 0, shortBeginningCandleTime, true) == 6) { + removeStopOrder(); + isShortEntry = false; + } + // finds an imbalance in the opposing direction + if(iHigh(NULL, 0, 1) < iLow(NULL, 0, 3)) { + closePosition(); + if(one_trade_per_session == false) { + if(isImbalanceLong() == true && isImbalanceLongEntered() == true) { + shortBeginningCandleTime = TimeCurrent(); + longOrShort = 1; + stopOrderAction(longOrShort, shortThirdBarLow, shortFirstBarLow, shortThirdBarHigh, shortFirstBarLow); + shortReEntry = true; + } + } else { + isShortEntry = false; + } + } +} + +void OnInit() +{ + // sets the timeframe to the 5 minute + ChartSetSymbolPeriod(0, NULL, PERIOD_M5); +} + +void OnTick() +{ + // when it approcahes london session, it can start to find an entry + if(isValidTime(HR0800, HR0830) && londonReset == false) { + closePosition(); + entry(); + newYorkReset = false; + } + + // when it approaches new york session, it can start to find an entry + if(isValidTime(HR1300, HR1330) && newYorkReset == false) { + printf("in new york session"); + closePosition(); + entry(); + londonReset = false; + } + + // finds when a long trade has been opened in the London session and it manages stop loss and what to do if prices reverse + if(isLongEntry == true && isValidTime(HR0800, HR1300)) { + reEntryLong(); + if(longReEntry == true) { + if(iBarShift(Symbol(), 0, longBeginningCandleTime, true) == 6) { + removeStopOrder(); + isLongEntry = false; + } + } + ChartSetSymbolPeriod(0, NULL, PERIOD_M5); + // finds an imbalance in the opposing direction + if(iLow(NULL, 0, 1) > iHigh(NULL, 0, 3)) { + closePosition(); + longReEntry = false; + } + } + + // finds when a long trade has been opened in the New York session and it manages stop loss and what to do if prices reverse + if(isLongEntry == true && isValidTime(HR1300, HR1900)) { + reEntryLong(); + if(longReEntry == true) { + if(iBarShift(Symbol(), 0, longBeginningCandleTime, true) == 6) { + removeStopOrder(); + isLongEntry = false; + } + } + ChartSetSymbolPeriod(0, NULL, PERIOD_M5); + // finds an imbalance in the opposing direction + if(iLow(NULL, 0, 1) > iHigh(NULL, 0, 3)) { + closePosition(); + longReEntry = false; + } + } + + // finds when a short trade in London session has been opened and it manages stop loss and what to do if prices reverse + if(isShortEntry == true && isValidTime(HR0800, HR1300)) { + reEntryShort(); + if(shortReEntry == true) { + if(iBarShift(Symbol(), 0, shortBeginningCandleTime, true) == 6) { + removeStopOrder(); + isShortEntry = false; + } + } + // finds an imbalance in the opposing direction + if(iHigh(NULL, 0, 1) < iLow(NULL, 0, 3)) { + closePosition(); + shortReEntry = false; + } + } + // finds when a short trade in New York session has been opened and it manages stop loss and what to do if prices reverse + if(isShortEntry == true && isValidTime(HR1300, HR1900)) { + reEntryShort(); + if(shortReEntry == true) { + if(iBarShift(Symbol(), 0, shortBeginningCandleTime, true) == 6) { + removeStopOrder(); + isShortEntry = false; + } + } + // finds an imbalance in the opposing direction + if(iHigh(NULL, 0, 1) < iLow(NULL, 0, 3)) { + closePosition(); + shortReEntry = false; + } + } +} + +// remove stop order and any open postions just in case +void OnDeinit(const int reason) +{ + removeStopOrder(); + closePosition(); +} diff --git a/35-ICT-Imbalance/ICT-Imbalance.pdf b/35-ICT-Imbalance/ICT-Imbalance.pdf new file mode 100644 index 0000000..ee7e597 Binary files /dev/null and b/35-ICT-Imbalance/ICT-Imbalance.pdf differ diff --git a/36-OpenEA/OpenEA.mq5 b/36-OpenEA/OpenEA.mq5 new file mode 100644 index 0000000..c2d8f36 --- /dev/null +++ b/36-OpenEA/OpenEA.mq5 @@ -0,0 +1,769 @@ +//+------------------------------------------------------------------+ +//| FxChartAI OpenEA | +//| Copyright 2025, FxChartAI | +//| https://www.fxchartai.com | +//+------------------------------------------------------------------+ +#property copyright "Copyright 2025, FxChartAI" +#property link "https://www.fxchartai.com" +#property version "1.1.0" + +#include +#include // JSON parser library per https://www.mql5.com/en/articles/14108 + +//--- Input Parameters +input double LotSize = 1; // Risk management: Position size +input int StopLossPips = 400; // Stop loss in pips +input int TakeProfitPips = 500; // Take profit in pips +input int ConfidenceLevel = 5; // Minimum confidence level required (1-5) +input int MaxDataSize = 7; // Maximum dataset size for analysis +input int OperationMode = 0; // 0 = Test (CSV), 1 = Live (API) +input int MaxRetryAttempts = 9; // Maximum data loading retry attempts +input int MagicNumber = 12345; +input int TrailingPips = 50; + +#define RETRY_DELAY_MS 60000 // 1 minute delay between retries + +//--- Constants +enum SIGNAL_POSITION { SIGNAL_SELL, SIGNAL_BUY, SIGNAL_NONE }; +enum TREND_WEIGHT { TREND_HIGH, TREND_LOW, TREND_NONE }; + +//--- Global variables +string m10FileName = "signal_dataset_" + _Symbol + "_m10.csv"; +string h1FileName = "signal_dataset_" + _Symbol + "_h1.csv"; +datetime lastM10UpdateTime = 0; +datetime lastH1UpdateTime = 0; +int pendingOrderTicket = -1; + +//--- SignalData structure +struct SignalData + { + datetime time; + SIGNAL_POSITION position; + TREND_WEIGHT weight; + }; + +//--- Global arrays for signal data (declared externally, e.g., in a header) +SignalData m10Data[]; +int m10DataIndex = 0; +SignalData h1Data[]; +int h1DataIndex = 0; + +//+------------------------------------------------------------------+ +//| Expert initialization function | +//+------------------------------------------------------------------+ +int OnInit() + { + if(OperationMode != 0 && OperationMode != 1) + { Print("Invalid OperationMode value"); return(INIT_FAILED); } + if(MaxRetryAttempts < 1 || MaxRetryAttempts > _Period) + { Print("Invalid MaxRetryAttempts value"); return(INIT_FAILED); } + if(MaxDataSize < 1) + { Print("Invalid MaxDataSize value"); return(INIT_FAILED); } + if(ConfidenceLevel < 1) + { Print("Invalid ConfidenceLevel value"); return(INIT_FAILED); } + if(TakeProfitPips < 1) + { Print("Invalid TakeProfitPips value"); return(INIT_FAILED); } + if(StopLossPips < 1) + { Print("Invalid StopLossPips value"); return(INIT_FAILED); } + if(LotSize < 0) + { Print("Invalid LotSize value"); return(INIT_FAILED); } + return(INIT_SUCCEEDED); + } + +//+------------------------------------------------------------------+ +//| Expert tick function | +//+------------------------------------------------------------------+ +void OnTick() + { + static datetime prevM10Bar = 0; + static datetime prevH1Bar = 0; + + if(_Period == PERIOD_M10) + { + datetime currentM10Bar = iTime(_Symbol, PERIOD_M10, 1); + if(currentM10Bar != prevM10Bar) + { + prevM10Bar = currentM10Bar; + ProcessTimeframe(PERIOD_M10, m10FileName, m10Data, m10DataIndex, lastM10UpdateTime); + } + } + else + if(_Period == PERIOD_H1) + { + datetime currentH1Bar = iTime(_Symbol, PERIOD_H1, 1); + if(currentH1Bar != prevH1Bar) + { + prevH1Bar = currentH1Bar; + ProcessTimeframe(PERIOD_H1, h1FileName, h1Data, h1DataIndex, lastH1UpdateTime); + } + } + } + +//+------------------------------------------------------------------+ +//| Process timeframe data | +//+------------------------------------------------------------------+ +void ProcessTimeframe(ENUM_TIMEFRAMES tf, string filename, SignalData &data[], int &dataIndex, datetime &lastUpdate) + { + datetime currentTime = iTime(_Symbol, tf, 1); + bool result = false; + + for(int attempt = 0; attempt < MaxRetryAttempts; attempt++) + { + if(OperationMode == 0 && LoadCSVData(filename, data, dataIndex, lastUpdate, currentTime)) + { + result = true; + break; + } + else + if(OperationMode == 1 && LoadAPIRequest(data, dataIndex, lastUpdate, currentTime, tf)) + { + result = true; + break; + } + + if(attempt < MaxRetryAttempts - 1) + { + Print("Load ", (OperationMode == 0 ? "test" : "live"), " data failed, retrying in 1 minute... Attempt ", attempt + 1, "/", MaxRetryAttempts); + Sleep(RETRY_DELAY_MS); + } + } + + if(result) + { + lastUpdate = currentTime; + AnalyzeAndTrade(tf, data); + ManageOpenOrders(tf); + } + else + { + Print("Failed to load ", (OperationMode == 0 ? "test" : "live"), " data after ", MaxRetryAttempts, " attempts"); + } + } + +//+------------------------------------------------------------------+ +//| Load CSV data using circular buffer | +//+------------------------------------------------------------------+ +bool LoadCSVData(string filePath, SignalData &data[], int &index, datetime &lastUpdate, datetime currentTime) + { + + int handle = FileOpen(filePath, FILE_READ|FILE_CSV|FILE_ANSI, '\n'); + if(handle == INVALID_HANDLE) + { + Print("Unable to load file"); + return false; + } + Print("Reading file"); + + bool updated = false; + while(!FileIsEnding(handle)) + { + string line = FileReadString(handle); + StringReplace(line, "\r", ""); + string parts[]; + + if(StringSplit(line, ',', parts) == 3) + { + datetime dt = StringToTime(parts[0]); + if(dt > lastUpdate && dt <= currentTime) + { + Print("Data found for ", currentTime); + SignalData newData; + newData.time = dt; + newData.position = (SIGNAL_POSITION)StringToInteger(parts[1]); + newData.weight = (TREND_WEIGHT)StringToInteger(parts[2]); + + // Update circular buffer + int size = ArraySize(data); + if(size < MaxDataSize) + ArrayResize(data, size + 1); + for(int x = size - 1; x > 0; x--) + data[x] = data[x - 1]; + data[0] = newData; + + updated = true; + } + } + } + Print("Done read csv"); + + FileClose(handle); + return updated; + } + +//+------------------------------------------------------------------+ +//| Function: LoadAPIRequest | +//| Description: Calls FxChartAI API via GET and parses the JSON | +//| response into an array of SignalData. | +//+------------------------------------------------------------------+ +bool LoadAPIRequest(SignalData &data[], int &index, datetime &lastUpdate, datetime currentTime, ENUM_TIMEFRAMES timeframe) + { +// Convert timeframe to string representation + string tfString; + switch(timeframe) + { + case PERIOD_M10: + tfString = "M10"; + break; + case PERIOD_H1: + tfString = "H1"; + break; + default: + tfString = "M10"; + break; + } + +// Construct API URL + string url = BuildAPIRequestURL(timeframe, currentTime); + +// Send HTTP GET request + uchar result[]; + string headers; + string requestMethod = "GET"; + int timeout = 5000; + string resulthHeaders; + char postData[]; // GET request uses empty POST data + int response = WebRequest(requestMethod,url, headers, timeout, postData, result, resulthHeaders); + Print(CharArrayToString(result)); + + if(response != 200) + { + Print("API request failed with error: ", GetLastError()); + return false; + } + +// Parse JSON response + CJAVal parser; + string jsonStr = CharArrayToString(result); + + if(!parser.Deserialize(jsonStr)) + { + Print("Failed to parse JSON response"); + return false; + } + + if(parser.m_type != jtARRAY) + { + Print("Invalid JSON structure received"); + return false; + } + + bool dataUpdated = false; + +// Process array in reverse chronological order + for(int i = parser.Size() - 1; i >= 0; i--) + { + CJAVal *item = parser[i]; + + // Parse trade date + string dateStr = item["tradedate"].ToStr(); + StringReplace(dateStr, "-", "."); + Print(dateStr); + datetime tradeDate = StringToTime(dateStr); + + if(tradeDate <= lastUpdate) + continue; + + // Create new signal data entry + SignalData newData; + newData.time = tradeDate; + newData.position = (SIGNAL_POSITION)item["position"].ToInt(); + newData.weight = (TREND_WEIGHT)item["weight"].ToInt(); + + // Update data array with new entry + int size = ArraySize(data); + if(size < MaxDataSize) + ArrayResize(data, size + 1); + + // Shift existing elements + for(int j = size - 1; j > 0; j--) + data[j] = data[j - 1]; + + data[0] = newData; + lastUpdate = tradeDate; + dataUpdated = true; + } + +// Maintain maximum data size + if(ArraySize(data) > MaxDataSize) + ArrayResize(data, MaxDataSize); + + if(ArraySize(data) > 0) + Print("Index0: "+data[0].time); + + return dataUpdated; + } + +//+------------------------------------------------------------------+ +//| Build API Request URL | +//+------------------------------------------------------------------+ +string BuildAPIRequestURL(ENUM_TIMEFRAMES tf, datetime time) + { + string timeframeStr = (tf == PERIOD_M10) ? "M10" : "H1"; + string formattedTime = TimeToString(time, TIME_DATE) + "T" + + TimeToString(time, TIME_MINUTES); + + return StringFormat( + "https://chartapi.fxchartai.com/easignal?currencypair=%s&size=%d&tradedate=%s&timeframe=%s", + _Symbol, MaxDataSize, formattedTime, timeframeStr + ); + } +//+------------------------------------------------------------------+ +//| Trend confirmation check | +//+------------------------------------------------------------------+ +bool IsTrendConfirmed(const SignalData &data[], int requiredConsecutive, SIGNAL_POSITION &result) + { + int count = 0; + SIGNAL_POSITION lastSignal = SIGNAL_NONE; + + for(int i = 0; i < ArraySize(data); i++) + { + if(data[i].position == SIGNAL_NONE) + continue; + + if(data[i].position == lastSignal) + { + if(++count >= requiredConsecutive) + { + result = data[i].position; + return true; + } + } + else + { + count = 1; + lastSignal = data[i].position; + } + } + return false; + } + +//+------------------------------------------------------------------+ +//| Order management | +//+------------------------------------------------------------------+ +void DeletePendingOrders() + { + for(int i = OrdersTotal()-1; i >= 0; i--) + { + ulong ticket = OrderGetTicket(i); + if(ticket <= 0) + continue; + + if(OrderGetInteger(ORDER_MAGIC) == MagicNumber && + (OrderGetInteger(ORDER_TYPE) == ORDER_TYPE_BUY_STOP || + OrderGetInteger(ORDER_TYPE) == ORDER_TYPE_SELL_STOP)) + { + MqlTradeRequest req = {}; + MqlTradeResult res = {}; + req.action = TRADE_ACTION_REMOVE; + req.order = ticket; + OrderSend(req, res); + } + } + pendingOrderTicket = -1; + } + +//+------------------------------------------------------------------+ +//| Candle tail signal detection | +//+------------------------------------------------------------------+ +SIGNAL_POSITION GetCandleTailSignal(ENUM_TIMEFRAMES tf) + { + double open = iOpen(_Symbol, tf, 1); + double close = iClose(_Symbol, tf, 1); + double high = iHigh(_Symbol, tf, 1); + double low = iLow(_Symbol, tf, 1); + + if(close > open) // Bullish + { + double upperTail = high - close; + double lowerTail = open - low; + return (upperTail > lowerTail*2) ? SIGNAL_SELL : + (lowerTail > upperTail*2) ? SIGNAL_BUY : SIGNAL_NONE; + } + +// Bearish + double upperTail = high - open; + double lowerTail = close - low; + return (upperTail > lowerTail*2) ? SIGNAL_SELL : + (lowerTail > upperTail*2) ? SIGNAL_BUY : SIGNAL_NONE; + } + +//+------------------------------------------------------------------+ +//| Trendline check | +//+------------------------------------------------------------------+ +bool CheckTrendline(ENUM_TIMEFRAMES tf, bool bullish) + { + double price = bullish ? iLow(_Symbol, tf, 1) : iHigh(_Symbol, tf, 1); + datetime time = iTime(_Symbol, tf, 1); + + int touches = 0; + for(int i = 2; i <= 20; i++) + { + double testPrice = bullish ? iLow(_Symbol, tf, i) : iHigh(_Symbol, tf, i); + datetime testTime = iTime(_Symbol, tf, i); + + if((bullish && testPrice <= price) || (!bullish && testPrice >= price)) + { + if(++touches >= 2) + return true; + } + else + if(iTime(_Symbol, tf, i) < time) + break; + } + return false; + } + +//+------------------------------------------------------------------+ +//| Execute trade | +//+------------------------------------------------------------------+ +void ExecuteTrade(SIGNAL_POSITION signal, ENUM_TIMEFRAMES tf) + { + DeletePendingOrders(); + + double price = (signal == SIGNAL_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) + : SymbolInfoDouble(_Symbol, SYMBOL_BID); + double sl = StopLossPips * _Point * ((tf == PERIOD_H1) ? 10 : 1); + double tp = TakeProfitPips * _Point * ((tf == PERIOD_H1) ? 10 : 1); + + MqlTradeRequest req = {}; + MqlTradeResult res = {}; + req.action = TRADE_ACTION_PENDING; + req.symbol = _Symbol; + req.volume = LotSize; + req.type = (signal == SIGNAL_BUY) ? ORDER_TYPE_BUY_STOP : ORDER_TYPE_SELL_STOP; + req.price = price + ((signal == SIGNAL_BUY) ? 100*_Point : -100*_Point); + req.sl = (signal == SIGNAL_BUY) ? req.price - sl : req.price + sl; + req.tp = (signal == SIGNAL_BUY) ? req.price + tp : req.price - tp; + req.magic = MagicNumber; + + if(OrderSend(req, res)) + pendingOrderTicket = res.order; + } + +//+------------------------------------------------------------------+ +//| Main trading logic | +//+------------------------------------------------------------------+ +void AnalyzeAndTrade(ENUM_TIMEFRAMES tf, const SignalData &data[]) + { + if(PositionsTotal() > 0) + return; + + SIGNAL_POSITION trendSignal; + if(IsTrendConfirmed(data,ConfidenceLevel, trendSignal)) + { + SIGNAL_POSITION candleSignal = GetCandleTailSignal(tf); + if(candleSignal == trendSignal) + ExecuteTrade(trendSignal, tf); + } + } + + +//+------------------------------------------------------------------+ +//| Manage Open Positions | +//+------------------------------------------------------------------+ +void ManageOpenOrders(ENUM_TIMEFRAMES timeframe) + { +// Process market positions + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + ulong ticket = PositionGetTicket(i); + if(ticket > 0) + { + if(PositionGetInteger(POSITION_MAGIC) == MagicNumber) + { + string symbol = PositionGetString(POSITION_SYMBOL); + ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); + double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); + double sl = PositionGetDouble(POSITION_SL); + double tp = PositionGetDouble(POSITION_TP); + double volume = PositionGetDouble(POSITION_VOLUME); + double currentPrice = (posType == POSITION_TYPE_BUY) ? + SymbolInfoDouble(symbol, SYMBOL_BID) : + SymbolInfoDouble(symbol, SYMBOL_ASK); + + // Check for TP/SL hit + if((posType == POSITION_TYPE_BUY && currentPrice >= tp) || + (posType == POSITION_TYPE_SELL && currentPrice <= tp)) + { + ClosePosition(ticket); + } + else + if((posType == POSITION_TYPE_BUY && currentPrice <= sl) || + (posType == POSITION_TYPE_SELL && currentPrice >= sl)) + { + ClosePosition(ticket); + } + else + { + // Trailing stop logic + UpdateTrailingStop(ticket, posType, currentPrice, timeframe); + } + } + } + } + +// Process pending orders + for(int i = OrdersTotal() - 1; i >= 0; i--) + { + ulong orderTicket = OrderGetTicket(i); + if(orderTicket > 0 && OrderGetInteger(ORDER_MAGIC) == MagicNumber) + { + CheckPendingOrderExpiry(orderTicket, timeframe); + } + } + } + +//+------------------------------------------------------------------+ +//| Close position | +//+------------------------------------------------------------------+ +bool ClosePosition(ulong ticket) + { + MqlTradeRequest request = {}; + MqlTradeResult result = {}; + + request.action = TRADE_ACTION_DEAL; + request.position = ticket; + request.symbol = PositionGetString(POSITION_SYMBOL); + request.volume = PositionGetDouble(POSITION_VOLUME); + request.deviation = 5; + request.type = (ENUM_ORDER_TYPE)(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY; + request.price = (request.type == ORDER_TYPE_BUY) ? + SymbolInfoDouble(request.symbol, SYMBOL_ASK) : + SymbolInfoDouble(request.symbol, SYMBOL_BID); + + if(OrderSend(request, result)) + { + Print("Position closed: ", ticket); + return true; + } + else + { + Print("Error closing position: ", GetLastError()); + return false; + } + } + +//+------------------------------------------------------------------+ +//| Perforrm Minor Trailing stop | +//+------------------------------------------------------------------+ +bool performMinorTrail(ulong ticket, ENUM_POSITION_TYPE posType, double priceOpen, double currentSl, int trailingPips, ENUM_TIMEFRAMES timeframe) + { + MqlTradeRequest request = {}; + MqlTradeResult result = {}; + double newSl = 0.0; + if(posType == POSITION_TYPE_BUY) + { + newSl = priceOpen + trailingPips * _Point; + if(newSl > currentSl) + { + request.action = TRADE_ACTION_SLTP; + request.position = ticket; + request.symbol = PositionGetString(POSITION_SYMBOL); + request.sl = newSl; + request.tp = PositionGetDouble(POSITION_TP); + if(OrderSend(request, result)) + { + Print("Minor trailing stop updated for buy position"); + return true; + } + } + } + else + { + newSl = priceOpen - trailingPips * _Point; + if(newSl < currentSl || currentSl == 0) + { + request.action = TRADE_ACTION_SLTP; + request.position = ticket; + request.symbol = PositionGetString(POSITION_SYMBOL); + request.sl = newSl; + request.tp = PositionGetDouble(POSITION_TP); + if(OrderSend(request, result)) + { + Print("Minor trailing stop updated for sell position"); + return true; + } + } + } + return false; + } + +//+------------------------------------------------------------------+ +//| Perforrm Major Trailing stop | +//+------------------------------------------------------------------+ +bool performMajorTrail(ulong ticket, ENUM_POSITION_TYPE posType, double currentPrice, double lastCandleSize, double currentSl, int trailingPips, ENUM_TIMEFRAMES timeframe) + { + MqlTradeRequest request = {}; + MqlTradeResult result = {}; + double newSl = 0.0; + double newTrailSL = 0.0; + if(posType == POSITION_TYPE_BUY) + { + newSl = currentPrice - lastCandleSize; + newTrailSL = currentPrice - trailingPips * _Point; + newSl = (newSl < newTrailSL) ? newTrailSL : newSl; + if(newSl > currentSl) + { + request.action = TRADE_ACTION_SLTP; + request.position = ticket; + request.symbol = PositionGetString(POSITION_SYMBOL); + request.sl = newSl; + request.tp = PositionGetDouble(POSITION_TP); + if(OrderSend(request, result)) + { + Print("Major trailing stop updated for buy position"); + return true; + } + else + { + Print("Failed: Major trailing stop for buy position"); + } + } + } + else + { + newSl = currentPrice + lastCandleSize; + newTrailSL = currentPrice + trailingPips * _Point; + newSl = (newSl > newTrailSL) ? newTrailSL : newSl; + if(newSl < currentSl || currentSl == 0) + { + request.action = TRADE_ACTION_SLTP; + request.position = ticket; + request.symbol = PositionGetString(POSITION_SYMBOL); + request.sl = newSl; + request.tp = PositionGetDouble(POSITION_TP); + if(OrderSend(request, result)) + { + Print("Major trailing stop updated for sell position"); + return true; + } + else + { + Print("Failed: Major trailing stop for sell position"); + } + } + } + return false; + } + +//+------------------------------------------------------------------+ +//| Update Trailing stop | +//+------------------------------------------------------------------+ +void UpdateTrailingStop(ulong ticket, ENUM_POSITION_TYPE posType, double currentPrice, ENUM_TIMEFRAMES timeframe) + { + double currentSl = PositionGetDouble(POSITION_SL); + double priceOpen = PositionGetDouble(POSITION_PRICE_OPEN); + double currentProfit = PositionGetDouble(POSITION_PROFIT); + double lastCandleHigh = iHigh(Symbol(), timeframe, 1); + double lastCandleLow = iLow(Symbol(), timeframe, 1); + double lastCandleSize = MathAbs(lastCandleHigh - lastCandleLow); + int candlesOpen = (int)((TimeCurrent() - PositionGetInteger(POSITION_TIME)) / PeriodSeconds(timeframe)); + + + if(candlesOpen > 1 && candlesOpen <= 3 && currentProfit > 0) + { + performMinorTrail(ticket, posType, priceOpen, currentSl, TrailingPips, timeframe); + } + else + if(candlesOpen >= 4 && currentProfit > 0) + { + performMajorTrail(ticket, posType, currentPrice, lastCandleSize, currentSl, TrailingPips, timeframe); + } + } + +//+------------------------------------------------------------------+ +//| Modify pending order | +//+------------------------------------------------------------------+ +bool ModifyPendingOrder(ulong ticket, double price, ENUM_TIMEFRAMES timeframe) + { + MqlTradeRequest request = {}; + MqlTradeResult result = {}; + + if(OrderSelect(ticket)) + { + double stopLoss = (timeframe == PERIOD_M10) ? StopLossPips * _Point : StopLossPips * _Point * 10; + double takeProfit = (timeframe == PERIOD_M10) ? TakeProfitPips * _Point : TakeProfitPips * _Point * 10; + + request.action = TRADE_ACTION_MODIFY; + request.order = ticket; + request.price = price; + request.sl = (OrderGetInteger(ORDER_TYPE) == ORDER_TYPE_BUY_STOP) ? price - stopLoss : price + stopLoss; + request.tp = (OrderGetInteger(ORDER_TYPE) == ORDER_TYPE_BUY_STOP) ? price + takeProfit : price - takeProfit; + request.deviation = 5; + + if(OrderSend(request, result)) + { + Print("Pending order modified successfully"); + return true; + } + else + { + Print("ModifyPendingOrder::Error modifying orderrr: ", GetLastError()); + return false; + } + } + else + { + Print("ModifyPendingOrder::order select failed for ticket"+ticket); + } + return false; + } + +//+------------------------------------------------------------------+ +//| Check and delete expired pending orders | +//+------------------------------------------------------------------+ +void CheckPendingOrderExpiry(ulong ticket, ENUM_TIMEFRAMES timeframe) + { + datetime expiration = OrderGetInteger(ORDER_TIME_EXPIRATION); + if(expiration > 0 && expiration < TimeCurrent()) + { + MqlTradeRequest request = {}; + MqlTradeResult result = {}; + + request.action = TRADE_ACTION_REMOVE; + request.order = ticket; + + if(OrderSend(request, result)) + { + Print("Expired order removed: ", ticket); + } + else + { + Print("Error removing order: ", GetLastError()); + } + } + } + +//+------------------------------------------------------------------+ +//| Place pending order | +//+------------------------------------------------------------------+ +ulong PlacePendingOrder(ENUM_ORDER_TYPE orderType, double price, ENUM_TIMEFRAMES timeframe) + { + MqlTradeRequest request = {}; + MqlTradeResult result = {}; + + double stopLoss = (timeframe == PERIOD_M10) ? StopLossPips * _Point : StopLossPips * _Point * 10; + double takeProfit = (timeframe == PERIOD_M10) ? TakeProfitPips * _Point : TakeProfitPips * _Point * 10; + + request.action = TRADE_ACTION_PENDING; + request.symbol = _Symbol; + request.volume = LotSize; + request.type = orderType; + request.price = price; + request.sl = (orderType == ORDER_TYPE_BUY_STOP) ? price - stopLoss : price + stopLoss; + request.tp = (orderType == ORDER_TYPE_BUY_STOP) ? price + takeProfit : price - takeProfit; + request.deviation = 5; + request.magic = MagicNumber; + + if(OrderSend(request, result)) + { + Print("Pending order placed: ", result.order); + return result.order; + } + else + { + Print("Error placing order: ", GetLastError()); + return 0; + } + } +//+------------------------------------------------------------------+ diff --git a/36-OpenEA/OpenEA.pdf b/36-OpenEA/OpenEA.pdf new file mode 100644 index 0000000..e32d098 Binary files /dev/null and b/36-OpenEA/OpenEA.pdf differ diff --git a/37-Order-Blocks-EA/Order-Blocks-EA.mq5 b/37-Order-Blocks-EA/Order-Blocks-EA.mq5 new file mode 100644 index 0000000..abba102 Binary files /dev/null and b/37-Order-Blocks-EA/Order-Blocks-EA.mq5 differ diff --git a/37-Order-Blocks-EA/Order-Blocks-EA.pdf b/37-Order-Blocks-EA/Order-Blocks-EA.pdf new file mode 100644 index 0000000..826923c Binary files /dev/null and b/37-Order-Blocks-EA/Order-Blocks-EA.pdf differ diff --git a/38-Mitigation-Order-Blocks/Mitigation-Order-Blocks.mq5 b/38-Mitigation-Order-Blocks/Mitigation-Order-Blocks.mq5 new file mode 100644 index 0000000..d9ffb0f --- /dev/null +++ b/38-Mitigation-Order-Blocks/Mitigation-Order-Blocks.mq5 @@ -0,0 +1,339 @@ +//+------------------------------------------------------------------+ +//| Mitigation Order Blocks EA.mq5 | +//| Copyright 2025, MetaQuotes Ltd. | +//| https://www.mql5.com | +//+------------------------------------------------------------------+ +#property copyright "Copyright 2025, MetaQuotes Ltd." +#property link "https://www.mql5.com" +#property version "1.00" + +#include +CTrade obj_Trade; + +input double tradeLotSize = 0.01; +input bool enableTrading = true; +input bool enableTrailingStop = true; +input double trailingStopPoints = 30; +input double minProfitToTrail = 50; +input int uniqueMagicNumber = 1234567; +input int consolidationBars = 7; +input double maxconsolidationSpread = 50; +input int barstowaitafterbreakout = 3; +input double impulseMultiplier = 1.0; +input double stoplossDistance = 1500; +input double takeProfitdistance = 1500; +input color bullishOrderBlockColor = clrGreen; +input color bearishOrderBlockColor = clrRed; +input color mitigatedOrderBlockColor = clrGray; +input color labelTextColor = clrBlack; + +struct PriceAndIndex{ + double price; + int index; +}; + +PriceAndIndex rangeHighestHigh = {0,0}; +PriceAndIndex rangeLowestLow = {0,0}; +bool isBreakoutDetected = false; +double lastImpulseLow = 0.0; +double lastImpulseHigh = 0.0; +int breakoutBarNumber = -1; +datetime breakoutTimestamp = 0; +string orderBlockNames[]; +string orderBlockLabels[]; +datetime orderBlockEndTimes[]; +bool orderblockMitigatedStatus[]; +bool isBullishImpulse = false; +bool isBearishImpulse = false; + +#define OB_Prefix "OB REC " + + +//+------------------------------------------------------------------+ +//| Expert initialization function | +//+------------------------------------------------------------------+ +int OnInit(){ +//--- + obj_Trade.SetExpertMagicNumber(uniqueMagicNumber); +//--- + return(INIT_SUCCEEDED); +} +//+------------------------------------------------------------------+ +//| Expert deinitialization function | +//+------------------------------------------------------------------+ +void OnDeinit(const int reason){ +//--- + +} +//+------------------------------------------------------------------+ +//| Expert tick function | +//+------------------------------------------------------------------+ +void OnTick(){ +//--- + + if (enableTrailingStop){ + applyTrailingStop(trailingStopPoints,obj_Trade,uniqueMagicNumber); + } + + static bool isNewBar = false; + int currentBarCount = iBars(_Symbol,_Period); + static int previousBarCount = currentBarCount; + if (previousBarCount == currentBarCount){ + isNewBar = false; + } + else if (previousBarCount != currentBarCount){ + isNewBar = true; + previousBarCount = currentBarCount; + } + + if (!isNewBar){ + return; + } + + int startBarIndex = 1; + + int chartscale = (int)ChartGetInteger(0,CHART_SCALE); + int dynamicFontSize = 8+(chartscale*2); + + if (!isBreakoutDetected){ + if (rangeHighestHigh.price == 0 && rangeLowestLow.price == 0){ + bool isConsolidated = true; + for (int i=startBarIndex; i maxconsolidationSpread * _Point){ + isConsolidated = false; + break; + } + if (MathAbs(low(i) - low(i+1)) > maxconsolidationSpread * _Point){ + isConsolidated = false; + break; + } + } + if (isConsolidated){ + rangeHighestHigh.price = high(startBarIndex); + rangeHighestHigh.index = startBarIndex; + for (int i=startBarIndex+1; i rangeHighestHigh.price){ + rangeHighestHigh.price = high(i); + rangeHighestHigh.index = i; + } + } + rangeLowestLow.price = low(startBarIndex); + rangeLowestLow.index = startBarIndex; + for (int i=startBarIndex+1; i= rangeLowestLow.price){ + Print("Range EXTENDED: High = ",currentHigh, ", Low = ",currentLow); + } + else { + Print("No extension: Bar outside range."); + } + } + } + + if (rangeHighestHigh.price > 0 && rangeLowestLow.price > 0){ + double currentClosePrice = close(1); + if (currentClosePrice > rangeHighestHigh.price){ + Print("Upward Breakout at ",currentClosePrice, " > ",rangeHighestHigh.price); + isBreakoutDetected = true; + } + else if (currentClosePrice < rangeLowestLow.price){ + Print("Downward Breakout at ",currentClosePrice, " < ",rangeLowestLow.price); + isBreakoutDetected = true; + } + } + + if (isBreakoutDetected){ + Print("Breakout detected. Resetting for the next range."); + breakoutBarNumber = 1; + breakoutTimestamp = TimeCurrent(); + lastImpulseHigh = rangeHighestHigh.price; + lastImpulseLow = rangeLowestLow.price; + isBreakoutDetected = false; + rangeHighestHigh.price = 0; + rangeLowestLow.price = 0; + rangeHighestHigh.index = 0; + rangeLowestLow.index = 0; + } + + if (breakoutBarNumber >= 0 && TimeCurrent() > breakoutTimestamp+barstowaitafterbreakout*PeriodSeconds()){ + double impulseRange = lastImpulseHigh - lastImpulseLow; + double impulseThresholdPrice = impulseRange * impulseMultiplier; + isBullishImpulse = false; + isBearishImpulse = false; + + for (int i=1; i<=barstowaitafterbreakout; i++){ + double closePrice = close(i); + if (closePrice >= lastImpulseHigh+impulseThresholdPrice){ + isBullishImpulse = true; + Print("Impulsive upward move: ",closePrice," >= ",lastImpulseHigh+impulseThresholdPrice); + break; + } + else if (closePrice <= lastImpulseLow-impulseThresholdPrice){ + isBearishImpulse = true; + Print("Impulsive downward move: ",closePrice," <= ",lastImpulseLow-impulseThresholdPrice); + break; + } + } + + if (!isBullishImpulse && !isBearishImpulse){ + Print("No impulsive movement detected."); + } + + bool isOrderBlockValid = isBearishImpulse || isBullishImpulse; + + if (isOrderBlockValid){ + datetime blockStartTime = iTime(_Symbol,_Period,consolidationBars+barstowaitafterbreakout+1); + double blockTopPrice = lastImpulseHigh; + int visibleBarsOnchart = (int)ChartGetInteger(0,CHART_VISIBLE_BARS); + datetime blockEndTime = blockStartTime+(visibleBarsOnchart/1)*PeriodSeconds(); + double blockBottomPrice = lastImpulseLow; + string orderBlockName = OB_Prefix+"("+TimeToString(blockStartTime)+")"; + color orderBlockColor = isBullishImpulse ? bullishOrderBlockColor : bearishOrderBlockColor; + string orderBlockLabel = isBullishImpulse ? "Bullish OB" : "Bearish OB"; + + if (ObjectFind(0, orderBlockName) < 0){ + ObjectCreate(0,orderBlockName,OBJ_RECTANGLE,0,blockStartTime,blockTopPrice,blockEndTime,blockBottomPrice); + ObjectSetInteger(0,orderBlockName,OBJPROP_TIME,0,blockStartTime); + ObjectSetDouble(0,orderBlockName,OBJPROP_PRICE,0,blockTopPrice); + ObjectSetInteger(0,orderBlockName,OBJPROP_TIME,1,blockEndTime); + ObjectSetDouble(0,orderBlockName,OBJPROP_PRICE,1,blockBottomPrice); + ObjectSetInteger(0,orderBlockName,OBJPROP_FILL,true); + ObjectSetInteger(0,orderBlockName,OBJPROP_COLOR,orderBlockColor); + ObjectSetInteger(0,orderBlockName,OBJPROP_BACK,false); + + datetime labelTime = blockStartTime + (blockEndTime-blockStartTime)/2; + double labelPrice = (blockTopPrice+blockBottomPrice)/2; + string labelObjectName = orderBlockName+orderBlockLabel; + if (ObjectFind(0,labelObjectName) < 0){ + ObjectCreate(0,labelObjectName,OBJ_TEXT,0,labelTime,labelPrice); + ObjectSetString(0,labelObjectName,OBJPROP_TEXT,orderBlockLabel); + ObjectSetInteger(0,labelObjectName,OBJPROP_COLOR,labelTextColor); + ObjectSetInteger(0,labelObjectName,OBJPROP_ANCHOR,ANCHOR_CENTER); + ObjectSetInteger(0,labelObjectName,OBJPROP_FONTSIZE,dynamicFontSize); + } + ChartRedraw(0); + + ArrayResize(orderBlockNames,ArraySize(orderBlockNames)+1); + orderBlockNames[ArraySize(orderBlockNames)-1] = orderBlockName; + ArrayResize(orderBlockLabels,ArraySize(orderBlockLabels)+1); + orderBlockLabels[ArraySize(orderBlockLabels)-1] = labelObjectName; + ArrayResize(orderBlockEndTimes,ArraySize(orderBlockEndTimes)+1); + orderBlockEndTimes[ArraySize(orderBlockEndTimes)-1] = blockEndTime; + ArrayResize(orderblockMitigatedStatus,ArraySize(orderblockMitigatedStatus)+1); + orderblockMitigatedStatus[ArraySize(orderblockMitigatedStatus)-1] = false; + + Print("Order Block created: ",orderBlockName); + } + } + breakoutBarNumber = -1; + breakoutTimestamp = 0; + lastImpulseHigh = 0; + lastImpulseLow = 0; + isBullishImpulse = false; + isBearishImpulse = false; + } + + for (int j=ArraySize(orderBlockNames)-1; j>=0; j--){ + string currentOrderBlockName = orderBlockNames[j]; + string currentOrderBlockLabel = orderBlockLabels[j]; + bool doesOrderBlockExist = false; + + double orderBlockHigh = ObjectGetDouble(0,currentOrderBlockName,OBJPROP_PRICE,0); + double orderBlockLow = ObjectGetDouble(0,currentOrderBlockName,OBJPROP_PRICE,1); + datetime orderBlockStartTime = (datetime)ObjectGetInteger(0,currentOrderBlockName,OBJPROP_TIME,0); + datetime orderBlockEndTime = (datetime)ObjectGetInteger(0,currentOrderBlockName,OBJPROP_TIME,1); + color orderBlockCurrentColor = (color)ObjectGetInteger(0,currentOrderBlockName,OBJPROP_COLOR); + + if (time(1) < orderBlockEndTime){ + doesOrderBlockExist = true; + } + + double currentAskPrice = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); + double currentBidPrice = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits); + + if (enableTrading && orderBlockCurrentColor == bullishOrderBlockColor && close(1) < orderBlockLow && !orderblockMitigatedStatus[j]){ + double entryPrice = currentBidPrice; + double stoplossPrice = entryPrice+stoplossDistance*_Point; + double takeprofitPrice = entryPrice-takeProfitdistance*_Point; + obj_Trade.Sell(tradeLotSize,_Symbol,entryPrice,stoplossPrice,takeprofitPrice); + orderblockMitigatedStatus[j] = true; + ObjectSetInteger(0,currentOrderBlockName,OBJPROP_COLOR,mitigatedOrderBlockColor); + string blockDescription = "Bullish Order Block"; + string textObjectName = currentOrderBlockName+blockDescription; + ObjectSetString(0,currentOrderBlockLabel,OBJPROP_TEXT,"Mitigated "+blockDescription); + + Print("Sell trade entered upon mitigation of the bullish OB: ",currentOrderBlockName); + } + else if (enableTrading && orderBlockCurrentColor == bearishOrderBlockColor && close(1) > orderBlockHigh && !orderblockMitigatedStatus[j]){ + double entryPrice = currentAskPrice; + double stoplossPrice = entryPrice-stoplossDistance*_Point; + double takeprofitPrice = entryPrice+takeProfitdistance*_Point; + obj_Trade.Buy(tradeLotSize,_Symbol,entryPrice,stoplossPrice,takeprofitPrice); + orderblockMitigatedStatus[j] = true; + ObjectSetInteger(0,currentOrderBlockName,OBJPROP_COLOR,mitigatedOrderBlockColor); + string blockDescription = "Bearish Order Block"; + string textObjectName = currentOrderBlockName+blockDescription; + ObjectSetString(0,currentOrderBlockLabel,OBJPROP_TEXT,"Mitigated "+blockDescription); + + Print("Buy trade entered upon mitigation of the bearish OB: ",currentOrderBlockName); + } + + if (!doesOrderBlockExist){ + bool removedName = ArrayRemove(orderBlockNames,j,1); + bool removedLabel = ArrayRemove(orderBlockLabels,j,1); + bool removedTime = ArrayRemove(orderBlockEndTimes,j,1); + bool removedStatus = ArrayRemove(orderblockMitigatedStatus,j,1); + if (removedName && removedTime && removedStatus && removedLabel){ + Print("Success removing OB data from arrays at index ",j); + } + } + } +} + +//+------------------------------------------------------------------+ + +double high (int index) {return iHigh(_Symbol,_Period,index);} +double low (int index) {return iLow(_Symbol,_Period,index);} +double open (int index) {return iOpen(_Symbol,_Period,index);} +double close (int index) {return iClose(_Symbol,_Period,index);} +datetime time (int index) {return iTime(_Symbol,_Period,index);} + +void applyTrailingStop(double trailingPoints, CTrade &trade_object, int magicNo = 0){ + double buyStopLoss = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID)-trailingPoints*_Point,_Digits); + double sellStopLoss = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK)+trailingPoints*_Point,_Digits); + + for (int i=PositionsTotal()-1; i>=0; i--){ + ulong ticket = PositionGetTicket(i); + if (ticket > 0){ + if (PositionSelectByTicket(ticket)){ + if (PositionGetString(POSITION_SYMBOL)==_Symbol && + (magicNo == 0 || PositionGetInteger(POSITION_MAGIC)==magicNo) + ){ + if (PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY && + buyStopLoss > PositionGetDouble(POSITION_PRICE_OPEN) && + (buyStopLoss > PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0) + ){ + trade_object.PositionModify(ticket,buyStopLoss,PositionGetDouble(POSITION_TP)); + } + else if (PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL && + sellStopLoss < PositionGetDouble(POSITION_PRICE_OPEN) && + (sellStopLoss < PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0) + ){ + trade_object.PositionModify(ticket,sellStopLoss,PositionGetDouble(POSITION_TP)); + } + } + } + } + } +} diff --git a/38-Mitigation-Order-Blocks/Mitigation-Order-Blocks.pdf b/38-Mitigation-Order-Blocks/Mitigation-Order-Blocks.pdf new file mode 100644 index 0000000..0f5ca2d Binary files /dev/null and b/38-Mitigation-Order-Blocks/Mitigation-Order-Blocks.pdf differ diff --git a/39-MACDexpertadvisor/MACDexpertadvisor.mq5 b/39-MACDexpertadvisor/MACDexpertadvisor.mq5 new file mode 100644 index 0000000..b0d572c --- /dev/null +++ b/39-MACDexpertadvisor/MACDexpertadvisor.mq5 @@ -0,0 +1,175 @@ +//+------------------------------------------------------------------+ +//| prueba.mq5 | +//| Copyright 2020, MetaQuotes Software Corp. | +//| https://www.mql5.com | +//+------------------------------------------------------------------+ +#property copyright "Copyright 2020, MetaQuotes Software Corp." +#property link "https://www.mql5.com" +#property version "1.00" +//+------------------------------------------------------------------+ +//| Include | +//+------------------------------------------------------------------+ +#include +//--- available signals +#include +//--- available trailing +#include +//--- available money management +#include +//+------------------------------------------------------------------+ +//| Inputs | +//+------------------------------------------------------------------+ +//--- inputs for expert +input string Expert_Title ="prueba"; // Document name +ulong Expert_MagicNumber =2727; // +bool Expert_EveryTick =false; // +//--- inputs for main signal +input int Signal_ThresholdOpen =10; // Signal threshold value to open [0...100] +input int Signal_ThresholdClose =10; // Signal threshold value to close [0...100] +input double Signal_PriceLevel =0.0; // Price level to execute a deal +input double Signal_StopLevel =50.0; // Stop Loss level (in points) +input double Signal_TakeLevel =50.0; // Take Profit level (in points) +input int Signal_Expiration =4; // Expiration of pending orders (in bars) +input int Signal_MACD_PeriodFast =12; // MACD(12,24,9,PRICE_CLOSE) Period of fast EMA +input int Signal_MACD_PeriodSlow =24; // MACD(12,24,9,PRICE_CLOSE) Period of slow EMA +input int Signal_MACD_PeriodSignal=9; // MACD(12,24,9,PRICE_CLOSE) Period of averaging of difference +input ENUM_APPLIED_PRICE Signal_MACD_Applied =PRICE_CLOSE; // MACD(12,24,9,PRICE_CLOSE) Prices series +input double Signal_MACD_Weight =1.0; // MACD(12,24,9,PRICE_CLOSE) Weight [0...1.0] +//--- inputs for money +input double Money_FixLot_Percent =10.0; // Percent +input double Money_FixLot_Lots =0.1; // Fixed volume +//+------------------------------------------------------------------+ +//| Global expert object | +//+------------------------------------------------------------------+ +CExpert ExtExpert; +//+------------------------------------------------------------------+ +//| Initialization function of the expert | +//+------------------------------------------------------------------+ +int OnInit() + { +//--- Initializing expert + if(!ExtExpert.Init(Symbol(),Period(),Expert_EveryTick,Expert_MagicNumber)) + { + //--- failed + printf(__FUNCTION__+": error initializing expert"); + ExtExpert.Deinit(); + return(INIT_FAILED); + } +//--- Creating signal + CExpertSignal *signal=new CExpertSignal; + if(signal==NULL) + { + //--- failed + printf(__FUNCTION__+": error creating signal"); + ExtExpert.Deinit(); + return(INIT_FAILED); + } +//--- + ExtExpert.InitSignal(signal); + signal.ThresholdOpen(Signal_ThresholdOpen); + signal.ThresholdClose(Signal_ThresholdClose); + signal.PriceLevel(Signal_PriceLevel); + signal.StopLevel(Signal_StopLevel); + signal.TakeLevel(Signal_TakeLevel); + signal.Expiration(Signal_Expiration); +//--- Creating filter CSignalMACD + CSignalMACD *filter0=new CSignalMACD; + if(filter0==NULL) + { + //--- failed + printf(__FUNCTION__+": error creating filter0"); + ExtExpert.Deinit(); + return(INIT_FAILED); + } + signal.AddFilter(filter0); +//--- Set filter parameters + filter0.PeriodFast(Signal_MACD_PeriodFast); + filter0.PeriodSlow(Signal_MACD_PeriodSlow); + filter0.PeriodSignal(Signal_MACD_PeriodSignal); + filter0.Applied(Signal_MACD_Applied); + filter0.Weight(Signal_MACD_Weight); +//--- Creation of trailing object + CTrailingNone *trailing=new CTrailingNone; + if(trailing==NULL) + { + //--- failed + printf(__FUNCTION__+": error creating trailing"); + ExtExpert.Deinit(); + return(INIT_FAILED); + } +//--- Add trailing to expert (will be deleted automatically)) + if(!ExtExpert.InitTrailing(trailing)) + { + //--- failed + printf(__FUNCTION__+": error initializing trailing"); + ExtExpert.Deinit(); + return(INIT_FAILED); + } +//--- Set trailing parameters +//--- Creation of money object + CMoneyFixedLot *money=new CMoneyFixedLot; + if(money==NULL) + { + //--- failed + printf(__FUNCTION__+": error creating money"); + ExtExpert.Deinit(); + return(INIT_FAILED); + } +//--- Add money to expert (will be deleted automatically)) + if(!ExtExpert.InitMoney(money)) + { + //--- failed + printf(__FUNCTION__+": error initializing money"); + ExtExpert.Deinit(); + return(INIT_FAILED); + } +//--- Set money parameters + money.Percent(Money_FixLot_Percent); + money.Lots(Money_FixLot_Lots); +//--- Check all trading objects parameters + if(!ExtExpert.ValidationSettings()) + { + //--- failed + ExtExpert.Deinit(); + return(INIT_FAILED); + } +//--- Tuning of all necessary indicators + if(!ExtExpert.InitIndicators()) + { + //--- failed + printf(__FUNCTION__+": error initializing indicators"); + ExtExpert.Deinit(); + return(INIT_FAILED); + } +//--- ok + return(INIT_SUCCEEDED); + } +//+------------------------------------------------------------------+ +//| Deinitialization function of the expert | +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) + { + ExtExpert.Deinit(); + } +//+------------------------------------------------------------------+ +//| "Tick" event handler function | +//+------------------------------------------------------------------+ +void OnTick() + { + ExtExpert.OnTick(); + } +//+------------------------------------------------------------------+ +//| "Trade" event handler function | +//+------------------------------------------------------------------+ +void OnTrade() + { + ExtExpert.OnTrade(); + } +//+------------------------------------------------------------------+ +//| "Timer" event handler function | +//+------------------------------------------------------------------+ +void OnTimer() + { + ExtExpert.OnTimer(); + } +//+------------------------------------------------------------------+ diff --git a/39-MACDexpertadvisor/MACDexpertadvisor.pdf b/39-MACDexpertadvisor/MACDexpertadvisor.pdf new file mode 100644 index 0000000..50d7eed Binary files /dev/null and b/39-MACDexpertadvisor/MACDexpertadvisor.pdf differ diff --git a/40-NNFX-AlphaStrategy/NNFX-AlphaStrategy.mq4 b/40-NNFX-AlphaStrategy/NNFX-AlphaStrategy.mq4 new file mode 100644 index 0000000..7715199 --- /dev/null +++ b/40-NNFX-AlphaStrategy/NNFX-AlphaStrategy.mq4 @@ -0,0 +1,483 @@ +//+------------------------------------------------------------------+ +//| AlphaStrategyV1.mq4 | +//| Copyright 2020, MetaQuotes Software Corp. | +//| https://www.mql5.com | +//+------------------------------------------------------------------+ +#property copyright "Copyright 2020, MetaQuotes Software Corp." +#property link "https://www.mql5.com" +#property version "1.00" +#property strict + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +//--- inputs +input int InpMagic = 1024; +input int Deviation = 50; + +input string x7 = "------Special Rules-----"; +input bool ApplySevenCandleRule = false; +input bool ApplyOneCandleRule = true; +input bool ApplyPullbackRule = true; +input bool ApplyContinuationRule = true; + + +input string x = "----Baseline-----"; +input int InpKijun_b = 26; //InpKijun + +input string x2 = "------C1-----------"; +input int InpPeriods_C1 = 14; // InpPeriods +input ENUM_MA_METHOD InpMethod_C1 = MODE_SMA; //InpMethod + +input string x3 = "------C2-----------"; +input string InpModeStr_C2="Mode: 0 - RSI, 1 - Stoch"; +input int InpMode_C2=0; // InpMode +input int InpLength_C2=9; //InpLength +input int InpSmooth_Length_C2=2; // InpSmooth_Length +input int InpPrice_C2=0; // InpPrice + // Applied price + // 0 - Close + // 1 - Open + // 2 - High + // 3 - Low + // 4 - Median + // 5 - Typical + // 6 - Weighted +input int InpMethod_C2=0; + // 0 - SMA + // 1 - EMA + // 2 - SMMA + // 3 - LWMA +input string x4 = "---------Volumen------"; +input int InpSensetive_v = 150; //InpSensitive +input int InpDeadZonePip_v = 30; // InpDeadZonePip +input int InpExplosionPower_v = 15; // InpExplosionPower +input int InpTrendPower_v = 15; // InpTrendPower + +input string x5 = "---------Exit---------"; +input int InpSmoothing_Length_x=14; // InpSmoothing_Length +input int InpSmoothing_Method_x=0; // InpSmoothing_Method + // 0 - SMA + // 1 - EMA + // 2 - SMMA + // 3 - LWMA +input int InpSignal_Length_x=14; // InpSignal_Length +input int InpSignal_Method_x=0; // InpSignal_Method + // 0 - SMA + // 1 - EMA + // 2 - SMMA + // 3 - LWMA +input string x6 = "---------Monet management------"; +input int InpTakeProfit = 100; +input int InpStopLoss = 100; +input double InpLotSize = 0.05; + +//--- global variables +CNewBar NewBar; +string symbol; +int period; + +bool longPosition; +bool shortPosition; +bool isPositionOpened; +bool OneCandleForLong; +bool OneCandleForShort; +OrderState orderState; + +EnterTime forLong; +EnterTime forShort; +TrendStatus trendStatus; + +NiKijun *baseline; +/* +NiSSLActivator *c1; +NiASH *c2; +NiWAE *volumeIndicator; +NiRex *exitIndicator; +*/ + +NiNone *c1; +NiNone *c2; +NiNone *volumeIndicator; +NiNone *exitIndicator; + + +NiMoneyScaleOut *money; + +//+------------------------------------------------------------------+ +//| Expert initialization function | +//+------------------------------------------------------------------+ +int OnInit() + { +//--- + orderState.LongPosition = false; + orderState.ShortPosition = false; + orderState.OrderStatus = false; + + symbol = _Symbol; + period = _Period; + + baseline = new NiKijun(InpKijun_b); + /* + c1 = new NiSSLActivator(InpPeriods_C1,InpMethod_C1); + c2 = new NiASH(InpModeStr_C2,InpMode_C2,InpLength_C2,InpSmooth_Length_C2,InpPrice_C2,InpMethod_C2); + volumeIndicator = new NiWAE(InpSensetive_v,InpDeadZonePip_v,InpExplosionPower_v,InpTrendPower_v); + exitIndicator = new NiRex(InpSmoothing_Length_x,InpSmoothing_Method_x,InpSignal_Length_x,InpSignal_Method_x); + */ + + c1 = new NiNone(); + c2 = new NiNone(); + volumeIndicator = new NiNone(); + exitIndicator = new NiNone(); + + baseline.InitIndicator(symbol,period); + c1.InitIndicator(symbol,period); + c2.InitIndicator(symbol,period); + volumeIndicator.InitIndicator(symbol,period); + exitIndicator.InitIndicator(symbol,period); + + //money = new NiMoney(InpLotSize,InpTakeProfit,InpStopLoss,InpMagic,Deviation); + money = new NiMoneyScaleOut(InpLotSize,InpMagic,Deviation); + + +//--- + return(INIT_SUCCEEDED); + } +//+------------------------------------------------------------------+ +//| Expert deinitialization function | +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) + { +//--- + delete baseline; + delete c1; + delete c2; + delete volumeIndicator; + delete exitIndicator; + delete money; + } +//+------------------------------------------------------------------+ +//| Expert tick function | +//+------------------------------------------------------------------+ +void OnTick() + { +//---Detect New Bar + bool isNewBar = NewBar.checkNewBar(_Symbol,_Period); + //datetime t = iTime(_Symbol,_Period,0); + + //Print("now: " + t); + if(!isNewBar){ + return; + } + + //Print("On new Bar"); + OnBar(); + + + } +//+------------------------------------------------------------------+ +//|OnBar function | +//+------------------------------------------------------------------+ +void OnBar() + { + +//---Update Buffers + Refresh(); +//---Exit rules + if(money.isOpenedPosition()){ + manageClose(); + } + +//---Entry Rules + if(!money.isOpenedPosition()){ + manageOpen(); + } + } +//+------------------------------------------------------------------+ +//|Open Positions: Entry rules | +//+------------------------------------------------------------------+ +void manageOpen() +{ + + + manageLong(); + + manageShort(); + + if(money.isOpenedPosition()) { + //Print("Position openend..."); + //Print("position status: "+orderState.OrderStatus); + } + +} +/* +* Checks conditions if is possible open long positions +*/ +bool manageLong() +{ + bool LongSignal = false; + bool InSevenCandles = false; + + //--- Triggers an entry signal event, then checks all rules + if(c1.entryLong() || c2.entryLong()|| baseline.entryLong() || (OneCandleForLong && ApplyOneCandleRule)){ + //--- Apply Seven Candle rule + if(ApplySevenCandleRule) + { InSevenCandles = SevenCandleRuleForLong(); } + else{ InSevenCandles = false;} + + //-- Checks all Indicators for a long trade + if((InSevenCandles && ApplySevenCandleRule) || !ApplySevenCandleRule ){ + LongSignal = checkLongConditions(); + if(OneCandleForLong && LongSignal) Print("Trade ok by one candle rule"); + } + + //--- Apply One candle Rule + if(!LongSignal && !OneCandleForLong && ApplyOneCandleRule) + { OneCandleForLong = true;} + else if(OneCandleForLong == true) + { OneCandleForLong = false;} + + + //---Apply Continuation Trade Rule + if(!LongSignal && ApplyContinuationRule && ((InSevenCandles && ApplySevenCandleRule) || !ApplySevenCandleRule)) + { + if(baseline.entryLong()) { + trendStatus.EnteredInLong = Time[1]; + } + if(trendStatus.EnteredInLong > trendStatus.EnteredInShort && trendStatus.EnteredInLong > Time[7]) + { + LongSignal = checkContinuationLongConditions(); + if(LongSignal) Print("Continuation Trade ok"); + } + + } + } + + if(LongSignal){ + money.OpenLong(); + Print("Open Signal Long"); + } + return LongSignal; +} + +/* +* Use Special conditions in a continuation trade +*/ +bool checkContinuationLongConditions() +{ + bool LongSignal = baseline.baselineDirection() > 0.0 && c1.confirmationLong() && c2.confirmationLong(); + return LongSignal; +} + +/* +* Checks if c1,c2 gives the signal with in seven candles +*/ +bool SevenCandleRuleForLong() +{ + bool entrySignal = false; + + if(c1.entryLong()) forLong.c1EnteredTime = Time[1]; + if(c2.entryLong()) forLong.c2EnteredTime = Time[1]; + + //Print("7 candle rule: c1:" + forLong.c1EnteredTime + ", c2:" + forLong.c2EnteredTime); + //Print("7 candle rule: time: " + Time[7]); + if(forLong.c1EnteredTime > Time[7] && forLong.c2EnteredTime > Time[7]) + { + //Print("7 candle rule: entry"); + entrySignal = true; + } + return entrySignal; +} +//+------------------------------------------------------------------+ +//|Check conditions for entry long position | +//+------------------------------------------------------------------+ +bool checkLongConditions(){ + bool openSignal = false; + double atr_value = iATR(symbol,period,14,1); + if(volumeIndicator.confirmationLong()){ // Enough volumen to open a position + if(baseline.baselineDirection() > 0.0 && (!ApplyPullbackRule || (ApplyPullbackRule && baseline.baselineDirection() < atr_value))){ // In long tendency + if(c1.confirmationLong() && c2.confirmationLong()){ + openSignal = true; + } + } + } + + Print("In long: " +volumeIndicator.confirmationLong() +" " + baseline.baselineDirection() + " "+ c1.confirmationLong() + c2.confirmationLong()); + //openSignal = volumeIndicator.isAbleOpenPosition() && (baseline.baselineDirection(0) > 0.0) && c1.signalLong() && c2.signalLong(); + + return openSignal; + +} + +bool manageShort() +{ + bool ShortSignal = false; + bool InSevenCandles = false; + + if(c1.entryShort() || c2.entryShort() || baseline.entryShort() || (OneCandleForShort && ApplyOneCandleRule) ){ + //--- Apply Seven Candle rule + if(ApplySevenCandleRule) + { InSevenCandles = SevenCandleRuleForShort(); } + else{ InSevenCandles = false;} + //-------------------- + + //-- Checks all Indicators for a short trade + if((InSevenCandles && ApplySevenCandleRule) || !ApplySevenCandleRule ){ + ShortSignal = checkShortConditions(); + if(OneCandleForShort && ShortSignal) Print("Short trade ok by one candle rule"); + } //----------- + + //--- Apply One candle Rule + if(!ShortSignal && !OneCandleForShort && ApplyOneCandleRule) + { OneCandleForShort = true;} + else if(OneCandleForShort == true) + { OneCandleForShort = false;} + //-------- + + //---Apply Continuation Trade Rule + if(!ShortSignal && ApplyContinuationRule && ((InSevenCandles && ApplySevenCandleRule) || !ApplySevenCandleRule)) + { + if(baseline.entryShort()) { + trendStatus.EnteredInShort = Time[1]; + } + if(trendStatus.EnteredInShort > trendStatus.EnteredInLong && trendStatus.EnteredInShort > Time[7]) + { + ShortSignal = checkContinuationShortConditions(); + if(ShortSignal) Print("Continuation short Trade ok"); + } + + }// end continuation trade rule + } + + + if(ShortSignal){ + money.OpenShort(); + Print("Open Signal Short"); + } + + return ShortSignal; + +} + +/* +* Use Special conditions in a continuation trade +*/ +bool checkContinuationShortConditions() +{ + bool ShortSignal = baseline.baselineDirection() < 0.0 && c1.confirmationShort() && c2.confirmationShort(); + + return ShortSignal; +} + +/* +* Checks if c1,c2 gives the signal with in seven candles +*/ +bool SevenCandleRuleForShort() +{ + bool entrySignal = false; + + if(c1.entryShort()) forShort.c1EnteredTime = Time[1]; + if(c2.entryShort()) forShort.c2EnteredTime = Time[1]; + + if(forShort.c1EnteredTime > Time[7] && forShort.c2EnteredTime > Time[7]) + { + entrySignal = true; + } + return entrySignal; +} + + +//+------------------------------------------------------------------+ +//|Check conditions for entry short position | +//+------------------------------------------------------------------+ +bool checkShortConditions(){ + bool openSignal = false; + double atr_value = iATR(symbol,period,14,1); + + if(volumeIndicator.confirmationShort()){ + if(baseline.baselineDirection() < 0.0 && (!ApplyPullbackRule || (ApplyPullbackRule && baseline.baselineDirection() < -1*atr_value)) ){ + if(c1.confirmationShort() && c2.confirmationShort()){ + openSignal = true; + } + } + } + + Print("In short: " + volumeIndicator.confirmationShort() +" " + baseline.baselineDirection() + " "+ c1.confirmationShort() + c2.confirmationShort()); + //openSignal = volumeIndicator.isAbleOpenPosition() && (baseline.baselineDirection(0) < 0.0) && c1.signalShort() && c2.signalShort(); + + return openSignal; +} + +//+------------------------------------------------------------------+ +//|Close Positions: Exit rules | +//+------------------------------------------------------------------+ +void manageClose() +{ + bool closeSignal = false; + + if(money.isLongPosition()){ + closeSignal = exitIndicator.exitLong(); + }else if(money.isShortPosition()){ + closeSignal = exitIndicator.exitShort(); + } + + if(closeSignal) + { + if(money.isLongPosition()){ + money.CloseLongPosition(); + }else if(money.isShortPosition()){ + money.CloseShortPosition(); + } + + Print("Close Signal"); + //Print("Position closed..."); + //Print("position status: "+orderState.OrderStatus); + } + +} + + +//+------------------------------------------------------------------+ +//|Refresh indicators buffers data | +//+------------------------------------------------------------------+ +void Refresh() +{ + + baseline.Refresh(); + c1.Refresh(); + c2.Refresh(); + volumeIndicator.Refresh(); + exitIndicator.Refresh(); + money.Refresh(); + + + + /*Maybe In NiMomey class*/ + int i = 0; + bool flag = false; + while(i< OrdersTotal() && !flag) + { + OrderSelect(i,SELECT_BY_POS); + if(OrderMagicNumber() == InpMagic) + { + flag = true; + } + i++; + } + + if(!flag){ /*No orders made by this EA founded*/ + money.InitTSParams(); + } + + //Print("Orders total :" + OrdersTotal()); + +} \ No newline at end of file diff --git a/40-NNFX-AlphaStrategy/NNFX-AlphaStrategy.pdf b/40-NNFX-AlphaStrategy/NNFX-AlphaStrategy.pdf new file mode 100644 index 0000000..0d9ec61 Binary files /dev/null and b/40-NNFX-AlphaStrategy/NNFX-AlphaStrategy.pdf differ diff --git a/41-NNFX-StrategyOne/NNFX-StrategyOne.mq4 b/41-NNFX-StrategyOne/NNFX-StrategyOne.mq4 new file mode 100644 index 0000000..edf4e3c --- /dev/null +++ b/41-NNFX-StrategyOne/NNFX-StrategyOne.mq4 @@ -0,0 +1,486 @@ +//+------------------------------------------------------------------+ +//| StrategyOne.mq4 | +//| Copyright 2020, MetaQuotes Software Corp. | +//| https://www.mql5.com | +//+------------------------------------------------------------------+ +#property copyright "Copyright 2020, MetaQuotes Software Corp." +#property link "https://www.mql5.com" +#property version "3.00" +#property strict + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +//--- inputs +input int InpMagic = 1024; +input int Deviation = 50; + +input string x7 = "------Special Rules-----"; +input bool ApplySevenCandleRule = false; +input bool ApplyOneCandleRule = true; +input bool ApplyPullbackRule = true; +input bool ApplyContinuationRule = true; + + +input string x = "----Baseline-----"; +input int InpKijun_b = 26; //InpKijun + +input string x2 = "------C1-----------"; +input int InpPeriods_C1 = 14; // InpPeriods +input ENUM_MA_METHOD InpMethod_C1 = MODE_SMA; //InpMethod + +input string x3 = "------C2-----------"; +input string InpModeStr_C2="Mode: 0 - RSI, 1 - Stoch"; +input int InpMode_C2=0; // InpMode +input int InpLength_C2=9; //InpLength +input int InpSmooth_Length_C2=2; // InpSmooth_Length +input int InpPrice_C2=0; // InpPrice + // Applied price + // 0 - Close + // 1 - Open + // 2 - High + // 3 - Low + // 4 - Median + // 5 - Typical + // 6 - Weighted +input int InpMethod_C2=0; + // 0 - SMA + // 1 - EMA + // 2 - SMMA + // 3 - LWMA +input string x4 = "---------Volumen------"; +input int InpSensetive_v = 150; //InpSensitive +input int InpDeadZonePip_v = 30; // InpDeadZonePip +input int InpExplosionPower_v = 15; // InpExplosionPower +input int InpTrendPower_v = 15; // InpTrendPower + +input string x5 = "---------Exit---------"; +input int InpSmoothing_Length_x=14; // InpSmoothing_Length +input int InpSmoothing_Method_x=0; // InpSmoothing_Method + // 0 - SMA + // 1 - EMA + // 2 - SMMA + // 3 - LWMA +input int InpSignal_Length_x=14; // InpSignal_Length +input int InpSignal_Method_x=0; // InpSignal_Method + // 0 - SMA + // 1 - EMA + // 2 - SMMA + // 3 - LWMA +input string x6 = "---------Monet management------"; +input int InpTakeProfit = 100; +input int InpStopLoss = 100; +input double InpLotSize = 0.05; + +//--- global variables +CNewBar NewBar; +string symbol; +int period; + +bool longPosition; +bool shortPosition; +bool isPositionOpened; +bool OneCandleForLong; +bool OneCandleForShort; +OrderState orderState; + +EnterTime forLong; +EnterTime forShort; +TrendStatus trendStatus; + +//NiKijun *baseline; +NiMA *baseline; +/* +NiSSLActivator *c1; +NiASH *c2; +NiWAE *volumeIndicator; +NiRex *exitIndicator; +*/ + +NiNone *c1; +NiNone *c2; +NiNone *volumeIndicator; +NiNone *exitIndicator; + + +IMoney *money; + +//+------------------------------------------------------------------+ +//| Expert initialization function | +//+------------------------------------------------------------------+ +int OnInit() + { +//--- + orderState.LongPosition = false; + orderState.ShortPosition = false; + orderState.OrderStatus = false; + + symbol = _Symbol; + period = _Period; + + //baseline = new NiKijun(InpKijun_b); + baseline = new NiMA(30,0,MODE_SMA,PRICE_CLOSE); + /* + c1 = new NiSSLActivator(InpPeriods_C1,InpMethod_C1); + c2 = new NiASH(InpModeStr_C2,InpMode_C2,InpLength_C2,InpSmooth_Length_C2,InpPrice_C2,InpMethod_C2); + volumeIndicator = new NiWAE(InpSensetive_v,InpDeadZonePip_v,InpExplosionPower_v,InpTrendPower_v); + exitIndicator = new NiRex(InpSmoothing_Length_x,InpSmoothing_Method_x,InpSignal_Length_x,InpSignal_Method_x); + */ + + c1 = new NiNone(); + c2 = new NiNone(); + volumeIndicator = new NiNone(); + exitIndicator = new NiNone(); + + baseline.InitIndicator(symbol,period); + c1.InitIndicator(symbol,period); + c2.InitIndicator(symbol,period); + volumeIndicator.InitIndicator(symbol,period); + exitIndicator.InitIndicator(symbol,period); + + //money = new NiMoney(InpLotSize,InpTakeProfit,InpStopLoss,InpMagic,Deviation); + money = new NiMoneyScaleOut(InpLotSize,InpMagic,Deviation); + + +//--- + return(INIT_SUCCEEDED); + } +//+------------------------------------------------------------------+ +//| Expert deinitialization function | +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) + { +//--- + delete baseline; + delete c1; + delete c2; + delete volumeIndicator; + delete exitIndicator; + delete money; + } +//+------------------------------------------------------------------+ +//| Expert tick function | +//+------------------------------------------------------------------+ +void OnTick() + { +//---Detect New Bar + bool isNewBar = NewBar.checkNewBar(_Symbol,_Period); + //datetime t = iTime(_Symbol,_Period,0); + + //Print("now: " + t); + if(!isNewBar){ + return; + } + + //Print("On new Bar"); + OnBar(); + + + } +//+------------------------------------------------------------------+ +//|OnBar function | +//+------------------------------------------------------------------+ +void OnBar() + { + +//---Update Buffers + Refresh(); +//---Exit rules + if(money.isOpenedPosition()){ + manageClose(); + } + +//---Entry Rules + if(!money.isOpenedPosition()){ + manageOpen(); + } + } +//+------------------------------------------------------------------+ +//|Open Positions: Entry rules | +//+------------------------------------------------------------------+ +void manageOpen() +{ + + + manageLong(); + + manageShort(); + + if(money.isOpenedPosition()) { + //Print("Position openend..."); + //Print("position status: "+orderState.OrderStatus); + } + +} + +/* +* Checks conditions if is possible open long positions +*/ +bool manageLong() +{ + bool LongSignal = false; + bool InSevenCandles = false; + + //--- Triggers an entry signal event, then checks all rules + if(c1.entryLong() || c2.entryLong()|| baseline.entryLong() || (OneCandleForLong && ApplyOneCandleRule)){ + //--- Apply Seven Candle rule + if(ApplySevenCandleRule) + { InSevenCandles = SevenCandleRuleForLong(); } + else{ InSevenCandles = false;} + + //-- Checks all Indicators for a long trade + if((InSevenCandles && ApplySevenCandleRule) || !ApplySevenCandleRule ){ + LongSignal = checkLongConditions(); + if(OneCandleForLong && LongSignal) Print("Trade ok by one candle rule"); + } + + //--- Apply One candle Rule + if(!LongSignal && !OneCandleForLong && ApplyOneCandleRule) + { OneCandleForLong = true;} + else if(OneCandleForLong == true) + { OneCandleForLong = false;} + + + //---Apply Continuation Trade Rule + if(!LongSignal && ApplyContinuationRule && ((InSevenCandles && ApplySevenCandleRule) || !ApplySevenCandleRule)) + { + if(baseline.entryLong()) { + trendStatus.EnteredInLong = Time[1]; + } + if(trendStatus.EnteredInLong > trendStatus.EnteredInShort && trendStatus.EnteredInLong > Time[7]) + { + LongSignal = checkContinuationLongConditions(); + if(LongSignal) Print("Continuation Trade ok"); + } + + } + } + + if(LongSignal){ + money.OpenLong(); + Print("Open Signal Long"); + } + return LongSignal; +} + +/* +* Use Special conditions in a continuation trade +*/ +bool checkContinuationLongConditions() +{ + bool LongSignal = baseline.baselineDirection() > 0.0 && c1.confirmationLong() && c2.confirmationLong(); + return LongSignal; +} + +/* +* Checks if c1,c2 gives the signal with in seven candles +*/ +bool SevenCandleRuleForLong() +{ + bool entrySignal = false; + + if(c1.entryLong()) forLong.c1EnteredTime = Time[1]; + if(c2.entryLong()) forLong.c2EnteredTime = Time[1]; + + //Print("7 candle rule: c1:" + forLong.c1EnteredTime + ", c2:" + forLong.c2EnteredTime); + //Print("7 candle rule: time: " + Time[7]); + if(forLong.c1EnteredTime > Time[7] && forLong.c2EnteredTime > Time[7]) + { + //Print("7 candle rule: entry"); + entrySignal = true; + } + return entrySignal; +} +//+------------------------------------------------------------------+ +//|Check conditions for entry long position | +//+------------------------------------------------------------------+ +bool checkLongConditions(){ + bool openSignal = false; + double atr_value = iATR(symbol,period,14,1); + if(volumeIndicator.confirmationLong()){ // Enough volumen to open a position + if(baseline.baselineDirection() > 0.0 && (!ApplyPullbackRule || (ApplyPullbackRule && baseline.baselineDirection() < atr_value))){ // In long tendency + if(c1.confirmationLong() && c2.confirmationLong()){ + openSignal = true; + } + } + } + + Print("In long: " +volumeIndicator.confirmationLong() +" " + baseline.baselineDirection() + " "+ c1.confirmationLong() + c2.confirmationLong()); + //openSignal = volumeIndicator.isAbleOpenPosition() && (baseline.baselineDirection(0) > 0.0) && c1.signalLong() && c2.signalLong(); + + return openSignal; + +} + +bool manageShort() +{ + bool ShortSignal = false; + bool InSevenCandles = false; + + if(c1.entryShort() || c2.entryShort() || baseline.entryShort() || (OneCandleForShort && ApplyOneCandleRule) ){ + //--- Apply Seven Candle rule + if(ApplySevenCandleRule) + { InSevenCandles = SevenCandleRuleForShort(); } + else{ InSevenCandles = false;} + //-------------------- + + //-- Checks all Indicators for a short trade + if((InSevenCandles && ApplySevenCandleRule) || !ApplySevenCandleRule ){ + ShortSignal = checkShortConditions(); + if(OneCandleForShort && ShortSignal) Print("Short trade ok by one candle rule"); + } //----------- + + //--- Apply One candle Rule + if(!ShortSignal && !OneCandleForShort && ApplyOneCandleRule) + { OneCandleForShort = true;} + else if(OneCandleForShort == true) + { OneCandleForShort = false;} + //-------- + + //---Apply Continuation Trade Rule + if(!ShortSignal && ApplyContinuationRule && ((InSevenCandles && ApplySevenCandleRule) || !ApplySevenCandleRule)) + { + if(baseline.entryShort()) { + trendStatus.EnteredInShort = Time[1]; + } + if(trendStatus.EnteredInShort > trendStatus.EnteredInLong && trendStatus.EnteredInShort > Time[7]) + { + ShortSignal = checkContinuationShortConditions(); + if(ShortSignal) Print("Continuation short Trade ok"); + } + + }// end continuation trade rule + } + + + if(ShortSignal){ + money.OpenShort(); + Print("Open Signal Short"); + } + + return ShortSignal; + +} + +/* +* Use Special conditions in a continuation trade +*/ +bool checkContinuationShortConditions() +{ + bool ShortSignal = baseline.baselineDirection() < 0.0 && c1.confirmationShort() && c2.confirmationShort(); + + return ShortSignal; +} + +/* +* Checks if c1,c2 gives the signal with in seven candles +*/ +bool SevenCandleRuleForShort() +{ + bool entrySignal = false; + + if(c1.entryShort()) forShort.c1EnteredTime = Time[1]; + if(c2.entryShort()) forShort.c2EnteredTime = Time[1]; + + if(forShort.c1EnteredTime > Time[7] && forShort.c2EnteredTime > Time[7]) + { + entrySignal = true; + } + return entrySignal; +} + + +//+------------------------------------------------------------------+ +//|Check conditions for entry short position | +//+------------------------------------------------------------------+ +bool checkShortConditions(){ + bool openSignal = false; + double atr_value = iATR(symbol,period,14,1); + + if(volumeIndicator.confirmationShort()){ + if(baseline.baselineDirection() < 0.0 && (!ApplyPullbackRule || (ApplyPullbackRule && baseline.baselineDirection() < -1*atr_value)) ){ + if(c1.confirmationShort() && c2.confirmationShort()){ + openSignal = true; + } + } + } + + Print("In short: " + volumeIndicator.confirmationShort() +" " + baseline.baselineDirection() + " "+ c1.confirmationShort() + c2.confirmationShort()); + //openSignal = volumeIndicator.isAbleOpenPosition() && (baseline.baselineDirection(0) < 0.0) && c1.signalShort() && c2.signalShort(); + + return openSignal; +} + +//+------------------------------------------------------------------+ +//|Close Positions: Exit rules | +//+------------------------------------------------------------------+ +void manageClose() +{ + bool closeSignal = false; + + if(money.isLongPosition()){ + closeSignal = exitIndicator.exitLong(); + }else if(money.isShortPosition()){ + closeSignal = exitIndicator.exitShort(); + } + + if(closeSignal) + { + if(money.isLongPosition()){ + money.CloseLongPosition(); + }else if(money.isShortPosition()){ + money.CloseShortPosition(); + } + + Print("Close Signal"); + //Print("Position closed..."); + //Print("position status: "+orderState.OrderStatus); + } + +} + + +//+------------------------------------------------------------------+ +//|Refresh indicators buffers data | +//+------------------------------------------------------------------+ +void Refresh() +{ + + baseline.Refresh(); + c1.Refresh(); + c2.Refresh(); + volumeIndicator.Refresh(); + exitIndicator.Refresh(); + money.Refresh(); + + + + /*Maybe In NiMomey class*/ + int i = 0; + bool flag = false; + while(i< OrdersTotal() && !flag) + { + OrderSelect(i,SELECT_BY_POS); + if(OrderMagicNumber() == InpMagic) + { + flag = true; + } + i++; + } + + if(!flag){ /*No orders made by this EA founded*/ + money.InitTSParams(); + } + + //Print("Orders total :" + OrdersTotal()); + +} \ No newline at end of file diff --git a/41-NNFX-StrategyOne/NNFX-StrategyOne.pdf b/41-NNFX-StrategyOne/NNFX-StrategyOne.pdf new file mode 100644 index 0000000..d4eb426 Binary files /dev/null and b/41-NNFX-StrategyOne/NNFX-StrategyOne.pdf differ diff --git a/42-NNFX-StrategyOneV4/NNFX-StrategyOneV4.mq4 b/42-NNFX-StrategyOneV4/NNFX-StrategyOneV4.mq4 new file mode 100644 index 0000000..b93f081 --- /dev/null +++ b/42-NNFX-StrategyOneV4/NNFX-StrategyOneV4.mq4 @@ -0,0 +1,208 @@ +//+------------------------------------------------------------------+ +//| StrategyOne.mq4 | +//| Copyright 2020, MetaQuotes Software Corp. | +//| https://www.mql5.com | +//+------------------------------------------------------------------+ +#property copyright "Copyright 2020, MetaQuotes Software Corp." +#property link "https://www.mql5.com" +#property version "4.00" +#property strict + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + + + + +//--- inputs +input int InpMagic = 1024; +input int Deviation = 50; +input double InpLotSize = 0.05; +input double InpSLalpha = 1.5; +input double InpTPbeta = 1; + +input string x7 = "------Special Rules-----"; +input bool ApplySevenCandleRule = true; +input bool ApplyOneCandleRule = true; +input bool ApplyPullbackRule = true; +input bool ApplyContinuationRule = true; + + +input string x = "----Baseline-----"; +input int InpKijun_b = 26; //InpKijun + +input string x2 = "------C1-----------"; +input int InpPeriods_C1 = 14; // InpPeriods +input ENUM_MA_METHOD InpMethod_C1 = MODE_SMA; //InpMethod + +input string x3 = "------C2-----------"; +input string InpModeStr_C2="Mode: 0 - RSI, 1 - Stoch"; +input int InpMode_C2=0; // InpMode +input int InpLength_C2=9; //InpLength +input int InpSmooth_Length_C2=2; // InpSmooth_Length +input int InpPrice_C2=0; // InpPrice +// Applied price +// 0 - Close +// 1 - Open +// 2 - High +// 3 - Low +// 4 - Median +// 5 - Typical +// 6 - Weighted +input int InpMethod_C2=0; +// 0 - SMA +// 1 - EMA +// 2 - SMMA +// 3 - LWMA +input string x4 = "---------Volumen------"; +input int InpSensetive_v = 150; //InpSensitive +input int InpDeadZonePip_v = 30; // InpDeadZonePip +input int InpExplosionPower_v = 15; // InpExplosionPower +input int InpTrendPower_v = 15; // InpTrendPower + +input string x5 = "---------Exit---------"; +input int InpSmoothing_Length_x=14; // InpSmoothing_Length +input int InpSmoothing_Method_x=0; // InpSmoothing_Method +// 0 - SMA +// 1 - EMA +// 2 - SMMA +// 3 - LWMA +input int InpSignal_Length_x=14; // InpSignal_Length +input int InpSignal_Method_x=0; // InpSignal_Method +// 0 - SMA +// 1 - EMA +// 2 - SMMA +// 3 - LWMA +input string x6 = "---------Monet management------"; +input int InpTakeProfit = 100; +input int InpStopLoss = 100; + + +//--- global variables +CNewBar NewBar; +string symbol; +int period; + +bool longPosition; +bool shortPosition; +bool isPositionOpened; +bool OneCandleForLong; +bool OneCandleForShort; +OrderState orderState; +EnterTime forLong; +EnterTime forShort; +TrendStatus trendStatus; + + +IBaseline *baseline; +IConfirmation *c1; +IConfirmation *c2; +IVolume *volumeIndicator; +IExit *exitIndicator; + +IMoney *money; +NExpert *expert; +/* +NiNone *c1; +NiNone *c2; +NiNone *volumeIndicator; +NiNone *exitIndicator; +*/ + +//+------------------------------------------------------------------+ +//| Expert initialization function | +//+------------------------------------------------------------------+ +int OnInit() + { +//--- + orderState.LongPosition = false; + orderState.ShortPosition = false; + orderState.OrderStatus = false; + + symbol = _Symbol; + period = _Period; + + baseline = new NiKijun(InpKijun_b); + //baseline = new NiMA(30,0,MODE_SMA,PRICE_CLOSE); + + c1 = new NiSSLActivator(InpPeriods_C1,InpMethod_C1); + c2 = new NiASH(InpModeStr_C2,InpMode_C2,InpLength_C2,InpSmooth_Length_C2,InpPrice_C2,InpMethod_C2); + volumeIndicator = new NiWAE(InpSensetive_v,InpDeadZonePip_v,InpExplosionPower_v,InpTrendPower_v); + exitIndicator = new NiRex(InpSmoothing_Length_x,InpSmoothing_Method_x,InpSignal_Length_x,InpSignal_Method_x); + //money = new NiMoneyScaleOut(InpLotSize,InpMagic,Deviation); + money = new NiMoneyScaleOut(InpLotSize,InpSLalpha,InpTPbeta,14,InpMagic,Deviation); + + /* + c1 = new NiNoneC(); + c2 = new NiNoneC(); + volumeIndicator = new NiNoneV(); + exitIndicator = new NiNoneE(); + */ + + baseline.InitIndicator(symbol,period); + c1.InitIndicator(symbol,period); + c2.InitIndicator(symbol,period); + volumeIndicator.InitIndicator(symbol,period); + exitIndicator.InitIndicator(symbol,period); + + + expert = new NExpert(baseline,c1,c2,exitIndicator,volumeIndicator,money,InpMagic, + ApplyPullbackRule,ApplySevenCandleRule,ApplyContinuationRule,ApplyOneCandleRule); + + + + +//money = new NiMoney(InpLotSize,InpTakeProfit,InpStopLoss,InpMagic,Deviation); + + + +//--- + return(INIT_SUCCEEDED); + } +//+------------------------------------------------------------------+ +//| Expert deinitialization function | +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) + { +//--- + delete baseline; + delete c1; + delete c2; + delete volumeIndicator; + delete exitIndicator; + delete money; + delete expert; + } +//+------------------------------------------------------------------+ +//| Expert tick function | +//+------------------------------------------------------------------+ +void OnTick() + { +//---Detect New Bar + bool isNewBar = NewBar.checkNewBar(_Symbol,_Period); +//datetime t = iTime(_Symbol,_Period,0); + +//Print("now: " + t); + if(!isNewBar) + { + return; + } + +//Print("On new Bar"); + expert.OnBar(); + + + } diff --git a/42-NNFX-StrategyOneV4/NNFX-StrategyOneV4.pdf b/42-NNFX-StrategyOneV4/NNFX-StrategyOneV4.pdf new file mode 100644 index 0000000..2081b48 Binary files /dev/null and b/42-NNFX-StrategyOneV4/NNFX-StrategyOneV4.pdf differ diff --git a/43-RSI-Mean-Reversion/RSI-Mean-Reversion.mq4 b/43-RSI-Mean-Reversion/RSI-Mean-Reversion.mq4 new file mode 100644 index 0000000..3776baf --- /dev/null +++ b/43-RSI-Mean-Reversion/RSI-Mean-Reversion.mq4 @@ -0,0 +1,212 @@ +/* +============================================================ +Demo File: RSI_Mean_Reversion_EA - Signal Logic Showcase +Category: Mean Reversion +Platform: MetaTrader 4 (MQL4) +Version: 1.0 +Author: Giacomo Cipolat Bares +Portfolio: MQL4 Expert Advisors Portfolio +============================================================ + +Description: +This is a simplified public demo derived from the full +RSI Mean Reversion EA. + +Included in this demo: +- RSI overbought / oversold logic +- optional candle-close confirmation +- optional moving average trend filter +- optional ATR volatility filter +- basic on-chart signal output + +Excluded from this demo: +- order execution +- risk management engine +- break-even / trailing stop +- retry logic +- broker protection handling +- full production trade framework +- chart visualization layer +============================================================ +*/ + +#property strict +#property version "1.00" + +//========================= INPUTS ================================== +input string __01_RSISettings = "01 =========== RSI Settings =========="; +input int RSIPeriod = 14; +input double RSIBuyLevel = 30.0; +input double RSISellLevel = 70.0; +input double RSIExitLevel = 50.0; +input int RSIPrice = PRICE_CLOSE; +input bool UseClosedCandleSignal = true; + +input string __02_TrendFilter = "02 ========= Trend Filter ========="; +input bool UseTrendFilter = false; +input int MAPeriod = 200; +input int MAMethod = MODE_SMA; +input int MAPrice = PRICE_CLOSE; + +input string __03_ATRFilter = "03 =========== ATR Filter =========="; +input bool UseATRFilter = false; +input int ATRPeriod = 14; +input double MinATRValuePips = 5.0; + +//======================= GLOBALS =================================== +double g_point; +double g_pip; +int g_digits; +datetime g_lastBarTime = 0; + +//+------------------------------------------------------------------+ +//| Expert initialization | +//+------------------------------------------------------------------+ +int OnInit() +{ + g_point = Point; + g_digits = Digits; + + if(g_digits == 5 || g_digits == 3) + g_pip = g_point * 10.0; + else + g_pip = g_point; + + Print("RSI Mean Reversion demo initialized"); + return(INIT_SUCCEEDED); +} + +//+------------------------------------------------------------------+ +//| Detects a new bar | +//+------------------------------------------------------------------+ +bool IsNewBar() +{ + datetime currentBarTime = iTime(NULL, 0, 0); + + if(currentBarTime != g_lastBarTime) + { + g_lastBarTime = currentBarTime; + return true; + } + + return false; +} + +//+------------------------------------------------------------------+ +//| Indicator helpers | +//+------------------------------------------------------------------+ +double GetRSI(int shift) +{ + return iRSI(NULL, 0, RSIPeriod, RSIPrice, shift); +} + +double GetMA(int shift) +{ + return iMA(NULL, 0, MAPeriod, 0, MAMethod, MAPrice, shift); +} + +double GetATRInPips(int shift) +{ + double atr = iATR(NULL, 0, ATRPeriod, shift); + return atr / g_pip; +} + +//+------------------------------------------------------------------+ +//| Filters | +//+------------------------------------------------------------------+ +bool TrendFilterBuyPassed() +{ + if(!UseTrendFilter) + return true; + + return (Close[1] >= GetMA(1)); +} + +bool TrendFilterSellPassed() +{ + if(!UseTrendFilter) + return true; + + return (Close[1] <= GetMA(1)); +} + +bool ATRFilterPassed() +{ + if(!UseATRFilter) + return true; + + return (GetATRInPips(1) >= MinATRValuePips); +} + +//+------------------------------------------------------------------+ +//| RSI signal logic | +//+------------------------------------------------------------------+ +bool IsOversoldSignal() +{ + double rsiPrev = GetRSI(2); + double rsiCurr = GetRSI(1); + + if(UseClosedCandleSignal) + return (rsiPrev <= RSIBuyLevel && rsiCurr > RSIBuyLevel); + + return (GetRSI(0) <= RSIBuyLevel); +} + +bool IsOverboughtSignal() +{ + double rsiPrev = GetRSI(2); + double rsiCurr = GetRSI(1); + + if(UseClosedCandleSignal) + return (rsiPrev >= RSISellLevel && rsiCurr < RSISellLevel); + + return (GetRSI(0) >= RSISellLevel); +} + +//+------------------------------------------------------------------+ +//| Demo wrappers | +//+------------------------------------------------------------------+ +bool BuySignal() +{ + if(!ATRFilterPassed()) + return false; + + if(!TrendFilterBuyPassed()) + return false; + + return IsOversoldSignal(); +} + +bool SellSignal() +{ + if(!ATRFilterPassed()) + return false; + + if(!TrendFilterSellPassed()) + return false; + + return IsOverboughtSignal(); +} + +//+------------------------------------------------------------------+ +//| Expert tick | +//+------------------------------------------------------------------+ +void OnTick() +{ + if(!IsNewBar()) + return; + + if(BuySignal()) + { + Comment("Demo Signal: BUY RSI mean reversion detected"); + return; + } + + if(SellSignal()) + { + Comment("Demo Signal: SELL RSI mean reversion detected"); + return; + } + + Comment("Demo Signal: No valid RSI mean reversion setup"); +} \ No newline at end of file diff --git a/43-RSI-Mean-Reversion/RSI-Mean-Reversion.pdf b/43-RSI-Mean-Reversion/RSI-Mean-Reversion.pdf new file mode 100644 index 0000000..7843344 Binary files /dev/null and b/43-RSI-Mean-Reversion/RSI-Mean-Reversion.pdf differ diff --git a/44-Bollinger-Pullback/Bollinger-Pullback.mq4 b/44-Bollinger-Pullback/Bollinger-Pullback.mq4 new file mode 100644 index 0000000..9f153cd Binary files /dev/null and b/44-Bollinger-Pullback/Bollinger-Pullback.mq4 differ diff --git a/44-Bollinger-Pullback/Bollinger-Pullback.pdf b/44-Bollinger-Pullback/Bollinger-Pullback.pdf new file mode 100644 index 0000000..e3c78af Binary files /dev/null and b/44-Bollinger-Pullback/Bollinger-Pullback.pdf differ diff --git a/45-Keltner-Pullback/Keltner-Pullback.mq4 b/45-Keltner-Pullback/Keltner-Pullback.mq4 new file mode 100644 index 0000000..7b3ccc9 Binary files /dev/null and b/45-Keltner-Pullback/Keltner-Pullback.mq4 differ diff --git a/45-Keltner-Pullback/Keltner-Pullback.pdf b/45-Keltner-Pullback/Keltner-Pullback.pdf new file mode 100644 index 0000000..1393fb6 Binary files /dev/null and b/45-Keltner-Pullback/Keltner-Pullback.pdf differ diff --git a/46-News-Trader/News-Trader.mq4 b/46-News-Trader/News-Trader.mq4 new file mode 100644 index 0000000..83e1ac2 --- /dev/null +++ b/46-News-Trader/News-Trader.mq4 @@ -0,0 +1,741 @@ +//+------------------------------------------------------------------+ +//| NewsTrader.mq4 | +//| Copyright © 2024, EarnForex.com | +//| https://www.earnforex.com/ | +//+------------------------------------------------------------------+ +#property copyright "Copyright © 2024, EarnForex" +#property link "https://www.earnforex.com/metatrader-expert-advisors/News-Trader/" +#property version "1.12" +#property strict + +#property description "Opens a buy/sell trade (random, chosen direction, or both directions) seconds before news release." +#property description "Sets SL and TP. Keeps updating them until the very release." +#property description "Can set use trailing stop and breakeven." +#property description "ATR-based stop-loss option is also available." +#property description "Closes trade after one hour." + +enum dir_enum +{ + Buy, + Sell, + Both, + Random +}; + +enum trailing_enum +{ + None, + Breakeven, + Normal, // Normal trailing stop + NormalPlusBE // Normal trailing stop + Breakeven +}; + +input group "Trading" +input datetime NewsTime = -1; // News date/time (Server) +input int StopLoss = 15; // Stop-loss in points +input int TakeProfit = 75; // Take-profit in points +input dir_enum Direction = Both; // Direction of the trade to open +input trailing_enum TrailingStop = None; // Trailing stop type +input int BEOnProfit = 0; // Profit to trigger breakeven, points +input int BEExtraProfit = 0; // Extra profit for breakeven, points +input int TSOnProfit = 0; // Profit to start trailing stop, points +input bool PreAdjustSLTP = false; // Preadjust SL/TP until news is out +input int SecondsBefore = 18; // Seconds before the news to open a trade +input int CloseAfterSeconds = 3600; // Close trade X seconds after the news, 0 - turn the feature off +input bool SpreadFuse = true; // SpreadFuse - prevent trading if spread >= stop-loss +input group "ATR" +input bool UseATR = false; // Use ATR-based stop-loss and take-profit levels +input int ATR_Period = 14; // ATR Period +input double ATR_Multiplier_SL = 1; // ATR multiplier for SL +input double ATR_Multiplier_TP = 5; // ATR multiplier for TP +input group "Money management" +input double Lots = 0.01; +input bool MM = true; // Money Management, if true - position sizing based on stop-loss +input double Risk = 1; // Risk - Risk tolerance in percentage points +input double FixedBalance = 0; // FixedBalance: If > 0, trade size calc. uses it as balance +input double MoneyRisk = 0; // MoneyRisk: Risk tolerance in account currency +input bool UseMoneyInsteadOfPercentage = false; // Use money risk instead of percentage +input bool UseEquityInsteadOfBalance = false; // Use equity instead of balance +input group "Timer" +input bool ShowTimer = true; // Show timer before and after news +input int FontSize = 18; +input string Font = "Arial"; +input color FontColor = clrRed; +input ENUM_BASE_CORNER Corner = CORNER_LEFT_UPPER; +input int X_Distance = 10; // X-axis distance from the chart corner +input int Y_Distance = 130; // Y-axis distance from the chart corner +input group "Miscellaneous" +input int Slippage = 3; +input int Magic = 794823491; +input string Commentary = "NewsTrader"; // Comment - trade description (e.g. "US CPI", "EU GDP", etc.) +input bool IgnoreECNMode = true; // IgnoreECNMode: Always attach SL/TP immediately + +// Global variables: +bool HaveLongPosition, HaveShortPosition; +bool ECN_Mode; + +int news_time; +bool CanTrade = false; +bool Terminal_Trade_Allowed = true; + +double SL, TP; + +// For tick value adjustment: +string ProfitCurrency = "", account_currency = "", BaseCurrency = "", ReferenceSymbol = NULL, AdditionalReferenceSymbol = NULL; +bool ReferenceSymbolMode, AdditionalReferenceSymbolMode; +int ProfitCalcMode; + +void OnInit() +{ + news_time = (int)NewsTime; + double min_lot = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN); + if ((Lots < min_lot) && (!MM)) + { + double lot_step = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_STEP); + int LotStep_digits = CountDecimalPlaces(lot_step); + Print("Minimum lot: ", DoubleToString(min_lot, LotStep_digits), ", lot step: ", DoubleToString(lot_step, LotStep_digits), "."); + Alert("Lots should be not less than: ", DoubleToString(min_lot, LotStep_digits), "."); + } + else CanTrade = true; + + if (ShowTimer) + { + ObjectCreate(ChartID(), "NewsTraderTimer", OBJ_LABEL, 0, 0, 0); + ObjectSetInteger(ChartID(), "NewsTraderTimer", OBJPROP_CORNER, Corner); + ObjectSetInteger(ChartID(), "NewsTraderTimer", OBJPROP_XDISTANCE, X_Distance); + ObjectSetInteger(ChartID(), "NewsTraderTimer", OBJPROP_YDISTANCE, Y_Distance); + ObjectSetInteger(ChartID(), "NewsTraderTimer", OBJPROP_SELECTABLE, true); + EventSetMillisecondTimer(100); // For smooth updates. + } + + // If UseATR = false, these values will be used. Otherwise, ATR values will be calculated later. + SL = StopLoss; + TP = TakeProfit; + + if (BEExtraProfit > BEOnProfit) Print("Extra profit for breakeven shouldn't be greater than the profit to trigger breakeven parameter. Please check your input parameters."); +} + +//+------------------------------------------------------------------+ +//| Deletes graphical object if needed. | +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) +{ + ObjectDelete(ChartID(), "NewsTraderTimer"); +} + +//+------------------------------------------------------------------+ +//| Updates text about time left to news or passed after news. | +//+------------------------------------------------------------------+ +void OnTimer() +{ + DoTrading(); + string text; + int difference = (int)TimeCurrent() - news_time; + if (difference <= 0) text = "Time to news: " + TimeDistance(-difference); + else text = "Time after news: " + TimeDistance(difference) + "."; + ObjectSetString(ChartID(), "NewsTraderTimer", OBJPROP_TEXT, text); + ObjectSetString(ChartID(), "NewsTraderTimer", OBJPROP_FONT, Font); + ObjectSetInteger(ChartID(), "NewsTraderTimer", OBJPROP_FONTSIZE, FontSize); + ObjectSetInteger(ChartID(), "NewsTraderTimer", OBJPROP_COLOR, FontColor); +} + +//+------------------------------------------------------------------+ +//| Format time distance from the number of seconds to normal string | +//| of years, days, hours, minutes, and seconds. | +//| t - number of seconds | +//| Returns: formatted string. | +//+------------------------------------------------------------------+ +string TimeDistance(int t) +{ + if (t == 0) return "0 seconds"; + string s = ""; + int y = 0; + int d = 0; + int h = 0; + int m = 0; + + y = t / 31536000; + t -= y * 31536000; + + d = t / 86400; + t -= d * 86400; + + h = t / 3600; + t -= h * 3600; + + m = t / 60; + t -= m * 60; + + if (y) s += IntegerToString(y) + " year"; + if (y > 1) s += "s"; + + if (d) s += " " + IntegerToString(d) + " day"; + if (d > 1) s += "s"; + + if (h) s += " " + IntegerToString(h) + " hour"; + if (h > 1) s += "s"; + + if (m) s += " " + IntegerToString(m) + " minute"; + if (m > 1) s += "s"; + + if (t) s += " " + IntegerToString(t) + " second"; + if (t > 1) s += "s"; + + return StringTrimLeft(s); +} + +void OnTick() +{ + DoTrading(); +} + +//+------------------------------------------------------------------+ +//| Main execution procedure. | +//+------------------------------------------------------------------+ +void DoTrading() +{ + if ((TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) == false) || (!CanTrade)) + { + if ((TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) == false) && (Terminal_Trade_Allowed == true)) + { + Print("Trading not allowed."); + Terminal_Trade_Allowed = false; + } + else if ((TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) == true) && (Terminal_Trade_Allowed == false)) + { + Print("Trading allowed."); + Terminal_Trade_Allowed = true; + } + return; + } + + ENUM_SYMBOL_TRADE_EXECUTION Execution_Mode = (ENUM_SYMBOL_TRADE_EXECUTION)SymbolInfoInteger(Symbol(), SYMBOL_TRADE_EXEMODE); + if (Execution_Mode == SYMBOL_TRADE_EXECUTION_MARKET) ECN_Mode = true; + else ECN_Mode = false; + if (IgnoreECNMode) ECN_Mode = false; + + // Do nothing if it is too early. + int time = (int)TimeCurrent(); + if (time < news_time - SecondsBefore) return; + + if (UseATR) + { + // Getting the ATR values + double ATR = iATR(NULL, 0, ATR_Period, 0); + SL = ATR * ATR_Multiplier_SL; + if (SL <= (SymbolInfoInteger(Symbol(), SYMBOL_TRADE_STOPS_LEVEL) + SymbolInfoInteger(Symbol(), SYMBOL_SPREAD)) * Point) SL = (SymbolInfoInteger(Symbol(), SYMBOL_TRADE_STOPS_LEVEL) + SymbolInfoInteger(Symbol(), SYMBOL_SPREAD)) * Point; + TP = ATR * ATR_Multiplier_TP; + if (TP <= (SymbolInfoInteger(Symbol(), SYMBOL_TRADE_STOPS_LEVEL) + SymbolInfoInteger(Symbol(), SYMBOL_SPREAD)) * Point) TP = (SymbolInfoInteger(Symbol(), SYMBOL_TRADE_STOPS_LEVEL) + SymbolInfoInteger(Symbol(), SYMBOL_SPREAD)) * Point; + SL /= Point; + TP /= Point; + } + + // Check what position is currently open. + GetPositionStates(); + + // Adjust SL and TP of the current position. + if ((HaveLongPosition) || (HaveShortPosition)) ControlPosition(); + else + { + // Time to news is less or equal to SecondsBefore but is not negative. + if ((news_time - time <= SecondsBefore) && (news_time > time)) + { + // Prevent position opening when spreads are too wide (bigger than StopLoss input). + int spread = (int)MarketInfo(Symbol(), MODE_SPREAD); + if ((SpreadFuse) && (spread >= StopLoss)) + { + Print(Symbol(), ": Spread fuse prevents positions from opening. Current spread: ", spread, " points."); + return; + } + if (Direction == Buy) fBuy(); + else if (Direction == Sell) fSell(); + else if (Direction == Both) + { + fBuy(); + fSell(); + } + else if (Direction == Random) + { + MathSrand((uint)TimeCurrent()); + if (MathRand() % 2 == 1) fBuy(); + else fSell(); + } + if (ECN_Mode) ControlPosition(); + } + } +} + +//+------------------------------------------------------------------+ +//| Check what positions are currently open. | +//+------------------------------------------------------------------+ +void GetPositionStates() +{ + HaveLongPosition = false; + HaveShortPosition = false; + int total = OrdersTotal(); + for (int cnt = 0; cnt < total; cnt++) + { + if (OrderSelect(cnt, SELECT_BY_POS, MODE_TRADES) == false) continue; + if (OrderMagicNumber() != Magic) continue; + if (OrderSymbol() != Symbol()) continue; + + if (OrderType() == OP_BUY) HaveLongPosition = true; + else if (OrderType() == OP_SELL) HaveShortPosition = true; + } +} + +//+------------------------------------------------------------------+ +//| Add SL/TP, adjust SL/TP, set breakeven, close trade. | +//+------------------------------------------------------------------+ +void ControlPosition() +{ + int total = OrdersTotal(); + for (int cnt = total - 1; cnt >= 0; cnt--) + { + if (OrderSelect(cnt, SELECT_BY_POS, MODE_TRADES) == false) continue; + if (OrderMagicNumber() != Magic) continue; + if (OrderSymbol() != Symbol()) continue; + + if ((OrderType() == OP_BUY) || (OrderType() == OP_SELL)) + { + int time = (int)TimeCurrent(); + + double new_sl, new_tp; + if (SL < MarketInfo(Symbol(), MODE_STOPLEVEL) + MarketInfo(Symbol(), MODE_SPREAD)) SL = MarketInfo(Symbol(), MODE_STOPLEVEL) + MarketInfo(Symbol(), MODE_SPREAD); + if (TP < MarketInfo(Symbol(), MODE_STOPLEVEL) + MarketInfo(Symbol(), MODE_SPREAD)) TP = MarketInfo(Symbol(), MODE_STOPLEVEL) + MarketInfo(Symbol(), MODE_SPREAD); + + if (OrderType() == OP_BUY) + { + RefreshRates(); + new_sl = NormalizeDouble(Ask - SL * Point, Digits); + new_tp = NormalizeDouble(Ask + TP * Point, Digits); + } + else if (OrderType() == OP_SELL) + { + RefreshRates(); + new_sl = NormalizeDouble(Bid + SL * Point, Digits); + new_tp = NormalizeDouble(Bid - TP * Point, Digits); + } + // Need to adjust or add SL/TP. + if (time < news_time) + { + // Adjust only if parameter is set or if in ECN mode and need to assign SL/TP first time. + if ((((new_sl != NormalizeDouble(OrderStopLoss(), Digits)) || (new_tp != NormalizeDouble(OrderTakeProfit(), Digits))) && (PreAdjustSLTP)) || + (((OrderStopLoss() == 0) || (OrderTakeProfit() == 0)) && (ECN_Mode))) + { + Print("Adjusting SL: ", DoubleToString(new_sl, _Digits), " and TP: ", DoubleToString(new_tp, _Digits), "."); + for (int i = 0; i < 10; i++) + { + bool result = OrderModify(OrderTicket(), OrderOpenPrice(), new_sl, new_tp, 0); + if (result) return; + else Print("Error modifying the order: ", GetLastError()); + } + } + } + // Check for breakeven or trade time out. Plus, sometimes, in ECN mode, it is necessary to check if SL/TP was set even after the news. + else + { + RefreshRates(); + // Adjust only if in ECN mode and need to assign SL/TP first time. + if (((OrderStopLoss() == 0) || (OrderTakeProfit() == 0)) && (ECN_Mode)) + { + Print("Adjusting SL: ", DoubleToString(new_sl, _Digits), " and TP: ", DoubleToString(new_tp, _Digits), "."); + for (int i = 0; i < 10; i++) + { + bool result = OrderModify(OrderTicket(), OrderOpenPrice(), new_sl, new_tp, 0); + if (result) return; + else Print("Error modifying the order: ", GetLastError()); + } + } + // Breakeven. + if (((TrailingStop == Breakeven) || (TrailingStop == NormalPlusBE)) && ((((OrderType() == OP_BUY) && (Bid - OrderOpenPrice() >= BEOnProfit * _Point)) || ((OrderType() == OP_SELL) && (OrderOpenPrice() - Ask >= BEOnProfit * _Point))))) + { + new_sl = NormalizeDouble(OrderOpenPrice(), _Digits); + if (BEExtraProfit > 0) // Breakeven extra profit? + { + if (OrderType() == OP_BUY) new_sl += BEExtraProfit * _Point; // For buys. + else new_sl -= BEExtraProfit * _Point; // For sells. + new_sl = NormalizeDouble(new_sl, _Digits); + } + if (((OrderType() == OP_BUY) && (new_sl > OrderStopLoss())) || ((OrderType() == OP_SELL) && ((new_sl < OrderStopLoss()) || (OrderStopLoss() == 0)))) // Avoid moving SL to BE if this SL is already there or in a better position. + { + Print("Moving SL to breakeven: ", new_sl, "."); + for (int i = 0; i < 10; i++) + { + bool result = OrderModify(OrderTicket(), OrderOpenPrice(), new_sl, OrderTakeProfit(), 0); + if (result) break; + else Print("Position modification error: ", GetLastError()); + } + } + } + // Trailing stop. + if (((TrailingStop == Normal) || (TrailingStop == NormalPlusBE)) && ((TSOnProfit == 0) || ((OrderType() == OP_BUY) && (Bid - OrderOpenPrice() >= TSOnProfit * _Point)) || ((OrderType() == OP_SELL) && (OrderOpenPrice() - Ask >= TSOnProfit * _Point)))) + { + if (OrderType() == OP_BUY) new_sl = NormalizeDouble(Bid - SL * _Point, _Digits); + else if (OrderType() == OP_SELL) new_sl = NormalizeDouble(Ask + SL * _Point, _Digits); + if (((OrderType() == OP_BUY) && (new_sl > OrderStopLoss())) || ((OrderType() == OP_SELL) && ((new_sl < OrderStopLoss()) || (OrderStopLoss() == 0)))) // Avoid moving the SL if this SL is already in a better position. + { + Print("Moving trailing SL to ", new_sl, "."); + for (int i = 0; i < 10; i++) + { + bool result = OrderModify(OrderTicket(), OrderOpenPrice(), new_sl, OrderTakeProfit(), 0); + if (result) break; + else Print("Position modification error: ", GetLastError()); + } + } + } + if (CloseAfterSeconds > 0) + { + if (time - news_time >= CloseAfterSeconds) + { + Print("Closing trade by time out."); + double price; + RefreshRates(); + if (OrderType() == OP_BUY) price = Bid; + else if (OrderType() == OP_SELL) price = Ask; + if (!OrderClose(OrderTicket(), OrderLots(), price, Slippage, clrBlue)) + { + Print("OrderClose() failed: ", GetLastError()); + } + } + } + } + } + } +} + +//+------------------------------------------------------------------+ +//| Generic buy. | +//+------------------------------------------------------------------+ +void fBuy() +{ + Print("Opening Buy."); + for (int i = 0; i < 10; i++) + { + double new_sl = 0, new_tp = 0; + double lots = LotsOptimized(OP_BUY); + RefreshRates(); + // Bid and Ask are swapped to preserve the probabilities and decrease/increase profit/loss size. + if (!ECN_Mode) + { + new_sl = NormalizeDouble(Ask - SL * Point, Digits); + new_tp = NormalizeDouble(Ask + TP * Point, Digits); + } + int result = OrderSend(Symbol(), OP_BUY, lots, Ask, Slippage, new_sl, new_tp, Commentary, Magic, 0, clrBlue); + Sleep(1000); + if (result == -1) + { + int e = GetLastError(); + Print("OrderSend Error: ", e, "."); + } + else return; + } +} + +//+------------------------------------------------------------------+ +//| Generic sell. | +//+------------------------------------------------------------------+ +void fSell() +{ + Print("Opening Sell."); + for (int i = 0; i < 10; i++) + { + double new_sl = 0, new_tp = 0; + double lots = LotsOptimized(OP_SELL); + RefreshRates(); + // Bid and Ask are swapped to preserve the probabilities and decrease/increase profit/loss size. + if (!ECN_Mode) + { + new_sl = NormalizeDouble(Bid + SL * Point, Digits); + new_tp = NormalizeDouble(Bid - TP * Point, Digits); + } + int result = OrderSend(Symbol(), OP_SELL, lots, Bid, Slippage, new_sl, new_tp, Commentary, Magic, 0, clrRed); + Sleep(1000); + if (result == -1) + { + int e = GetLastError(); + Print("OrderSend Error: ", e, "."); + } + else return; + } +} + +//+------------------------------------------------------------------+ +//| Calculate position size depending on money management parameters.| +//+------------------------------------------------------------------+ +double LotsOptimized(int dir) +{ + if (!MM) return Lots; + + double Size, RiskMoney, PositionSize = 0, UnitCost; + ProfitCurrency = SymbolInfoString(Symbol(), SYMBOL_CURRENCY_PROFIT); + BaseCurrency = SymbolInfoString(Symbol(), SYMBOL_CURRENCY_BASE); + ProfitCalcMode = (int)MarketInfo(Symbol(), MODE_PROFITCALCMODE); + account_currency = AccountCurrency(); + // A rough patch for cases when account currency is set as RUR instead of RUB. + if (account_currency == "RUR") account_currency = "RUB"; + if (ProfitCurrency == "RUR") ProfitCurrency = "RUB"; + if (BaseCurrency == "RUR") BaseCurrency = "RUB"; + double LotStep = MarketInfo(Symbol(), MODE_LOTSTEP); + int LotStep_digits = CountDecimalPlaces(LotStep); + + if (AccountCurrency() == "") return 0; + + if (FixedBalance > 0) + { + Size = FixedBalance; + } + else if (UseEquityInsteadOfBalance) + { + Size = AccountEquity(); + } + else + { + Size = AccountBalance(); + } + + if (!UseMoneyInsteadOfPercentage) RiskMoney = Size * Risk / 100; + else RiskMoney = MoneyRisk; + + // If Symbol is CFD. + if (ProfitCalcMode == 1) + UnitCost = SymbolInfoDouble(Symbol(), SYMBOL_TRADE_TICK_SIZE) * SymbolInfoDouble(Symbol(), SYMBOL_TRADE_CONTRACT_SIZE); // Apparently, it is more accurate than taking TICKVALUE directly in some cases. + else UnitCost = MarketInfo(Symbol(), MODE_TICKVALUE); // Futures or Forex. + + if (ProfitCalcMode != 0) // Non-Forex might need to be adjusted. + { + // If profit currency is different from account currency. + if (ProfitCurrency != account_currency) + { + double CCC = CalculateAdjustment(); // Valid only for loss calculation. + // Adjust the unit cost. + UnitCost *= CCC; + } + } + + // If account currency == pair's base currency, adjust UnitCost to future rate (SL). Works only for Forex pairs. + if ((account_currency == BaseCurrency) && (ProfitCalcMode == 0)) + { + double current_rate = 1, future_rate = 1; + RefreshRates(); + if (dir == OP_BUY) + { + current_rate = Ask; + future_rate = current_rate - SL * _Point; + } + else if (dir == OP_SELL) + { + current_rate = Bid; + future_rate = current_rate + SL * _Point; + } + if (future_rate == 0) future_rate = _Point; // Zero divide prevention. + UnitCost *= (current_rate / future_rate); + } + + double TickSize = MarketInfo(Symbol(), MODE_TICKSIZE); + + if ((SL != 0) && (UnitCost != 0) && (TickSize != 0)) PositionSize = NormalizeDouble(RiskMoney / (SL * _Point * UnitCost / TickSize), LotStep_digits); + + if (PositionSize < MarketInfo(Symbol(), MODE_MINLOT)) + { + Print("Calculated position size (" + DoubleToString(PositionSize, 2) + ") is less than minimum position size (" + DoubleToString(MarketInfo(Symbol(), MODE_MINLOT), 2) + "). Setting position size to minimum."); + PositionSize = MarketInfo(Symbol(), MODE_MINLOT); + } + else if (PositionSize > MarketInfo(Symbol(), MODE_MAXLOT)) + { + Print("Calculated position size (" + DoubleToString(PositionSize, 2) + ") is greater than maximum position size (" + DoubleToString(MarketInfo(Symbol(), MODE_MAXLOT), 2) + "). Setting position size to maximum."); + PositionSize = MarketInfo(Symbol(), MODE_MAXLOT); + } + + double steps = PositionSize / LotStep; + if (MathFloor(steps) < steps) + { + Print("Calculated position size (" + DoubleToString(PositionSize, 2) + ") uses uneven step size. Allowed step size = " + DoubleToString(MarketInfo(Symbol(), MODE_LOTSTEP), 2) + ". Setting position size to " + DoubleToString(MathFloor(steps) * LotStep, 2) + "."); + PositionSize = MathFloor(steps) * LotStep; + } + + return PositionSize; +} + +//+-----------------------------------------------------------------------------------+ +//| Calculates necessary adjustments for cases when ProfitCurrency != AccountCurrency.| +//+-----------------------------------------------------------------------------------+ +#define FOREX_SYMBOLS_ONLY 0 +#define NONFOREX_SYMBOLS_ONLY 1 +double CalculateAdjustment() +{ + double add_coefficient = 1; // Might be necessary for correction coefficient calculation if two pairs are used for profit currency to account currency conversion. This is handled differently in MT5 version. + if (ReferenceSymbol == NULL) + { + ReferenceSymbol = GetSymbolByCurrencies(ProfitCurrency, account_currency, FOREX_SYMBOLS_ONLY); + if (ReferenceSymbol == NULL) ReferenceSymbol = GetSymbolByCurrencies(ProfitCurrency, account_currency, NONFOREX_SYMBOLS_ONLY); + ReferenceSymbolMode = true; + // Failed. + if (ReferenceSymbol == NULL) + { + // Reversing currencies. + ReferenceSymbol = GetSymbolByCurrencies(account_currency, ProfitCurrency, FOREX_SYMBOLS_ONLY); + if (ReferenceSymbol == NULL) ReferenceSymbol = GetSymbolByCurrencies(account_currency, ProfitCurrency, NONFOREX_SYMBOLS_ONLY); + ReferenceSymbolMode = false; + } + if (ReferenceSymbol == NULL) + { + // The condition checks whether we are caclulating conversion coefficient for the chart's symbol or for some other. + // The error output is OK for the current symbol only because it won't be repeated ad infinitum. + // It should be avoided for non-chart symbols because it will just flood the log. + Print("Couldn't detect proper currency pair for adjustment calculation. Profit currency: ", ProfitCurrency, ". Account currency: ", account_currency, ". Trying to find a possible two-symbol combination."); + if ((FindDoubleReferenceSymbol("USD")) // USD should work in 99.9% of cases. + || (FindDoubleReferenceSymbol("EUR")) // For very rare cases. + || (FindDoubleReferenceSymbol("GBP")) // For extremely rare cases. + || (FindDoubleReferenceSymbol("JPY"))) // For extremely rare cases. + { + Print("Converting via ", ReferenceSymbol, " and ", AdditionalReferenceSymbol, "."); + } + else + { + Print("Adjustment calculation critical failure. Failed both simple and two-pair conversion methods."); + return 1; + } + } + } + if (AdditionalReferenceSymbol != NULL) // If two reference pairs are used. + { + // Calculate just the additional symbol's coefficient and then use it in final return's multiplication. + MqlTick tick; + SymbolInfoTick(AdditionalReferenceSymbol, tick); + add_coefficient = GetCurrencyCorrectionCoefficient(tick, AdditionalReferenceSymbolMode); + } + MqlTick tick; + SymbolInfoTick(ReferenceSymbol, tick); + return GetCurrencyCorrectionCoefficient(tick, ReferenceSymbolMode) * add_coefficient; +} + +//+---------------------------------------------------------------------------+ +//| Returns a currency pair with specified base currency and profit currency. | +//+---------------------------------------------------------------------------+ +string GetSymbolByCurrencies(const string base_currency, const string profit_currency, const uint symbol_type) +{ + // Cycle through all symbols. + for (int s = 0; s < SymbolsTotal(false); s++) + { + // Get symbol name by number. + string symbolname = SymbolName(s, false); + string b_cur; + + // Normal case - Forex pairs: + if (MarketInfo(symbolname, MODE_PROFITCALCMODE) == 0) + { + if (symbol_type == NONFOREX_SYMBOLS_ONLY) continue; // Avoid checking symbols of a wrong type. + // Get its base currency. + b_cur = SymbolInfoString(symbolname, SYMBOL_CURRENCY_BASE); + } + else // Weird case for brokers that set conversion pairs as CFDs. + { + if (symbol_type == FOREX_SYMBOLS_ONLY) continue; // Avoid checking symbols of a wrong type. + // Get its base currency as the initial three letters - prone to huge errors! + b_cur = StringSubstr(symbolname, 0, 3); + } + + // Get its profit currency. + string p_cur = SymbolInfoString(symbolname, SYMBOL_CURRENCY_PROFIT); + + // If the currency pair matches both currencies, select it in Market Watch and return its name. + if ((b_cur == base_currency) && (p_cur == profit_currency)) + { + // Select if necessary. + if (!(bool)SymbolInfoInteger(symbolname, SYMBOL_SELECT)) SymbolSelect(symbolname, true); + + return symbolname; + } + } + return NULL; +} + +//+----------------------------------------------------------------------------+ +//| Finds reference symbols using 2-pair method. | +//| Results are returned via reference parameters. | +//| Returns true if found the pairs, false otherwise. | +//+----------------------------------------------------------------------------+ +bool FindDoubleReferenceSymbol(const string cross_currency) +{ + // A hypothetical example for better understanding: + // The trader buys CAD/CHF. + // account_currency is known = SEK. + // cross_currency = USD. + // profit_currency = CHF. + // I.e., we have to buy dollars with francs (using the Ask price) and then sell those for SEKs (using the Bid price). + + ReferenceSymbol = GetSymbolByCurrencies(cross_currency, account_currency, FOREX_SYMBOLS_ONLY); + if (ReferenceSymbol == NULL) ReferenceSymbol = GetSymbolByCurrencies(cross_currency, account_currency, NONFOREX_SYMBOLS_ONLY); + ReferenceSymbolMode = true; // If found, we've got USD/SEK. + + // Failed. + if (ReferenceSymbol == NULL) + { + // Reversing currencies. + ReferenceSymbol = GetSymbolByCurrencies(account_currency, cross_currency, FOREX_SYMBOLS_ONLY); + if (ReferenceSymbol == NULL) ReferenceSymbol = GetSymbolByCurrencies(account_currency, cross_currency, NONFOREX_SYMBOLS_ONLY); + ReferenceSymbolMode = false; // If found, we've got SEK/USD. + } + if (ReferenceSymbol == NULL) + { + Print("Error. Couldn't detect proper currency pair for 2-pair adjustment calculation. Cross currency: ", cross_currency, ". Account currency: ", account_currency, "."); + return false; + } + + AdditionalReferenceSymbol = GetSymbolByCurrencies(cross_currency, ProfitCurrency, FOREX_SYMBOLS_ONLY); + if (AdditionalReferenceSymbol == NULL) AdditionalReferenceSymbol = GetSymbolByCurrencies(cross_currency, ProfitCurrency, NONFOREX_SYMBOLS_ONLY); + AdditionalReferenceSymbolMode = false; // If found, we've got USD/CHF. Notice that mode is swapped for cross/profit compared to cross/acc, because it is used in the opposite way. + + // Failed. + if (AdditionalReferenceSymbol == NULL) + { + // Reversing currencies. + AdditionalReferenceSymbol = GetSymbolByCurrencies(ProfitCurrency, cross_currency, FOREX_SYMBOLS_ONLY); + if (AdditionalReferenceSymbol == NULL) AdditionalReferenceSymbol = GetSymbolByCurrencies(ProfitCurrency, cross_currency, NONFOREX_SYMBOLS_ONLY); + AdditionalReferenceSymbolMode = true; // If found, we've got CHF/USD. Notice that mode is swapped for profit/cross compared to acc/cross, because it is used in the opposite way. + } + if (AdditionalReferenceSymbol == NULL) + { + Print("Error. Couldn't detect proper currency pair for 2-pair adjustment calculation. Cross currency: ", cross_currency, ". Chart's pair currency: ", ProfitCurrency, "."); + return false; + } + + return true; +} + +//+------------------------------------------------------------------+ +//| Get profit correction coefficient based on current prices. | +//| Valid for loss calculation only. | +//+------------------------------------------------------------------+ +double GetCurrencyCorrectionCoefficient(MqlTick &tick, const bool ref_symbol_mode) +{ + if ((tick.ask == 0) || (tick.bid == 0)) return -1; // Data is not yet ready. + // Reverse quote. + if (ref_symbol_mode) + { + // Using Buy price for reverse quote. + return tick.ask; + } + // Direct quote. + else + { + // Using Sell price for direct quote. + return (1 / tick.bid); + } +} + +//+------------------------------------------------------------------+ +//| Counts decimal places. | +//+------------------------------------------------------------------+ +int CountDecimalPlaces(double number) +{ + // 100 as maximum length of number. + for (int i = 0; i < 100; i++) + { + double pwr = MathPow(10, i); + if (MathRound(number * pwr) / pwr == number) return i; + } + return -1; +} +//+------------------------------------------------------------------+ \ No newline at end of file diff --git a/46-News-Trader/News-Trader.pdf b/46-News-Trader/News-Trader.pdf new file mode 100644 index 0000000..7a76ad0 Binary files /dev/null and b/46-News-Trader/News-Trader.pdf differ diff --git a/47-Amazing-News/Amazing-News.mq4 b/47-Amazing-News/Amazing-News.mq4 new file mode 100644 index 0000000..c535d91 --- /dev/null +++ b/47-Amazing-News/Amazing-News.mq4 @@ -0,0 +1,728 @@ +//+------------------------------------------------------------------+ +//| Amazing | +//| Copyright © 2023, EarnForex.com | +//| https://www.earnforex.com/ | +//| Based on the EA by FiFtHeLeMeNt. | +//+------------------------------------------------------------------+ +#property copyright "Copyright © 2023, EarnForex" +#property link "https://www.earnforex.com/metatrader-expert-advisors/Amazing/" +#property version "1.04" +#property strict + +#property description "Amazing - EA that helps to trade on news." +#property description "Set the NewsDateTime input parameter to the actual date and time of the news." +#property description "EA will set up the pending orders (buy and sell) to be triggered by the news." +#property description "It will use a profit target, breakeven, and trailing stop to manage it." + +#include + +input group "Main" +input datetime NewsDateTime = __DATE__; // NewsDateTime: Date and time of the news release. +input int EntryDistance = 100; // EntryDistance: Entry distance from recent high/low in points. +input int StopLoss = 200; // StopLoss: Stop-loss in points. +input int TakeProfit = 200; // TakeProfit: Take-profit in points. +input int CTCBN = 0; // CTCBN: Number of candles to check before news for High & Low. +input int SecBPO = 300; // SecBPO: Seconds before news to place pending orders. +input int SecBMO = 0; // SecBMO: Seconds before news when to stop modifying orders. +input int STWAN = 150; // STWAN: Seconds to wait after news to delete pending orders. +input bool OCO = true; // OCO: EA will cancel the other pending order if one is hit. +input int BEPoints = 0; // BEPoints: Points of profit when EA will move SL to breakeven + 1. +input int TrailingStop = 0; // Trailing Stop in points +input group "ATR" +input bool UseATR = false; // Use ATR-based stop-loss and take-profit levels. +input int ATR_Period = 14; // ATR Period. +input double ATR_Multiplier_SL = 5; // ATR multiplier for SL. +input double ATR_Multiplier_TP = 5; // ATR multiplier for TP. +input group "Money management" +input double Lots = 0.01; +input bool MM = true; // Money Management, if true - position sizing based on stop-loss. +input double Risk = 1; // Risk - Risk tolerance in percentage points. +input double FixedBalance = 0; // FixedBalance: If > 0, trade size calc. uses it as balance. +input double MoneyRisk = 0; // MoneyRisk: Risk tolerance in account currency. +input bool UseMoneyInsteadOfPercentage = false; // Use money risk instead of percentage. +input bool UseEquityInsteadOfBalance = false; // Use equity instead of balance. +input group "Miscellaneous" +input string TradeLog = "Am_Log_"; // TradeLog: Log file prefix. +input string Commentary = "Amazing"; // Commentary: trade description. + +// Global variables: +double buy_stop_entry, sell_stop_entry, buy_stop_loss, sell_stop_loss, buy_take_profit, sell_take_profit; +int Magic; +string filename; + +double SL, TP; +double RiskMoney; + +// For tick value adjustment: +string ProfitCurrency = "", account_currency = "", BaseCurrency = "", ReferenceSymbol = NULL, AdditionalReferenceSymbol = NULL; +bool ReferenceSymbolMode, AdditionalReferenceSymbolMode; +int ProfitCalcMode; + +void OnInit() +{ + Magic = (int)NewsDateTime; // Dynamically generated Magic number to allow multiple instances for different news announcements. + + double min_lot = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN); + double lot_step = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_STEP); + Print("Minimum lot: ", DoubleToString(min_lot, 2), ", lot step: ", DoubleToString(lot_step, 2), "."); + if ((Lots < min_lot) && (!MM)) Alert("Lots should be not less than: ", DoubleToString(min_lot, 2), "."); + + // If UseATR = false, these values will be used. Otherwise, ATR values will be calculated later. + SL = StopLoss; + TP = TakeProfit; + + if (StringLen(Commentary) > 0) filename = TradeLog + Symbol() + "-" + IntegerToString(Month()) + "-" + IntegerToString(Day()) + ".txt"; + else filename = ""; // Turning logging off. +} + +void OnDeinit(const int reason) +{ + Comment(""); +} + +// Result Pattern +// 1 1 1 1 +// | | | | +// | | | -------- Sell Stop Order +// | | --------Buy Stop Order +// | --------Sell Position +// --------Buy Position +int CheckOrdersCondition() +{ + int result = 0; + + for (int i = 0; i < OrdersTotal(); i++) + { + if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) + { + Write(__FUNCTION__ + " | Error selecting an order: " + ErrorDescription(GetLastError())); + continue; + } + if ((OrderSymbol() != Symbol()) || (OrderMagicNumber() != Magic)) continue; + + if (OrderType() == OP_BUY) + { + result = result + 1000; + } + else if (OrderType() == OP_SELL) + { + result = result + 100; + } + else if (OrderType() == OP_BUYSTOP) + { + result = result + 10; + } + else if (OrderType() == OP_SELLSTOP) + { + result = result + 1; + } + } + + return result; // 0 means there are no trades. +} + +void OpenBuyStop() +{ + for (int tries = 0; tries < 10; tries++) + { + int ticket = OrderSend(Symbol(), OP_BUYSTOP, LotsOptimized(OP_BUY, buy_stop_entry), buy_stop_entry, 0, buy_stop_loss, buy_take_profit, Commentary, Magic); + if (ticket < 0) + { + Write("Error in OrderSend: " + ErrorDescription(GetLastError()) + " Buy Stop @ " + DoubleToString(buy_stop_entry, _Digits) + " SL @ " + DoubleToString(buy_stop_loss, _Digits) + " TP @" + DoubleToString(buy_take_profit, _Digits)); + } + else + { + Write("Open Buy Stop: OrderSend executed. Ticket = " + IntegerToString(ticket)); + break; + } + } +} + +void OpenSellStop() +{ + for (int tries = 0; tries < 10; tries++) + { + int ticket = OrderSend(Symbol(), OP_SELLSTOP, LotsOptimized(OP_SELL, sell_stop_entry), sell_stop_entry, 0, sell_stop_loss, sell_take_profit, Commentary, Magic); + if (ticket < 0) + { + Write("Error in OrderSend: " + ErrorDescription(GetLastError()) + " Sell Stop @ " + DoubleToString(sell_stop_entry, _Digits) + " SL @ " + DoubleToString(sell_stop_loss, _Digits) + " TP @" + DoubleToString(sell_take_profit, _Digits)); + } + else + { + Write("Open Sell Stop: OrderSend executed. Ticket = " + IntegerToString(ticket)); + break; + } + } +} + +// Set breakeven on positions if needed. +void DoBE(int byPoints) +{ + for (int i = 0; i < OrdersTotal(); i++) + { + if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) + { + Write(__FUNCTION__ + " | Error selecting an order: " + ErrorDescription(GetLastError())); + continue; + } + if ((OrderSymbol() != Symbol()) || (OrderMagicNumber() != Magic)) continue; + + if ((OrderType() == OP_BUY) && (NormalizeDouble(Bid - OrderOpenPrice(), _Digits) > NormalizeDouble(byPoints * _Point, _Digits)) && (OrderStopLoss() < OrderOpenPrice())) + { + Write("Moving stop-loss of Buy order to breakeven + 1 point."); + if (!OrderModify(OrderTicket(), OrderOpenPrice(), OrderOpenPrice() + _Point, OrderTakeProfit(), OrderExpiration())) + { + Write(__FUNCTION__ + " | Error modifying Buy: " + ErrorDescription(GetLastError())); + } + } + else if ((OrderType() == OP_SELL) && (NormalizeDouble(OrderOpenPrice() - Ask, _Digits) > NormalizeDouble(byPoints * _Point, _Digits)) && (OrderStopLoss() > OrderOpenPrice())) + { + Write("Moving stop-loss of Sell order to breakeven - 1 point."); + if (!OrderModify(OrderTicket(), OrderOpenPrice(), OrderOpenPrice() - _Point, OrderTakeProfit(), OrderExpiration())) + { + Write(__FUNCTION__ + " | Error modifying Sell: " + ErrorDescription(GetLastError())); + } + } + } +} + +// Trailing stop for open positions. +void DoTrail() +{ + for (int i = 0; i < OrdersTotal(); i++) + { + if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) + { + Write(__FUNCTION__ + " | Error selecting an order: " + ErrorDescription(GetLastError())); + continue; + } + if ((OrderSymbol() != Symbol()) || (OrderMagicNumber() != Magic)) continue; + + if (OrderType() == OP_BUY) + { + if (Bid - OrderOpenPrice() > _Point * TrailingStop) + { + if (OrderStopLoss() < NormalizeDouble(Bid - _Point * TrailingStop, _Digits)) + { + if (!OrderModify(OrderTicket(), OrderOpenPrice(), Bid - _Point * TrailingStop, OrderTakeProfit(), OrderExpiration())) + { + Write(__FUNCTION__ + " | Error modifying Buy: " + ErrorDescription(GetLastError())); + } + } + } + } + else if (OrderType() == OP_SELL) + { + if (OrderOpenPrice() - Ask > _Point * TrailingStop) + { + if ((OrderStopLoss() > NormalizeDouble(Ask + _Point * TrailingStop, _Digits)) || (OrderStopLoss() == 0)) + { + if (!OrderModify(OrderTicket(), OrderOpenPrice(), Ask + _Point * TrailingStop, OrderTakeProfit(), OrderExpiration())) + { + Write(__FUNCTION__ + " | Error modifying Sell: " + ErrorDescription(GetLastError())); + } + } + } + } + } +} + +void DeleteBuyStop() +{ + for (int i = 0; i < OrdersTotal(); i++) // The order of cycle doesn't matter as only one order will be deleted. + { + if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) + { + Write(__FUNCTION__ + " | Error selecting an order: " + ErrorDescription(GetLastError())); + continue; + } + if ((OrderSymbol() != Symbol()) || (OrderMagicNumber() != Magic)) continue; + + if (OrderType() == OP_BUYSTOP) + { + if (!OrderDelete(OrderTicket())) + { + Write("Error deleting Buy Stop: " + ErrorDescription(GetLastError())); + } + else Write("Buy Stop order deleted."); + return; + } + } +} + +void DeleteSellStop() +{ + for (int i = 0; i < OrdersTotal(); i++) + { + if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) + { + Write(__FUNCTION__ + " | Error selecting an order: " + ErrorDescription(GetLastError())); + continue; + } + if ((OrderSymbol() != Symbol()) || (OrderMagicNumber() != Magic)) continue; + + if (OrderType() == OP_SELLSTOP) + { + if (!OrderDelete(OrderTicket())) + { + Write("Error deleting Sell Stop: " + ErrorDescription(GetLastError())); + } + else Write("Sell Stop order deleted."); + return; + } + } +} + +// Update pending stop orders according to new price levels. +void DoModify() +{ + for (int i = 0; i < OrdersTotal(); i++) + { + if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) + { + Write(__FUNCTION__ + " | Error selecting an order: " + ErrorDescription(GetLastError())); + continue; + } + if ((OrderSymbol() != Symbol()) || (OrderMagicNumber() != Magic)) continue; + + if (OrderType() == OP_BUYSTOP) + { + if (OrderOpenPrice() != buy_stop_entry) + { + if (!OrderModify(OrderTicket(), buy_stop_entry, buy_stop_loss, buy_take_profit, OrderExpiration())) + { + Write(__FUNCTION__ + " | Error modifying Buy Stop: " + ErrorDescription(GetLastError())); + } + else Write("Buy Stop OrderModify executed: " + DoubleToString(OrderOpenPrice(), _Digits) + " -> " + DoubleToString(buy_stop_entry, _Digits)); + } + } + + if (OrderType() == OP_SELLSTOP) + { + if (OrderOpenPrice() != sell_stop_entry) + { + if (!OrderModify(OrderTicket(), sell_stop_entry, sell_stop_loss, sell_take_profit, OrderExpiration())) + { + Write(__FUNCTION__ + " | Error modifying Sell Stop: " + ErrorDescription(GetLastError())); + } + else Write("Sell Stop OrderModify executed: " + DoubleToString(OrderOpenPrice(), _Digits) + " -> " + DoubleToString(sell_stop_entry, _Digits)); + } + } + } +} + +// Prints a string and writes it to a log file too. +void Write(string str) +{ + Print(str); + + if (filename == "") return; + + int handle = FileOpen(filename, FILE_READ | FILE_WRITE | FILE_TXT); + if (handle == INVALID_HANDLE) + { + Print("Error opening file ", filename, ": ", ErrorDescription(GetLastError())); + return; + } + FileSeek(handle, 0, SEEK_END); + FileWrite(handle, str + " Time " + TimeToStr(CurTime(), TIME_DATE | TIME_SECONDS)); + FileClose(handle); +} + +void OnTick() +{ + if (BEPoints > 0) DoBE(BEPoints); + if (TrailingStop > 0) DoTrail(); + + int OrdersCondition = CheckOrdersCondition(); + + // Find recent High/Low for pre-news orders. + double recent_high = iHigh(NULL, PERIOD_M1, 0); + double recent_low = iLow(NULL, PERIOD_M1, 0); + for (int i = 1; i <= CTCBN; i++) + { + if (iHigh(NULL, PERIOD_M1, i) > recent_high) recent_high = iHigh(NULL, PERIOD_M1, i); + if (iLow(NULL, PERIOD_M1, i) < recent_low) recent_low = iLow(NULL, PERIOD_M1, i); + } + + double spread = Ask - Bid; + buy_stop_entry = NormalizeDouble(recent_high + spread + EntryDistance * _Point, _Digits); + sell_stop_entry = NormalizeDouble(recent_low - EntryDistance * _Point, _Digits); + + if (UseATR) + { + // Getting the ATR values + double ATR = iATR(NULL, 0, ATR_Period, 0); + SL = ATR * ATR_Multiplier_SL; + if (SL <= (SymbolInfoInteger(Symbol(), SYMBOL_TRADE_STOPS_LEVEL) + SymbolInfoInteger(Symbol(), SYMBOL_SPREAD)) * Point) SL = (SymbolInfoInteger(Symbol(), SYMBOL_TRADE_STOPS_LEVEL) + SymbolInfoInteger(Symbol(), SYMBOL_SPREAD)) * Point; + TP = ATR * ATR_Multiplier_TP; + if (TP <= (SymbolInfoInteger(Symbol(), SYMBOL_TRADE_STOPS_LEVEL) + SymbolInfoInteger(Symbol(), SYMBOL_SPREAD)) * Point) TP = (SymbolInfoInteger(Symbol(), SYMBOL_TRADE_STOPS_LEVEL) + SymbolInfoInteger(Symbol(), SYMBOL_SPREAD)) * Point; + SL /= Point; + TP /= Point; + } + + buy_stop_loss = NormalizeDouble(buy_stop_entry - SL * _Point, _Digits); + sell_stop_loss = NormalizeDouble(sell_stop_entry + SL * _Point, _Digits); + buy_take_profit = NormalizeDouble(buy_stop_entry + TP * _Point, _Digits); + sell_take_profit= NormalizeDouble(sell_stop_entry - TP * _Point, _Digits); + + int sectonews = (int)(NewsDateTime - TimeCurrent()); + Comment("\nAmazing Expert Advisor", + "\nHigh @ ", recent_high, " Buy Order @ ", buy_stop_entry, " Stop-loss @ ", buy_stop_loss, " Take-profit @ ", buy_take_profit, + "\nLow @ ", recent_low, " Sell Order @ ", sell_stop_entry, " Stop-loss @ ", sell_stop_loss, " Take-profit @ ", sell_take_profit, + "\nNews time: ", TimeToString(NewsDateTime), + "\nCurrent time: ", TimeToString(TimeCurrent()), + "\nSeconds left to news: ", IntegerToString(sectonews), + "\nCTCBN: ", CTCBN, " SecBPO: ", SecBPO, " SecBMO: ", SecBMO, " STWAN: ", STWAN, " OCO: ", OCO, " BEPips: ", BEPoints, + "\nMoney management: ", MM, " Risk: ", DoubleToString(RiskMoney, 2), " ", AccountCurrency(), " Lots (B/S): ", DoubleToString(LotsOptimized(OP_BUY, buy_stop_entry), 2), "/", DoubleToString(LotsOptimized(OP_SELL, sell_stop_entry), 2)); + + // Before the news, but after the time when orders have to be placed. + if ((TimeCurrent() < NewsDateTime) && (TimeCurrent() >= NewsDateTime - SecBPO)) + { + if (OrdersCondition == 0) // No orders. + { + Write("Opening Buy Stop and Sell Stop. OrdersCondition = " + IntegerToString(OrdersCondition) + " Timestamp = " + TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS) + "."); + OpenBuyStop(); + OpenSellStop(); + } + else if (OrdersCondition == 10) + { + Write("Opening Sell Stop. OrdersCondition = " + IntegerToString(OrdersCondition) + " Timestamp = " + TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS) + "."); + OpenSellStop(); + } + else if (OrdersCondition == 1) + { + Write("Opening Buy Stop. OrdersCondition = " + IntegerToString(OrdersCondition) + " Timestamp = " + TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS) + "."); + OpenBuyStop(); + } + } + + // Still have time to modify the orders. + if ((TimeCurrent() < NewsDateTime) && (TimeCurrent() >= NewsDateTime - SecBPO) && (TimeCurrent() < NewsDateTime - SecBMO)) + { + Write("Modifying orders. OrdersCondition = " + IntegerToString(OrdersCondition) + " Timestamp = " + TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS) + "."); + DoModify(); + } + + // News announcement already happened, but it is too early to delete all untriggered orders, yet the EA has to delete the untriggered one due to OCO if the opposite was hit. + if ((TimeCurrent() > NewsDateTime) && (TimeCurrent() < NewsDateTime + STWAN) && (OCO)) + { + if (OrdersCondition == 1001) + { + Write("Deleting Sell Stop because Buy Stop was hit. OrdersCondition = " + IntegerToString(OrdersCondition) + " Timestamp = " + TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS) + "."); + DeleteSellStop(); + } + else if (OrdersCondition == 110) + { + Write("Deleting Buy Stop because Sell Stop was hit. OrdersCondition=" + IntegerToString(OrdersCondition) + " Timestamp=" + TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS) + "."); + DeleteBuyStop(); + } + } + + // News has passed and it is time to delete untriggered orders. + if ((TimeCurrent() > NewsDateTime) && (TimeCurrent() > NewsDateTime + STWAN)) + { + if (OrdersCondition == 11) + { + Write("Deleting Buy Stop and Sell Stop because time expired. OrdersCondition = " + IntegerToString(OrdersCondition) + " Timestamp=" + TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS) + "."); + DeleteBuyStop(); + DeleteSellStop(); + } + + if ((OrdersCondition == 10) || (OrdersCondition == 110)) + { + Write("Deleting BuyStop Because expired, OrdersCondition=" + IntegerToString(OrdersCondition) + " Timestamp = " + TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS) + "."); + DeleteBuyStop(); + } + + if ((OrdersCondition == 1) || (OrdersCondition == 1001)) + { + Write("Deleting SellStop Because expired, OrdersCondition=" + IntegerToString(OrdersCondition) + " Timestamp = " + TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS) + "."); + DeleteSellStop(); + } + } +} + +//+------------------------------------------------------------------+ +//| Calculate position size depending on money management parameters.| +//+------------------------------------------------------------------+ +double LotsOptimized(int dir, double entry) +{ + if (!MM) return Lots; + + double Size, PositionSize = 0, UnitCost; + ProfitCurrency = SymbolInfoString(Symbol(), SYMBOL_CURRENCY_PROFIT); + BaseCurrency = SymbolInfoString(Symbol(), SYMBOL_CURRENCY_BASE); + ProfitCalcMode = (int)MarketInfo(Symbol(), MODE_PROFITCALCMODE); + account_currency = AccountCurrency(); + // A rough patch for cases when account currency is set as RUR instead of RUB. + if (account_currency == "RUR") account_currency = "RUB"; + if (ProfitCurrency == "RUR") ProfitCurrency = "RUB"; + if (BaseCurrency == "RUR") BaseCurrency = "RUB"; + double LotStep = MarketInfo(Symbol(), MODE_LOTSTEP); + int LotStep_digits = CountDecimalPlaces(LotStep); + + if (AccountCurrency() == "") return 0; + + if (FixedBalance > 0) + { + Size = FixedBalance; + } + else if (UseEquityInsteadOfBalance) + { + Size = AccountEquity(); + } + else + { + Size = AccountBalance(); + } + + if (!UseMoneyInsteadOfPercentage) RiskMoney = Size * Risk / 100; + else RiskMoney = MoneyRisk; + + // If Symbol is CFD. + if (ProfitCalcMode == 1) + UnitCost = SymbolInfoDouble(Symbol(), SYMBOL_TRADE_TICK_SIZE) * SymbolInfoDouble(Symbol(), SYMBOL_TRADE_CONTRACT_SIZE); // Apparently, it is more accurate than taking TICKVALUE directly in some cases. + else UnitCost = MarketInfo(Symbol(), MODE_TICKVALUE); // Futures or Forex. + + if (ProfitCalcMode != 0) // Non-Forex might need to be adjusted. + { + // If profit currency is different from account currency. + if (ProfitCurrency != account_currency) + { + double CCC = CalculateAdjustment(); // Valid only for loss calculation. + // Adjust the unit cost. + UnitCost *= CCC; + } + } + + // If account currency == pair's base currency, adjust UnitCost to future rate (SL). Works only for Forex pairs. + if ((account_currency == BaseCurrency) && (ProfitCalcMode == 0)) + { + double current_rate = 1, future_rate = 1; + RefreshRates(); + if (dir == OP_BUY) + { + if (entry == 0) current_rate = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + else current_rate = entry; + future_rate = current_rate - SL * _Point; + } + else if (dir == OP_SELL) + { + if (entry == 0) current_rate = SymbolInfoDouble(_Symbol, SYMBOL_BID); + else current_rate = entry; + future_rate = current_rate + SL * _Point; + } + if (future_rate == 0) future_rate = _Point; // Zero divide prevention. + UnitCost *= (current_rate / future_rate); + } + + double TickSize = MarketInfo(Symbol(), MODE_TICKSIZE); + + if ((SL != 0) && (UnitCost != 0) && (TickSize != 0)) PositionSize = NormalizeDouble(RiskMoney / (SL * _Point * UnitCost / TickSize), LotStep_digits); + + if (PositionSize < MarketInfo(Symbol(), MODE_MINLOT)) + { + Print("Calculated position size (" + DoubleToString(PositionSize, 2) + ") is less than minimum position size (" + DoubleToString(MarketInfo(Symbol(), MODE_MINLOT), 2) + "). Setting position size to minimum."); + PositionSize = MarketInfo(Symbol(), MODE_MINLOT); + } + else if (PositionSize > MarketInfo(Symbol(), MODE_MAXLOT)) + { + Print("Calculated position size (" + DoubleToString(PositionSize, 2) + ") is greater than maximum position size (" + DoubleToString(MarketInfo(Symbol(), MODE_MAXLOT), 2) + "). Setting position size to maximum."); + PositionSize = MarketInfo(Symbol(), MODE_MAXLOT); + } + + double steps = PositionSize / LotStep; + if (MathAbs(MathRound(steps) - steps) < 0.00000001) steps = MathRound(steps); + if (steps - MathFloor(steps) > 0.5) + { +Print(steps, " ", MathFloor(steps)); + Print("Calculated position size (" + DoubleToString(PositionSize, 2) + ") uses uneven step size. Allowed step size = " + DoubleToString(MarketInfo(Symbol(), MODE_LOTSTEP), 2) + ". Setting position size to " + DoubleToString(MathFloor(steps) * LotStep, 2) + "."); + PositionSize = MathFloor(steps) * LotStep; + } + + return PositionSize; +} + +//+-----------------------------------------------------------------------------------+ +//| Calculates necessary adjustments for cases when ProfitCurrency != AccountCurrency.| +//+-----------------------------------------------------------------------------------+ +#define FOREX_SYMBOLS_ONLY 0 +#define NONFOREX_SYMBOLS_ONLY 1 +double CalculateAdjustment() +{ + double add_coefficient = 1; // Might be necessary for correction coefficient calculation if two pairs are used for profit currency to account currency conversion. This is handled differently in MT5 version. + if (ReferenceSymbol == NULL) + { + ReferenceSymbol = GetSymbolByCurrencies(ProfitCurrency, account_currency, FOREX_SYMBOLS_ONLY); + if (ReferenceSymbol == NULL) ReferenceSymbol = GetSymbolByCurrencies(ProfitCurrency, account_currency, NONFOREX_SYMBOLS_ONLY); + ReferenceSymbolMode = true; + // Failed. + if (ReferenceSymbol == NULL) + { + // Reversing currencies. + ReferenceSymbol = GetSymbolByCurrencies(account_currency, ProfitCurrency, FOREX_SYMBOLS_ONLY); + if (ReferenceSymbol == NULL) ReferenceSymbol = GetSymbolByCurrencies(account_currency, ProfitCurrency, NONFOREX_SYMBOLS_ONLY); + ReferenceSymbolMode = false; + } + if (ReferenceSymbol == NULL) + { + // The condition checks whether we are caclulating conversion coefficient for the chart's symbol or for some other. + // The error output is OK for the current symbol only because it won't be repeated ad infinitum. + // It should be avoided for non-chart symbols because it will just flood the log. + Print("Couldn't detect proper currency pair for adjustment calculation. Profit currency: ", ProfitCurrency, ". Account currency: ", account_currency, ". Trying to find a possible two-symbol combination."); + if ((FindDoubleReferenceSymbol("USD")) // USD should work in 99.9% of cases. + || (FindDoubleReferenceSymbol("EUR")) // For very rare cases. + || (FindDoubleReferenceSymbol("GBP")) // For extremely rare cases. + || (FindDoubleReferenceSymbol("JPY"))) // For extremely rare cases. + { + Print("Converting via ", ReferenceSymbol, " and ", AdditionalReferenceSymbol, "."); + } + else + { + Print("Adjustment calculation critical failure. Failed both simple and two-pair conversion methods."); + return 1; + } + } + } + if (AdditionalReferenceSymbol != NULL) // If two reference pairs are used. + { + // Calculate just the additional symbol's coefficient and then use it in final return's multiplication. + MqlTick tick; + SymbolInfoTick(AdditionalReferenceSymbol, tick); + add_coefficient = GetCurrencyCorrectionCoefficient(tick, AdditionalReferenceSymbolMode); + } + MqlTick tick; + SymbolInfoTick(ReferenceSymbol, tick); + return GetCurrencyCorrectionCoefficient(tick, ReferenceSymbolMode) * add_coefficient; +} + +//+---------------------------------------------------------------------------+ +//| Returns a currency pair with specified base currency and profit currency. | +//+---------------------------------------------------------------------------+ +string GetSymbolByCurrencies(const string base_currency, const string profit_currency, const uint symbol_type) +{ + // Cycle through all symbols. + for (int s = 0; s < SymbolsTotal(false); s++) + { + // Get symbol name by number. + string symbolname = SymbolName(s, false); + string b_cur; + + // Normal case - Forex pairs: + if (MarketInfo(symbolname, MODE_PROFITCALCMODE) == 0) + { + if (symbol_type == NONFOREX_SYMBOLS_ONLY) continue; // Avoid checking symbols of a wrong type. + // Get its base currency. + b_cur = SymbolInfoString(symbolname, SYMBOL_CURRENCY_BASE); + } + else // Weird case for brokers that set conversion pairs as CFDs. + { + if (symbol_type == FOREX_SYMBOLS_ONLY) continue; // Avoid checking symbols of a wrong type. + // Get its base currency as the initial three letters - prone to huge errors! + b_cur = StringSubstr(symbolname, 0, 3); + } + + // Get its profit currency. + string p_cur = SymbolInfoString(symbolname, SYMBOL_CURRENCY_PROFIT); + + // If the currency pair matches both currencies, select it in Market Watch and return its name. + if ((b_cur == base_currency) && (p_cur == profit_currency)) + { + // Select if necessary. + if (!(bool)SymbolInfoInteger(symbolname, SYMBOL_SELECT)) SymbolSelect(symbolname, true); + + return symbolname; + } + } + return NULL; +} + +//+----------------------------------------------------------------------------+ +//| Finds reference symbols using 2-pair method. | +//| Results are returned via reference parameters. | +//| Returns true if found the pairs, false otherwise. | +//+----------------------------------------------------------------------------+ +bool FindDoubleReferenceSymbol(const string cross_currency) +{ + // A hypothetical example for better understanding: + // The trader buys CAD/CHF. + // account_currency is known = SEK. + // cross_currency = USD. + // profit_currency = CHF. + // I.e., we have to buy dollars with francs (using the Ask price) and then sell those for SEKs (using the Bid price). + + ReferenceSymbol = GetSymbolByCurrencies(cross_currency, account_currency, FOREX_SYMBOLS_ONLY); + if (ReferenceSymbol == NULL) ReferenceSymbol = GetSymbolByCurrencies(cross_currency, account_currency, NONFOREX_SYMBOLS_ONLY); + ReferenceSymbolMode = true; // If found, we've got USD/SEK. + + // Failed. + if (ReferenceSymbol == NULL) + { + // Reversing currencies. + ReferenceSymbol = GetSymbolByCurrencies(account_currency, cross_currency, FOREX_SYMBOLS_ONLY); + if (ReferenceSymbol == NULL) ReferenceSymbol = GetSymbolByCurrencies(account_currency, cross_currency, NONFOREX_SYMBOLS_ONLY); + ReferenceSymbolMode = false; // If found, we've got SEK/USD. + } + if (ReferenceSymbol == NULL) + { + Print("Error. Couldn't detect proper currency pair for 2-pair adjustment calculation. Cross currency: ", cross_currency, ". Account currency: ", account_currency, "."); + return false; + } + + AdditionalReferenceSymbol = GetSymbolByCurrencies(cross_currency, ProfitCurrency, FOREX_SYMBOLS_ONLY); + if (AdditionalReferenceSymbol == NULL) AdditionalReferenceSymbol = GetSymbolByCurrencies(cross_currency, ProfitCurrency, NONFOREX_SYMBOLS_ONLY); + AdditionalReferenceSymbolMode = false; // If found, we've got USD/CHF. Notice that mode is swapped for cross/profit compared to cross/acc, because it is used in the opposite way. + + // Failed. + if (AdditionalReferenceSymbol == NULL) + { + // Reversing currencies. + AdditionalReferenceSymbol = GetSymbolByCurrencies(ProfitCurrency, cross_currency, FOREX_SYMBOLS_ONLY); + if (AdditionalReferenceSymbol == NULL) AdditionalReferenceSymbol = GetSymbolByCurrencies(ProfitCurrency, cross_currency, NONFOREX_SYMBOLS_ONLY); + AdditionalReferenceSymbolMode = true; // If found, we've got CHF/USD. Notice that mode is swapped for profit/cross compared to acc/cross, because it is used in the opposite way. + } + if (AdditionalReferenceSymbol == NULL) + { + Print("Error. Couldn't detect proper currency pair for 2-pair adjustment calculation. Cross currency: ", cross_currency, ". Chart's pair currency: ", ProfitCurrency, "."); + return false; + } + + return true; +} + +//+------------------------------------------------------------------+ +//| Get profit correction coefficient based on current prices. | +//| Valid for loss calculation only. | +//+------------------------------------------------------------------+ +double GetCurrencyCorrectionCoefficient(MqlTick &tick, const bool ref_symbol_mode) +{ + if ((tick.ask == 0) || (tick.bid == 0)) return -1; // Data is not yet ready. + // Reverse quote. + if (ref_symbol_mode) + { + // Using Buy price for reverse quote. + return tick.ask; + } + // Direct quote. + else + { + // Using Sell price for direct quote. + return (1 / tick.bid); + } +} + +//+------------------------------------------------------------------+ +//| Counts decimal places. | +//+------------------------------------------------------------------+ +int CountDecimalPlaces(double number) +{ + // 100 as maximum length of number. + for (int i = 0; i < 100; i++) + { + double pwr = MathPow(10, i); + if (MathRound(number * pwr) / pwr == number) return i; + } + return -1; +} +//+------------------------------------------------------------------+ \ No newline at end of file diff --git a/47-Amazing-News/Amazing-News.pdf b/47-Amazing-News/Amazing-News.pdf new file mode 100644 index 0000000..a8229fa Binary files /dev/null and b/47-Amazing-News/Amazing-News.pdf differ diff --git a/48-Binario/Binario.mq4 b/48-Binario/Binario.mq4 new file mode 100644 index 0000000..271c86f --- /dev/null +++ b/48-Binario/Binario.mq4 @@ -0,0 +1,174 @@ +//+------------------------------------------------------------------+ +//| Binario.mq4 | +//| Copyright © 2008-2022, EarnForex.com | +//| https://www.earnforex.com/ | +//| Based on the EA by don_forex. | +//+------------------------------------------------------------------+ +#property copyright "Copyright © 2008-2022, EarnForex" +#property link "https://www.earnforex.com/metatrader-expert-advisors/Binario/" +#property version "1.01" +#property strict + +#property description "Uses a band of two same period MAs - one over High prices, one over Low prices." +#property description "A breakout from within the bands triggers a trade." + +input group "Main" +input int MA_Period = 144; // MA Period +input ENUM_MA_METHOD MA_Method = MODE_EMA; // MA Method +input int TakeProfit = 100; +input int PipDifference = 25; // PipDifference: distance from MA for breakout. +input group "Money management" +input double Lots = 0.1; // Lots: Fixed position size. +input double MaximumRisk = 2; // MaximumRisk: Position sizing increase coefficient. 0 - disable. +input group "Miscellaneous" +input int Slippage = 3; +input string OrderCommentary = "Binario"; +input int Magic = 16384; + +double Poin; + +void OnInit() +{ + // Checking for unconvetional Point digits number. + if (Point == 0.00001) Poin = 0.0001; // 5 digits. + else if (Point == 0.001) Poin = 0.01; // 3 digits. + else Poin = Point; // Normal. +} + +void OnTick() +{ + if (Bars(Symbol(), Period()) < 144) + { + Print("Fewer than 144 bars on the chart. Trading disabled."); + return; + } + + double MA144H = MathRound(iMA(NULL, 0, 144, 0, MODE_EMA, PRICE_HIGH, 0) / Poin) * Poin; + double MA144L = MathRound(iMA(NULL, 0, 144, 0, MODE_EMA, PRICE_LOW, 0) / Poin) * Poin; + + double Spread = Ask - Bid; + + double BuyPrice = NormalizeDouble(MA144H + Spread + PipDifference * Poin, _Digits); + double BuyStopLoss = NormalizeDouble(MA144L - Poin, _Digits); + double BuyTakeProfit = NormalizeDouble(MA144H + (PipDifference + TakeProfit) * Poin, _Digits); + double SellPrice = NormalizeDouble(MA144L - (PipDifference) * Poin, _Digits); + double SellStopLoss = NormalizeDouble(MA144H + Spread + Poin, _Digits); + double SellTakeProfit = NormalizeDouble(MA144L - Spread - (PipDifference + TakeProfit) * Poin, _Digits); + + double Lot = Lots; + if (MaximumRisk > 0) // Use increasing position size. + { + int LotStep_digits = CountDecimalPlaces(SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP)); + Lot = NormalizeDouble(AccountInfoDouble(ACCOUNT_MARGIN_FREE) * MaximumRisk / 50000, LotStep_digits); + } + if (Lot < SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN)) Lot = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN); + if (Lot > SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MAX)) Lot = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MAX); + + bool need_long = true; + bool need_short = true; + + int total = OrdersTotal(); + for (int cnt = 0; cnt < total; cnt++) + { + if (!OrderSelect(cnt, SELECT_BY_POS, MODE_TRADES)) + { + Print("OrderSelect() failed. Error: ", GetLastError()); + continue; + } + if ((OrderSymbol() != Symbol()) || (OrderMagicNumber() != Magic)) continue; + + if (OrderType() == OP_BUYSTOP) + { + need_long = false; + if (OrderStopLoss() != BuyStopLoss) + { + if (!OrderModify(OrderTicket(), BuyPrice, BuyStopLoss, BuyTakeProfit, 0)) + { + Print("OrderModify() failed. Error: ", GetLastError()); + } + } + } + else if (OrderType() == OP_SELLSTOP) + { + need_short = false; + if (OrderStopLoss() != SellStopLoss) + { + if (!OrderModify(OrderTicket(), SellPrice, SellStopLoss, SellTakeProfit, 0)) + { + Print("OrderModify() failed. Error: ", GetLastError()); + } + } + } + else if (OrderType() == OP_BUY) + { + need_long = false; + if (OrderStopLoss() < BuyStopLoss) + { + if (!OrderModify(OrderTicket(), OrderOpenPrice(), BuyStopLoss, BuyTakeProfit, 0)) + { + Print("OrderModify() failed. Error: ", GetLastError()); + } + } + } + else if (OrderType() == OP_SELL) + { + need_short = false; + if (OrderStopLoss() > SellStopLoss) + { + if (!OrderModify(OrderTicket(), OrderOpenPrice(), SellStopLoss, SellTakeProfit, 0)) + { + Print("OrderModify() failed. Error: ", GetLastError()); + } + } + } + } + + if (AccountFreeMargin() < (1000 * Lot)) + { + Print("No money. Free margin = ", AccountFreeMargin()); + return; + } + + if ((Bid < MA144H) && (Bid > MA144L)) // Inside the MA bands. + { + if (need_long) + { + for (int i = 0; i < 10; i++) // 10 attempts. + { + int ticket = OrderSend(Symbol(), OP_BUYSTOP, Lot, BuyPrice, Slippage, BuyStopLoss, BuyTakeProfit, OrderCommentary, Magic, 0, clrGreen); + if (ticket == -1) + { + Print("OrderSend() failed. Error: ", GetLastError()); + } + else break; + } + } + else if (need_short) + { + for (int i = 0; i < 10; i++) // 10 attempts. + { + int ticket = OrderSend(Symbol(), OP_SELLSTOP, Lot, SellPrice, Slippage, SellStopLoss, SellTakeProfit, OrderCommentary, Magic, 0, clrRed); + if (ticket == -1) + { + Print("OrderSend() failed. Error: ", GetLastError()); + } + else break; + } + } + } +} + +//+------------------------------------------------------------------+ +//| Counts decimal places. | +//+------------------------------------------------------------------+ +int CountDecimalPlaces(double number) +{ + // 100 as maximum length of number. + for (int i = 0; i < 100; i++) + { + double pwr = MathPow(10, i); + if (MathRound(number * pwr) / pwr == number) return i; + } + return -1; +} +//+------------------------------------------------------------------+ \ No newline at end of file diff --git a/48-Binario/Binario.pdf b/48-Binario/Binario.pdf new file mode 100644 index 0000000..64be640 Binary files /dev/null and b/48-Binario/Binario.pdf differ diff --git a/49-SingleLogic-Long/SingleLogic-Long.mq4 b/49-SingleLogic-Long/SingleLogic-Long.mq4 new file mode 100644 index 0000000..7f0bfd7 Binary files /dev/null and b/49-SingleLogic-Long/SingleLogic-Long.mq4 differ diff --git a/49-SingleLogic-Long/SingleLogic-Long.pdf b/49-SingleLogic-Long/SingleLogic-Long.pdf new file mode 100644 index 0000000..5795d4a Binary files /dev/null and b/49-SingleLogic-Long/SingleLogic-Long.pdf differ diff --git a/50-SingleLogic-LongShort/SingleLogic-LongShort.mq4 b/50-SingleLogic-LongShort/SingleLogic-LongShort.mq4 new file mode 100644 index 0000000..eabefb5 Binary files /dev/null and b/50-SingleLogic-LongShort/SingleLogic-LongShort.mq4 differ diff --git a/50-SingleLogic-LongShort/SingleLogic-LongShort.pdf b/50-SingleLogic-LongShort/SingleLogic-LongShort.pdf new file mode 100644 index 0000000..a19f638 Binary files /dev/null and b/50-SingleLogic-LongShort/SingleLogic-LongShort.pdf differ diff --git a/51-MA-Cross-Demo/MA-Cross-Demo.mq4 b/51-MA-Cross-Demo/MA-Cross-Demo.mq4 new file mode 100644 index 0000000..f68a0ed --- /dev/null +++ b/51-MA-Cross-Demo/MA-Cross-Demo.mq4 @@ -0,0 +1,174 @@ +/* +============================================================ +Demo File: MA_Crossover_EA - Signal Logic Showcase +Category: Trend Following +Platform: MetaTrader 4 (MQL4) +Version: 1.0 +Author: Giacomo Cipolat Bares +Portfolio: MQL4 Expert Advisors Portfolio +============================================================ + +Description: +This is a simplified public demo derived from the full +Moving Average Crossover EA. + +Included in this demo: +- moving average signal logic +- crossover detection +- minimum cross distance validation +- basic on-chart signal output + +Excluded from this demo: +- order execution +- risk management engine +- break-even / trailing stop +- retry logic +- broker protection handling +- full production trade framework +============================================================ +*/ + +#property strict +#property version "1.00" + +//========================= INPUTS ================================== +input string __01_GeneralSettings = "01 ======== General Settings ========"; +input int FastMAPeriod = 10; +input int SlowMAPeriod = 20; +input int MAMethod = MODE_EMA; +input int MAPrice = PRICE_CLOSE; +input double MinCrossDistancePips = 0.5; + +//======================= GLOBALS =================================== +double g_point; +double g_pip; +int g_digits; +datetime g_lastBarTime = 0; + +//+------------------------------------------------------------------+ +//| Expert initialization | +//+------------------------------------------------------------------+ +int OnInit() +{ + g_point = Point; + g_digits = Digits; + + if(g_digits == 5 || g_digits == 3) + g_pip = g_point * 10.0; + else + g_pip = g_point; + + if(FastMAPeriod >= SlowMAPeriod) + { + Print("ERROR: FastMAPeriod must be smaller than SlowMAPeriod"); + return(INIT_FAILED); + } + + Print("MA Crossover demo initialized"); + return(INIT_SUCCEEDED); +} + +//+------------------------------------------------------------------+ +//| Detects a new bar | +//+------------------------------------------------------------------+ +bool IsNewBar() +{ + datetime currentBarTime = iTime(NULL, 0, 0); + + if(currentBarTime != g_lastBarTime) + { + g_lastBarTime = currentBarTime; + return true; + } + + return false; +} + +//+------------------------------------------------------------------+ +//| Moving average helper | +//+------------------------------------------------------------------+ +double GetMA(int period, int shift) +{ + return iMA(NULL, 0, period, 0, MAMethod, MAPrice, shift); +} + +//+------------------------------------------------------------------+ +//| Minimum cross distance filter | +//+------------------------------------------------------------------+ +bool CrossDistancePassed(double fastValue, double slowValue) +{ + double distance = MathAbs(fastValue - slowValue); + return (distance >= MinCrossDistancePips * g_pip); +} + +//+------------------------------------------------------------------+ +//| Bullish crossover | +//+------------------------------------------------------------------+ +bool IsBullishCross() +{ + double fastPrev = GetMA(FastMAPeriod, 2); + double slowPrev = GetMA(SlowMAPeriod, 2); + double fastCurr = GetMA(FastMAPeriod, 1); + double slowCurr = GetMA(SlowMAPeriod, 1); + + if(!(fastPrev <= slowPrev && fastCurr > slowCurr)) + return false; + + if(!CrossDistancePassed(fastCurr, slowCurr)) + return false; + + return true; +} + +//+------------------------------------------------------------------+ +//| Bearish crossover | +//+------------------------------------------------------------------+ +bool IsBearishCross() +{ + double fastPrev = GetMA(FastMAPeriod, 2); + double slowPrev = GetMA(SlowMAPeriod, 2); + double fastCurr = GetMA(FastMAPeriod, 1); + double slowCurr = GetMA(SlowMAPeriod, 1); + + if(!(fastPrev >= slowPrev && fastCurr < slowCurr)) + return false; + + if(!CrossDistancePassed(fastCurr, slowCurr)) + return false; + + return true; +} + +//+------------------------------------------------------------------+ +//| Demo buy signal wrapper | +//+------------------------------------------------------------------+ +bool BuySignal() +{ + return IsBullishCross(); +} + +//+------------------------------------------------------------------+ +//| Demo sell signal wrapper | +//+------------------------------------------------------------------+ +bool SellSignal() +{ + return IsBearishCross(); +} + +//+------------------------------------------------------------------+ +//| Expert tick | +//+------------------------------------------------------------------+ +void OnTick() +{ + if(!IsNewBar()) + return; + + if(BuySignal()) + Comment("Demo Signal: BUY crossover detected"); + + else if(SellSignal()) + Comment("Demo Signal: SELL crossover detected"); + + else + Comment("Demo Signal: No valid crossover"); +} \ No newline at end of file diff --git a/51-MA-Cross-Demo/MA-Cross-Demo.pdf b/51-MA-Cross-Demo/MA-Cross-Demo.pdf new file mode 100644 index 0000000..fd9a1ee Binary files /dev/null and b/51-MA-Cross-Demo/MA-Cross-Demo.pdf differ diff --git a/52-MACD-EA/MACD-EA.mq5 b/52-MACD-EA/MACD-EA.mq5 new file mode 100644 index 0000000..1942e7a --- /dev/null +++ b/52-MACD-EA/MACD-EA.mq5 @@ -0,0 +1,87 @@ +/** + * @copyright 2019, pipbolt.io + * @license https://github.com/pipbolt/experts/blob/master/LICENSE + */ + +#include + +#define NAME "MACD EA" +#define VERSION "0.022" + +#property copyright COPYRIGHT +#property link LINK +#property icon ICON +#property description DESCRIPTION +#property version VERSION + +#include + +input group "Entry Strategy"; +enum ENUM_ENTRY_STRATEGY +{ + SINGAL_CROSSES_HISTOGRAM, // Signal Line Crosses Histogram + HISTOGRAM_CROSSES_ZERO, // Histogram Crosses Zero Line + SIGNAL_CROSSES_ZERO // Signal Line Crosses Zero Line +}; +input ENUM_ENTRY_STRATEGY EntryStrategy = 0; // Entry Strategy + +input group "Exit Strategy"; +input bool UseExitStrategy = false; // Use Exit Strategy + +input group "MACD"; +input int MACDFastPeriod = 12; // Fast Period +input int MACDSlowPeriod = 26; // Slow Period +input int MACDSignalPeriod = 9; // Signal Period +input ENUM_APPLIED_PRICE MACDPrice = PRICE_CLOSE; // Applied Price + +#include + +CiMACD MACD; + +int OnInit(void) +{ + if (ONINIT() != INIT_SUCCEEDED) + return INIT_FAILED; + + MACD.Init(NULL, NULL, MACDFastPeriod, MACDSlowPeriod, MACDSignalPeriod, MACDPrice); + + return (INIT_SUCCEEDED); +} + +void OnTick(void) { ONTICK(); } +void OnDeinit(const int reason) { ONDEINIT(reason); } +void OnTimer() { ONTIMER(); } + +void CheckForOpen(bool &openBuy, bool &openSell) +{ + + // Check Entry Strategy + switch (EntryStrategy) + { + case SINGAL_CROSSES_HISTOGRAM: + openBuy = MACD.Main(0) <= 0 && MACD.Signal(0) < MACD.Main(0) && MACD.Signal(1) >= MACD.Main(1); + openSell = MACD.Main(0) >= 0 && MACD.Signal(0) > MACD.Main(0) && MACD.Signal(1) <= MACD.Main(1); + break; + case HISTOGRAM_CROSSES_ZERO: + openBuy = MACD.Main(0) > 0 && MACD.Main(1) <= 0; + openSell = MACD.Main(0) < 0 && MACD.Main(1) >= 0; + break; + case SIGNAL_CROSSES_ZERO: + openBuy = MACD.Signal(0) > 0 && MACD.Signal(1) <= 0; + openSell = MACD.Signal(0) < 0 && MACD.Signal(1) >= 0; + break; + } + + // Apply MA Filter + openBuy = openBuy && MAFilter.Check(DIR_BUY); + openSell = openSell && MAFilter.Check(DIR_SELL); +} + +void CheckForClose(bool &closeBuy, bool &closeSell) +{ + // Buy Exit Strategy + closeBuy = MACD.Signal(0) > MACD.Main(0); + + // Sell Exit Strategy + closeSell = MACD.Signal(0) < MACD.Main(0); +} \ No newline at end of file diff --git a/52-MACD-EA/MACD-EA.pdf b/52-MACD-EA/MACD-EA.pdf new file mode 100644 index 0000000..63ce951 Binary files /dev/null and b/52-MACD-EA/MACD-EA.pdf differ diff --git a/53-Same-Situation/Same-Situation.mq4 b/53-Same-Situation/Same-Situation.mq4 new file mode 100644 index 0000000..7d9246a --- /dev/null +++ b/53-Same-Situation/Same-Situation.mq4 @@ -0,0 +1,195 @@ +#include +#include + +#property copyright "Copyright 2023, MetaQuotes Ltd." +#property link "https://www.mql5.com" +#property version "1.00" +#property strict + +// config parameters +double initialDistance = 3; // Initial distance for the first order +double orderInterval = 0.25; // Distance between orders in points +int orderCount = 10; // Number of orders to place on each side +double stopLossDistance = 2.5; // Stop loss distance in points +double takeProfitDistance = 3; // Take profit distance in points +double Lots = 0.01; // Lot size for orders +double stdThreshold = 0.1; // Standard deviation threshold for placing orders +int stdTicksNumbers = 50; +int removeOrderTime = 120; // seconds +int waitForOrderTime = 120; // seconds + +datetime lastOrderOpenTime = 0; + +// global variables +double tickPrices[]; +int tickCounts = 0; + + +int OnInit() +{ + // create zero array for tickPrices + ArraySetAsSeries(tickPrices, true); + ArrayResize(tickPrices, stdTicksNumbers); + ArraySetAsSeries(tickPrices, false); + ArrayInitialize(tickPrices, 0.0); + + return(INIT_SUCCEEDED); +} + +void OnDeinit(const int reason) +{ +} + +void PrintArray(const double &array[]) +{ + string arrayString = ""; + int arraySize = ArraySize(array); + for (int i = 0; i < arraySize; i++) + arrayString += DoubleToString(array[i], 5) + " "; + Print(arrayString); +} + +double getAverage(const int _period) +{ + double sum = 0.0; + for (int i = 0; i < _period; i++) + sum += iClose(NULL, 0, i); + return sum / _period; +} + +void PlaceOrders() +{ + double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); + + // Calculate the distance for the first order + double firstOrderDistance = initialDistance; + + for (int i = 0; i < orderCount; i++) + { + // Place Buy Orders1 + double orderPrice = currentPrice + firstOrderDistance; + double tp = orderPrice - takeProfitDistance; + double sl = orderPrice + stopLossDistance; + int result = OrderSend(_Symbol, OP_SELLLIMIT, Lots, orderPrice , 10, sl, tp, "Sell Order", 0, 0, clrRed); + if (result > 0){ + Print("Sell order placed. Ticket: ", result); + } + else{ + int error = GetLastError(); + string errorDescription = ErrorDescription(error); + Print("OrderSend failed with error #", error, ": ", errorDescription); + } + orderPrice = currentPrice - firstOrderDistance; + tp = orderPrice + takeProfitDistance; + sl = orderPrice - stopLossDistance; + result = OrderSend(_Symbol, OP_BUYLIMIT, Lots, orderPrice , 10, sl, tp , "Buy Order", 0, 0, clrGreen); + if (result > 0){ + Print("Buy order placed. Ticket: ", result); + } + else{ + int error = GetLastError(); + string errorDescription = ErrorDescription(error); + Print("OrderSend failed with error #", error, ": ", errorDescription); + } + // Increment distance for subsequent orders + firstOrderDistance += orderInterval; + } +} + +double getStdTicks() +{ + if (tickCounts < stdTicksNumbers ) + return stdThreshold + 1; + + // Calculate mean + double mean = 0.0; + for (int i = 0; i < stdTicksNumbers ; i++) + mean += tickPrices[i]; + mean /= stdTicksNumbers ; + + + // Calculate sum of squared differences + double ss = 0.0; + for (int i = 0; i < stdTicksNumbers ; i++) + { + double tickPrice = tickPrices[i]; + ss += MathPow(tickPrice - mean, 2); + } + + return MathSqrt(ss / stdTicksNumbers ); +} + + +void updateTickData() +{ + tickCounts ++; + // shift + for(int i = stdTicksNumbers - 1; i > 0; i--) + tickPrices[i] = tickPrices[i-1]; + tickPrices[0] = (Bid + Ask) / 2.0; +} + + +void CheckAndRemovePendingOrders() +{ + int totalOrders = OrdersTotal(); + + for (int i = 0; i < totalOrders; i++) + { + if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES)==false){ + Print("ERROR - Unable to select the order - ", GetLastError()); + continue; + } + // check time + int type = OrderType(); + if (type != OP_BUYLIMIT && type != OP_SELLLIMIT) + continue; + + // Check if the order hasn't been opened yet + if (OrderSymbol() == _Symbol && OrderMagicNumber() ==0) + { + datetime orderOpenTime = OrderOpenTime(); + datetime currentTime = iTime(NULL, 0, 0); + // Calculate the time difference in seconds + int timeDifference = currentTime - orderOpenTime; + // If the order's pending time exceeds 3 minutes (180 seconds), remove the order + if (timeDifference > removeOrderTime ) + { + bool deleteResult = OrderDelete(OrderTicket()); + if (deleteResult) + { + Print("Pending order removed. Ticket: ", OrderTicket()); + } + else + { + int error = GetLastError(); + string errorDescription = ErrorDescription(error); + Print("OrderDelete failed with error #", error, ": ", errorDescription); + } + } + } + } +} + + +void OnTick() +{ + updateTickData(); + double mean = getAverage(10); // average per minute + double std = getStdTicks(); // std per ticks + CheckAndRemovePendingOrders(); + + datetime currentTime = iTime(NULL, 0, 0); + int timeDifferenceSinceLastOrder = currentTime - lastOrderOpenTime; + + if (timeDifferenceSinceLastOrder > waitForOrderTime || lastOrderOpenTime == 0) // 120 seconds = 2 minutes + { + // Additional condition: if std is below the threshold, place orders + Print(std); + if (std < stdThreshold) + { + PlaceOrders(); + lastOrderOpenTime = iTime(NULL, 0, 0); // Update the last order open time + } + } +} \ No newline at end of file diff --git a/53-Same-Situation/Same-Situation.pdf b/53-Same-Situation/Same-Situation.pdf new file mode 100644 index 0000000..d463dc7 Binary files /dev/null and b/53-Same-Situation/Same-Situation.pdf differ diff --git a/54-RT-MA-Pullback/RT-MA-Pullback.mq4 b/54-RT-MA-Pullback/RT-MA-Pullback.mq4 new file mode 100644 index 0000000..a6f4865 Binary files /dev/null and b/54-RT-MA-Pullback/RT-MA-Pullback.mq4 differ diff --git a/54-RT-MA-Pullback/RT-MA-Pullback.pdf b/54-RT-MA-Pullback/RT-MA-Pullback.pdf new file mode 100644 index 0000000..813ed43 Binary files /dev/null and b/54-RT-MA-Pullback/RT-MA-Pullback.pdf differ diff --git a/55-Monkey-EA/Monkey-EA.mq4 b/55-Monkey-EA/Monkey-EA.mq4 new file mode 100644 index 0000000..ff349f7 Binary files /dev/null and b/55-Monkey-EA/Monkey-EA.mq4 differ diff --git a/55-Monkey-EA/Monkey-EA.pdf b/55-Monkey-EA/Monkey-EA.pdf new file mode 100644 index 0000000..fccc7b7 Binary files /dev/null and b/55-Monkey-EA/Monkey-EA.pdf differ diff --git a/56-Account-Protector/Account-Protector.mq4 b/56-Account-Protector/Account-Protector.mq4 new file mode 100644 index 0000000..8c510b8 --- /dev/null +++ b/56-Account-Protector/Account-Protector.mq4 @@ -0,0 +1,463 @@ +//+------------------------------------------------------------------+ +//| Account Protector.mq4 | +//| Copyright © 2017-2026, EarnForex.com | +//| https://www.earnforex.com/ | +//+------------------------------------------------------------------+ +#property copyright "EarnForex.com" +#property link "https://www.earnforex.com/metatrader-expert-advisors/Account-Protector/" +#property version "1.14" +string Version = "1.14"; +#property strict + +#property description "Protects account balance by applying given actions when set conditions trigger." +#property description "Trails stop-losses, applies breakeven, logs its actions, sends notifications.\r\n" +#property description "WARNING: There is no guarantee that the expert advisor will work as intended. Use at your own risk." + +#include "Account Protector.mqh"; + +input string ____Main = ""; +input bool EnableEmergencyButton = false; // Enable emergency button +input bool DoNotDisableConditions = false; // DoNotDisableConditions: Don't disable conditions on trigger? +input bool DoNotDisableActions = false; // DoNotDisableActions: Don't disable actions on trigger? +input bool DoNotDisableEquityTS = false; // DoNotDisableEquityTS: Don't disable equity TS on trigger? +input bool DoNotDisableTimer = false; // DoNotDisableTimer: Don't disable timer on trigger? +input int ConditionDelay = 0; // ConditionDelay: How long should condition be active to trigger? +input bool CountFloatingInDailyPL = true; // CountFloatingInDailyPL: Count floating P/L in daily P/L? +input bool EnableAutoSwitchOnPeriod = false; // Enable auto switch ON period +input string ____Conditions = ""; +input bool DisableFloatLossRisePerc = false; // Disable floating loss rises % condition. +input bool DisableFloatLossFallPerc = true; // Disable floating loss falls % condition. +input bool DisableFloatLossRiseCurr = false; // Disable floating loss rises currency units condition. +input bool DisableFloatLossFallCurr = true; // Disable floating loss falls currency units condition. +input bool DisableFloatLossRisePoints = false; // Disable floating loss rises points condition. +input bool DisableFloatLossFallPoints = true; // Disable floating loss falls points condition. +input bool DisableFloatProfitRisePerc = false; // Disable floating profit rises % condition. +input bool DisableFloatProfitFallPerc = true; // Disable floating profit falls % condition. +input bool DisableFloatProfitRiseCurr = false; // Disable floating profit rises currency units condition. +input bool DisableFloatProfitFallCurr = true; // Disable floating profit falls currency units condition. +input bool DisableFloatProfitRisePoints = false; // Disable floating profit rises points condition. +input bool DisableFloatProfitFallPoints = true; // Disable floating profit falls points condition. +input bool DisableCurrentPriceGE = true; // Disable current price greater or equal condition. +input bool DisableCurrentPriceLE = true; // Disable current price less or equal condition. +input bool DisableEquityUnitsLE = false; // Disable equity less or equal currency units condition. +input bool DisableEquityUnitsGE = false; // Disable equity greater or equal currency units condition. +input bool DisableEquityPercLE = false; // Disable equity less or equal % of snapshot condition. +input bool DisableEquityPercGE = false; // Disable equity greater or equal % of snapshot condition. +input bool DisableEquityMinusSnapshot = true; // Disable (Equity - snapshot) greater or equal condition. +input bool DisableSnapshotMinusEquity = true; // Disable (snapshot - Equity) greater or equal condition. +input bool DisableMarginUnitsLE = false; // Disable free margin less or equal currency units condition. +input bool DisableMarginUnitsGE = false; // Disable free margin greater or equal currency units condition. +input bool DisableMarginPercLE = false; // Disable free margin less or equal % of snapshot condition. +input bool DisableMarginPercGE = false; // Disable free margin greater or equal % of snapshot condition. +input bool DisableMarginLevelGE = true; // Disable margin level greater or equal condition. +input bool DisableMarginLevelLE = true; // Disable margin level less or equal condition. +input bool DisableSpreadGE = true; // Disable spread greater or equal condition. +input bool DisableSpreadLE = true; // Disable spread less or equal condition. +input bool DisableDailyProfitLossUnitsGE = true; // Disable daily profit/loss greater or equal units condition. +input bool DisableDailyProfitLossUnitsLE = true; // Disable daily profit/loss less or equal units condition. +input bool DisableDailyProfitLossPointsGE = true; // Disable daily profit/loss greater or equal points condition. +input bool DisableDailyProfitLossPointsLE = true; // Disable daily profit/loss less or equal points condition. +input bool DisableDailyProfitLossPercGE = true; // Disable daily profit/loss greater or equal percentage condition. +input bool DisableDailyProfitLossPercLE = true; // Disable daily profit/loss less or equal percentage condition. +input bool DisableNumberOfPositionsGE = true; // Disable number of positions greater or equal condition. +input bool DisableNumberOfOrdersGE = true; // Disable number of pending orders greater or equal condition. +input bool DisableNumberOfPositionsLE = true; // Disable number of positions less or equal condition. +input bool DisableNumberOfOrdersLE = true; // Disable number of pending orders less or equal condition. +input bool DisableBalanceGE = true; // Disable balance greater or equal condition. +input bool DisableBalanceLE = true; // Disable balance less or equal condition. +input bool DisableListenToSignal = true; // Disable signal condition. +input bool WaitForAllConditions = false; // WaitForAllConditions: Only trigger when all conditions are met. +input string ____Trading = ""; +input int DelayOrderClose = 0; // DelayOrderClose: Delay in milliseconds. +input bool UseTotalVolume = false; // UseTotalVolume: enable if trading with many small trades and partial position closing. +input ENUM_CLOSE_TRADES CloseFirst = ENUM_CLOSE_TRADES_DEFAULT; // CloseFirst: Close which trades first? +input bool BreakEvenProfitInCurrencyUnits = false; // BreakEvenProfitInCurrencyUnits: currency instead of points. +input bool EquityTrailingStopInPercentage = false; // EquityTrailingStopInPercentage: % instead of $. +input bool DisableAutoTradingOnTS = false; // DisableAutoTradingOnTS: Disable autotrading on eq. TS trigger. +input string ____Miscellaneous = ""; +input bool AlertOnEquityTS = false; // AlertOnEquityTS: Alert when equity trailing stop triggers? +input double AdditionalFunds = 0; // AdditionalFunds: Added to balance, equity, and free margin. +input string Instruments = ""; // Instruments: Default list of trading instruments for order filtering. +input bool GlobalSnapshots = false; // GlobalSnapshots: AP instances share equity & margin snapshots. +input int Slippage = 2; // Slippage +input bool CloseOtherChartsOnEmergencyButton = false; // Close other charts on emergency button. +input string LogFileName = "ap_log.txt"; // Log file name +input string SettingsFileName = ""; // Settings file: Load custom panel settings from \Files\ folder. +input bool Silent = false; // Silent: No log output to the Experts tab. +input bool DarkMode = false; // DarkMode: Enable dark mode for a less bright panel. +input bool IncludeAccountCredit = true; // IncludeAccountCredit: Account credit is added to balance. + +CAccountProtector ExtDialog; + +int DeinitializationReason = -1; + +//+------------------------------------------------------------------+ +//| Initialization function | +//+------------------------------------------------------------------+ +int OnInit() +{ + if (DarkMode) + { + CONTROLS_EDIT_COLOR_ENABLE = DARKMODE_EDIT_BG_COLOR; + CONTROLS_EDIT_COLOR_DISABLE = 0x999999; + CONTROLS_BUTTON_COLOR_ENABLE = DARKMODE_BUTTON_BG_COLOR; + CONTROLS_BUTTON_COLOR_DISABLE = 0x919999; + } + else + { + CONTROLS_EDIT_COLOR_ENABLE = C'255,255,255'; + CONTROLS_EDIT_COLOR_DISABLE = C'221,221,211'; + CONTROLS_BUTTON_COLOR_ENABLE = C'200,200,200'; + CONTROLS_BUTTON_COLOR_DISABLE = C'224,224,224'; + } + + if (DeinitializationReason == REASON_CHARTCHANGE) + { + EventSetTimer(1); + return INIT_SUCCEEDED; + } + + MathSrand(GetTickCount() + 2202051901); // Used by CreateInstanceId() in Dialog.mqh (standard library). Keep the second number unique across other panel indicators/EAs. + + if (SettingsFileName != "") // Load a custom settings file if given via input parameters. + { + ExtDialog.SetFileName(SettingsFileName); + } + ExtDialog.InitVariables(); + + if (!ExtDialog.LoadSettingsFromDisk()) + { + sets.OnOff = false; + sets.CountCommSwaps = true; + sets.UseTimer = false; + sets.Timer = TimeToString(TimeCurrent() - 7200, TIME_MINUTES); + sets.TimeLeft = ""; + sets.intTimeType = 0; + sets.dtTimerLastTriggerTime = 0; + sets.boolTrailingStart = false; + sets.intTrailingStart = 0; + sets.boolTrailingStep = false; + sets.intTrailingStep = 0; + sets.boolBreakEven = false; + sets.intBreakEven = 0; + sets.doubleBreakEven = 0; + sets.boolBreakEvenExtra = false; + sets.intBreakEvenExtra = 0; + sets.boolEquityTrailingStop = false; + sets.doubleEquityTrailingStop = 0; + sets.doubleCurrentEquityStopLoss = 0; + sets.SnapEquity = AccountInfoDouble(ACCOUNT_EQUITY) + AdditionalFunds; + sets.SnapEquityTime = TimeToString(TimeCurrent(), TIME_DATE | TIME_MINUTES | TIME_SECONDS); + sets.SnapMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE) + AdditionalFunds; + sets.SnapMarginTime = TimeToString(TimeCurrent(), TIME_DATE | TIME_MINUTES | TIME_SECONDS); + if (GlobalSnapshots) + { + SaveGlobalEquitySnapshots(); + SaveGlobalMarginSnapshots(); + } + sets.OrderCommentary = ""; + sets.intOrderCommentaryCondition = 0; + sets.intOrderDirection = 0; + sets.MagicNumbers = ""; + sets.boolExcludeMagics = false; + sets.intInstrumentFilter = 0; + sets.Instruments = Instruments; + sets.boolIgnoreLossTrades = false; + sets.boolIgnoreProfitTrades = false; + sets.boolLossPerBalance = false; + sets.boolLossQuanUnits = false; + sets.boolLossPoints = false; + sets.boolProfPerBalance = false; + sets.boolProfQuanUnits = false; + sets.boolProfPoints = false; + sets.boolLossPerBalanceReverse = false; + sets.boolLossQuanUnitsReverse = false; + sets.boolLossPointsReverse = false; + sets.boolProfPerBalanceReverse = false; + sets.boolProfQuanUnitsReverse = false; + sets.boolProfPointsReverse = false; + sets.boolEquityLessUnits = false; + sets.boolEquityGrUnits = false; + sets.boolEquityLessPerSnap = false; + sets.boolEquityGrPerSnap = false; + sets.boolEquityMinusSnapshot = false; + sets.boolSnapshotMinusEquity = false; + sets.boolMarginLessUnits = false; + sets.boolMarginGrUnits = false; + sets.boolMarginLessPerSnap = false; + sets.boolMarginGrPerSnap = false; + sets.boolPriceGE = false; + sets.boolPriceLE = false; + sets.boolMarginLevelGE = false; + sets.boolMarginLevelLE = false; + sets.boolSpreadGE = false; + sets.boolSpreadLE = false; + sets.boolDailyProfitLossUnitsGE = false; + sets.boolDailyProfitLossUnitsLE = false; + sets.boolDailyProfitLossPointsGE = false; + sets.boolDailyProfitLossPointsLE = false; + sets.boolDailyProfitLossPercGE = false; + sets.boolDailyProfitLossPercLE = false; + sets.boolNumberOfPositionsGE = false; + sets.boolNumberOfOrdersGE = false; + sets.boolNumberOfPositionsLE = false; + sets.boolNumberOfOrdersLE = false; + sets.boolBalanceGE = false; + sets.boolBalanceLE = false; + sets.boolListenToSignal = false; + sets.doubleLossPerBalance = 0; + sets.doubleLossQuanUnits = 0; + sets.intLossPoints = 0; + sets.doubleProfPerBalance = 0; + sets.doubleProfQuanUnits = 0; + sets.intProfPoints = 0; + sets.doubleLossPerBalanceReverse = 0; + sets.doubleLossQuanUnitsReverse = 0; + sets.intLossPointsReverse = 0; + sets.doubleProfPerBalanceReverse = 0; + sets.doubleProfQuanUnitsReverse = 0; + sets.intProfPointsReverse = 0; + sets.doubleEquityLessUnits = 0; + sets.doubleEquityGrUnits = 0; + sets.doubleEquityLessPerSnap = 0; + sets.doubleEquityGrPerSnap = 0; + sets.doubleEquityMinusSnapshot = 0; + sets.doubleSnapshotMinusEquity = 0; + sets.doubleMarginLessUnits = 0; + sets.doubleMarginGrUnits = 0; + sets.doubleMarginLessPerSnap = 0; + sets.doubleMarginGrPerSnap = 0; + sets.doublePriceGE = 0; + sets.doublePriceLE = 0; + sets.doubleMarginLevelGE = 0; + sets.doubleMarginLevelLE = 0; + sets.intSpreadGE = 0; + sets.intSpreadLE = 0; + sets.doubleDailyProfitLossUnitsGE= 0; + sets.doubleDailyProfitLossUnitsLE = 0; + sets.intDailyProfitLossPointsGE= 0; + sets.intDailyProfitLossPointsLE = 0; + sets.doubleDailyProfitLossPercGE= 0; + sets.doubleDailyProfitLossPercLE = 0; + sets.intNumberOfPositionsGE = 0; + sets.intNumberOfOrdersGE = 0; + sets.intNumberOfPositionsLE = 0; + sets.intNumberOfOrdersLE = 0; + sets.doubleBalanceGE = 0; + sets.doubleBalanceLE = 0; + sets.intListenToSignal = 0; + sets.ClosePos = true; + sets.doubleClosePercentage = 100; + sets.CloseWhichPositions = All; + sets.DeletePend = true; + sets.SendMails = false; + sets.SendNotif = false; + sets.ClosePlatform = false; + sets.DisAuto = false; + sets.EnableAuto = false; + sets.RecaptureSnapshots = false; + sets.CloseAllOtherCharts = false; + sets.SelectedTab = MainTab; + + ExtDialog.SilentLogging = true; + ExtDialog.Logging("=====EA IS FIRST ATTACHED TO CHART====="); + ExtDialog.Logging("Account Number = " + IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)) + ", Client Name = " + AccountInfoString(ACCOUNT_NAME)); + ExtDialog.Logging("Server Name = " + AccountInfoString(ACCOUNT_SERVER) + ", Broker Name = " + AccountInfoString(ACCOUNT_COMPANY)); + ExtDialog.Logging("Account Currency = " + AccountInfoString(ACCOUNT_CURRENCY) + ", Account Leverage = " + IntegerToString(AccountInfoInteger(ACCOUNT_LEVERAGE))); + ExtDialog.Logging("Account Balance = " + DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2) + " " + AccountInfoString(ACCOUNT_CURRENCY) + ", Account Credit = " + DoubleToString(AccountInfoDouble(ACCOUNT_CREDIT), 2) + " " + AccountInfoString(ACCOUNT_CURRENCY)); + ExtDialog.Logging("Account Equity = " + DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY), 2) + " " + AccountInfoString(ACCOUNT_CURRENCY) + ", Account Free Margin = " + DoubleToString(AccountInfoDouble(ACCOUNT_MARGIN_FREE), 2) + " " + AccountInfoString(ACCOUNT_CURRENCY)); + ExtDialog.Logging("Account Margin Call / Stop-Out Mode = " + EnumToString((ENUM_ACCOUNT_TRADE_MODE)AccountInfoInteger(ACCOUNT_MARGIN_SO_MODE))); + string units; + int decimal_places; + if (AccountInfoInteger(ACCOUNT_MARGIN_SO_MODE) == ACCOUNT_STOPOUT_MODE_PERCENT) + { + units = "%"; + decimal_places = 0; + } + else + { + units = AccountInfoString(ACCOUNT_CURRENCY); + decimal_places = 2; + } + ExtDialog.Logging("Account Margin Call Level = " + DoubleToString(AccountInfoDouble(ACCOUNT_MARGIN_SO_CALL), decimal_places) + units + ", Account Margin Stopout Level = " + DoubleToString(AccountInfoDouble(ACCOUNT_MARGIN_SO_SO), decimal_places) + units); + ExtDialog.Logging("Enable Emergency Button = " + IntegerToString(EnableEmergencyButton)); + ExtDialog.Logging("EnableAutoSwitchOnPeriod = " + IntegerToString(EnableAutoSwitchOnPeriod)); + ExtDialog.Logging("DelayOrderClose = " + IntegerToString(DelayOrderClose)); + ExtDialog.Logging("UseTotalVolume = " + IntegerToString(UseTotalVolume)); + ExtDialog.Logging("AdditionalBalance = " + DoubleToString(AdditionalFunds, 2)); + ExtDialog.SilentLogging = false; + + sets.Triggered = false; + sets.TriggeredTime = ""; + sets.TimerDayOfWeek = Any; + sets.boolAutoSwitchOnPeriod = false; + sets.AutoSwitchOnPeriod = ""; + } + + if ((!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) || (!MQLInfoInteger(MQL_TRADE_ALLOWED))) + { + string where = ""; + if ((!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) && (!MQLInfoInteger(MQL_TRADE_ALLOWED))) where = "in both EA's and platform's settings"; // Both. + else if (!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) where = "in the platform's settings"; // Platform level. + else if (!MQLInfoInteger(MQL_TRADE_ALLOWED)) where = "in the EA's settings"; // EA level. + Alert("AutoTrading is disabled " + where + "! EA will be not able to perform trading operations!"); + sets.ClosePos = false; + sets.DeletePend = false; + sets.DisAuto = false; + sets.boolTrailingStart = false; + sets.boolTrailingStep = false; + sets.boolBreakEven = false; + sets.boolBreakEvenExtra = false; + } + + if (!ExtDialog.Create(0, Symbol() + " Account Protector (ver. " + Version + ")", 0, 20, 20)) return(-1); + ExtDialog.Run(); + ExtDialog.IniFileLoad(); + + // Brings panel on top of other objects without actual maximization of the panel. + ExtDialog.HideShowMaximize(false); + + ExtDialog.ShowSelectedTab(); + ExtDialog.RefreshPanelControls(); + ExtDialog.RefreshValues(); + + EventSetTimer(1); + + if (DarkMode) + { + int total = ObjectsTotal(ChartID()); + for (int i = 0; i < total; i++) + { + string obj_name = ObjectName(ChartID(), i); + if (StringSubstr(obj_name, 0, StringLen(ExtDialog.Name())) != ExtDialog.Name()) continue; // Skip non-panel objects. + if (obj_name == ExtDialog.Name() + "Back") + { + + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BGCOLOR, DARKMODE_BG_DARK_COLOR); + } + if (obj_name == ExtDialog.Name() + "Caption") + { + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BGCOLOR, DARKMODE_BG_DARK_COLOR); + ObjectSetInteger(ChartID(), obj_name, OBJPROP_COLOR, DARKMODE_CONTROL_BRODER_COLOR); + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BORDER_COLOR, DARKMODE_BG_DARK_COLOR); + } + else if (obj_name == ExtDialog.Name() + "ClientBack") + { + ObjectSetInteger(ChartID(), obj_name, OBJPROP_COLOR, DARKMODE_MAIN_AREA_BORDER_COLOR); + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BGCOLOR, DARKMODE_MAIN_AREA_BG_COLOR); + } + else if (StringSubstr(obj_name, 0, StringLen(ExtDialog.Name() + "m_Edt")) == ExtDialog.Name() + "m_Edt") + { + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BGCOLOR, DARKMODE_EDIT_BG_COLOR); + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BORDER_COLOR, DARKMODE_CONTROL_BRODER_COLOR); + } + else if (StringSubstr(obj_name, 0, StringLen(ExtDialog.Name() + "m_Btn")) == ExtDialog.Name() + "m_Btn") + { + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BGCOLOR, DARKMODE_BUTTON_BG_COLOR); + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BORDER_COLOR, DARKMODE_CONTROL_BRODER_COLOR); + } + else if (StringSubstr(obj_name, 0, StringLen(ExtDialog.Name() + "m_Chk")) == ExtDialog.Name() + "m_Chk") + { + ObjectSetInteger(ChartID(), obj_name, OBJPROP_COLOR, DARKMODE_TEXT_COLOR); + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BGCOLOR, DARKMODE_MAIN_AREA_BG_COLOR); + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BORDER_COLOR, DARKMODE_MAIN_AREA_BG_COLOR); + } + else if (StringSubstr(obj_name, 0, StringLen(ExtDialog.Name() + "m_Rgp")) == ExtDialog.Name() + "m_Rgp") + { + if (ObjectGetInteger(ChartID(), obj_name, OBJPROP_TYPE) == OBJ_RECTANGLE_LABEL) ObjectSetInteger(ChartID(), obj_name, OBJPROP_COLOR, DARKMODE_MAIN_AREA_BG_COLOR); + else ObjectSetInteger(ChartID(), obj_name, OBJPROP_COLOR, DARKMODE_TEXT_COLOR); + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BGCOLOR, DARKMODE_MAIN_AREA_BG_COLOR); + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BORDER_COLOR, DARKMODE_MAIN_AREA_BG_COLOR); + } + else + { + if (obj_name == ExtDialog.Name() + "m_LblURL") ObjectSetInteger(ChartID(), obj_name, OBJPROP_COLOR, 0x224400); + else if (obj_name != ExtDialog.Name() + "m_LblOnOff") ObjectSetInteger(ChartID(), obj_name, OBJPROP_COLOR, DARKMODE_TEXT_COLOR); // Avoid changing On/Off label color. + } + } + } + + return INIT_SUCCEEDED; +} + +//+------------------------------------------------------------------+ +//| Deinitialization function | +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) +{ + DeinitializationReason = reason; // Remember reason to avoid recreating the panel in the OnInit() if it is not deleted here. + EventKillTimer(); + if ((reason == REASON_REMOVE) || (reason == REASON_CHARTCLOSE) || (reason == REASON_PROGRAM)) + { + if (SettingsFileName == "") ExtDialog.DeleteSettingsFile(); // Only delete settings file if no custom file name is given. + Print("Trying to delete ini file."); + if (!FileIsExist(ExtDialog.IniFileName() + ".dat")) Print("File doesn't exist."); + else if (!FileDelete(ExtDialog.IniFileName() + ".dat")) Print("Failed to delete file: " + ExtDialog.IniFileName() + ".dat. Error: " + IntegerToString(GetLastError())); + else Print("Deleted ini file successfully."); + ExtDialog.SilentLogging = true; + ExtDialog.Logging("EA Account Protector is removed."); + ExtDialog.Logging("Account Balance = " + DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2) + " " + AccountInfoString(ACCOUNT_CURRENCY) + ", Account Credit = " + DoubleToString(AccountInfoDouble(ACCOUNT_CREDIT), 2) + " " + AccountInfoString(ACCOUNT_CURRENCY)); + ExtDialog.Logging("Account Equity = " + DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY), 2) + " " + AccountInfoString(ACCOUNT_CURRENCY) + ", Account Free Margin = " + DoubleToString(AccountInfoDouble(ACCOUNT_MARGIN_FREE), 2) + " " + AccountInfoString(ACCOUNT_CURRENCY)); + ExtDialog.SilentLogging = false; + ExtDialog.Logging_Current_Settings(); + } + else if (reason != REASON_CHARTCHANGE) + { + if (reason == REASON_PARAMETERS) GlobalVariableSet("AP-" + IntegerToString(ChartID()) + "-Parameters", 1); + ExtDialog.SaveSettingsOnDisk(); + ExtDialog.IniFileSave(); + } + if (reason != REASON_CHARTCHANGE) ExtDialog.Destroy(); +} + +//+------------------------------------------------------------------+ +//| ChartEvent function | +//+------------------------------------------------------------------+ +void OnChartEvent(const int id, + const long &lparam, + const double &dparam, + const string &sparam) +{ + // Remember the panel's location to have the same location for minimized and maximized states. + if ((id == CHARTEVENT_CUSTOM + ON_DRAG_END) && (lparam == -1)) + { + ExtDialog.remember_top = ExtDialog.Top(); + ExtDialog.remember_left = ExtDialog.Left(); + } + + // Call Panel's event handler only if it is not a CHARTEVENT_CHART_CHANGE - workaround for minimization bug on chart switch. + if (id != CHARTEVENT_CHART_CHANGE) ExtDialog.OnEvent(id, lparam, dparam, sparam); + + if (ExtDialog.Top() < 0) ExtDialog.Move(ExtDialog.Left(), 0); +} + +//+------------------------------------------------------------------+ +//| Tick event handler | +//+------------------------------------------------------------------+ +void OnTick() +{ + ExtDialog.RefreshValues(); + ExtDialog.CheckAutoSwitchOnPeriod(); + if (!sets.OnOff) return; + ExtDialog.Trailing(); + ExtDialog.EquityTrailing(); + ExtDialog.MoveToBreakEven(); + ExtDialog.CheckAllConditions(); + ChartRedraw(); +} + +//+------------------------------------------------------------------+ +//| Timer event handler | +//+------------------------------------------------------------------+ +void OnTimer() +{ + ExtDialog.RefreshValues(); + ExtDialog.CheckAutoSwitchOnPeriod(); + if (!sets.OnOff) return; + ExtDialog.Trailing(); + ExtDialog.EquityTrailing(); + ExtDialog.MoveToBreakEven(); + ExtDialog.CheckAllConditions(); + ChartRedraw(); +} +//+------------------------------------------------------------------+ \ No newline at end of file diff --git a/56-Account-Protector/Account-Protector.pdf b/56-Account-Protector/Account-Protector.pdf new file mode 100644 index 0000000..8fc2c13 Binary files /dev/null and b/56-Account-Protector/Account-Protector.pdf differ diff --git a/57-Position-Sizer/Position-Sizer.mq4 b/57-Position-Sizer/Position-Sizer.mq4 new file mode 100644 index 0000000..f36469e --- /dev/null +++ b/57-Position-Sizer/Position-Sizer.mq4 @@ -0,0 +1,1014 @@ +//+------------------------------------------------------------------+ +//| Position Sizer.mq4 | +//| Copyright © 2026, EarnForex.com | +//| https://www.earnforex.com/ | +//+------------------------------------------------------------------+ +#property copyright "EarnForex.com" +#property link "https://www.earnforex.com/metatrader-expert-advisors/Position-Sizer/" +#define VERSION "3.15" +#property icon "EF-Icon-64x64px.ico" +#property version VERSION +#property strict + +#include "Translations\English.mqh" +//#include "Translations\Arabic.mqh" +//#include "Translations\Chinese.mqh" +//#include "Translations\ChineseTraditional.mqh" // Contributed by fxchess. +//#include "Translations\Japanese.mqh" // Contributed by Satoru Hoshino. +//#include "Translations\Portuguese.mqh" // Contributed by Matheus Sevaroli. +//#include "Translations\Russian.mqh" +//#include "Translations\Spanish.mqh" +//#include "Translations\Ukrainian.mqh" + +#property description DESCRIPTION_LINE_1 +#property description DESCRIPTION_LINE_2 +#property description DESCRIPTION_LINE_3 +#property description DESCRIPTION_LINE_4 + +#include "Position Sizer.mqh" +#include "Position Sizer Trading.mqh" +#include "TesterSupport.mqh" + +input group "Compactness" +input string ____Compactness = ""; +input bool ShowMainLineLabels = true; // ShowMainLineLabels: Show point distance for TP/SL near lines? +input bool ShowAdditionalSLLabel = false; // ShowAdditionalSLLabel: Show SL $/% label? +input bool ShowAdditionalTPLabel = false; // ShowAdditionalTPLabel: Show TP $/% + R/R label? +input bool ShowAdditionalEntryLabel = false; // ShowAdditionalEntryLabel: Show Position Size label? +input bool DrawTextAsBackground = false; // DrawTextAsBackground: Draw label objects as background? +input bool HideAccSize = false; // HideAccSize: Hide account size? +input bool ShowPointValue = false; // ShowPointValue: Show point value? +input bool ShowMaxPSButton = false; // ShowMaxPSButton: Show Max Position Size button? +input bool StartPanelMinimized = false; // StartPanelMinimized: Start the panel minimized? +input bool ShowATROptions = false; // ShowATROptions: If true, SL and TP can be set via ATR. +input bool ShowMaxParametersOnTrading = true; // Show max parameters on Trading tab? +input bool ShowFusesOnTrading = true; // Show trading "fuses" on Trading tab? +input bool ShowCheckboxesOnTrading = true; // Show checkboxes on Trading tab? +input bool HideEntryLineOnInstant = false; // Hide Entry line for Instant orders? +input bool ShowAdditionalMarginSettings = false; // Show additional margin settings/info? +input ADDITIONAL_TRADE_BUTTONS AdditionalTradeButtons = ADDITIONAL_TRADE_BUTTONS_NONE; // Additional Trade buttons: +input group "Fonts" +input string ____Fonts = ""; +input color sl_label_font_color = clrGreen; // SL Label Color +input color tp_label_font_color = clrGoldenrod; // TP Label Font Color +input color entry_label_font_color = clrBlue; // Entry Label Font Color +input uint font_size = 13; // Labels Font Size +input string font_face = "Courier"; // Labels Font Face +input group "Lines" +input string ____Lines = ""; +input color entry_line_color = clrBlue; // Entry Line Color +input color stoploss_line_color = clrGreen; // Stop-Loss Line Color +input color takeprofit_line_color = clrGoldenrod; // Take-Profit Line Color +input color be_line_color = clrNONE; // BE Line Color +input ENUM_LINE_STYLE entry_line_style = STYLE_SOLID; // Entry Line Style +input ENUM_LINE_STYLE stoploss_line_style = STYLE_SOLID; // Stop-Loss Line Style +input ENUM_LINE_STYLE takeprofit_line_style = STYLE_SOLID; // Take-Profit Line Style +input ENUM_LINE_STYLE be_line_style = STYLE_DOT; // BE Line Style +input uint entry_line_width = 1; // Entry Line Width +input uint stoploss_line_width = 1; // Stop-Loss Line Width +input uint takeprofit_line_width = 1; // Take-Profit Line Width +input uint be_line_width = 1; // BE Line Width +input group "Defaults" +input string ____Defaults = ""; +input TRADE_DIRECTION DefaultTradeDirection = Long; // TradeDirection: Default trade direction. +input int DefaultSL = 0; // SL: Default stop-loss value, in points. +input int DefaultTP = 0; // TP: Default take-profit value, in points. +input int DefaultTakeProfitsNumber = 1; // TakeProfitsNumber: More than 1 target to split trades. +input ENTRY_TYPE DefaultEntryType = Instant; // EntryType: Instant or Pending. +input bool DefaultShowLines = true; // ShowLines: Show the lines by default? +input bool DefaultLinesSelected = true; // LinesSelected: SL/TP (Entry in Pending) lines selected. +input int DefaultATRPeriod = 14; // ATRPeriod: Default ATR period. +input double DefaultATRMultiplierSL = 0; // ATRMultiplierSL: Default ATR multiplier for SL. +input double DefaultATRMultiplierTP = 0; // ATRMultiplierTP: Default ATR multiplier for TP. +input ENUM_TIMEFRAMES DefaultATRTimeframe = PERIOD_CURRENT; // ATRTimeframe: Default timeframe for ATR. +input bool DefaultSpreadAdjustmentSL = false; // SpreadAdjustmentSL: Adjust SL by Spread value in ATR mode. +input bool DefaultSpreadAdjustmentTP = false; // SpreadAdjustmentTP: Adjust TP by Spread value in ATR mode. +input double DefaultCommission = 0; // Commission: Default one-way commission per 1 lot. +input COMMISSION_TYPE DefaultCommissionType = COMMISSION_CURRENCY; // CommissionType: Default commission type. +input ACCOUNT_BUTTON DefaultAccountButton = Balance; // AccountButton: Balance/Equity/Balance-CPR +input double DefaultRisk = 1; // Risk: Initial risk tolerance in percentage points +input double DefaultMoneyRisk = 0; // MoneyRisk: If > 0, money risk tolerance in currency. +input double DefaultPositionSize = 0; // PositionSize: If > 0, position size in lots. +input INCLUDE_ORDERS DefaultIncludeOrders = INCLUDE_ORDERS_ALL; // IncludeOrders: Include which orders for portfolio risk? +input bool DefaultIgnoreOrdersWithoutSL = false; // IgnoreOrdersWithoutSL: Ignore orders w/o SL in portfolio risk. +input bool DefaultIgnoreOrdersWithoutTP = false; // IgnoreOrdersWithoutTP: Ignore orders w/o TP in portfolio risk. +input INCLUDE_SYMBOLS DefaultIncludeSymbols = INCLUDE_SYMBOLS_ALL; // IncludeSymbols: Include trades in which symbols for portfolio risk? +input INCLUDE_DIRECTIONS DefaultIncludeDirections = INCLUDE_DIRECTIONS_ALL; // IncludeDirections: Include which directions for portfolio risk? +input double DefaultCustomLeverage = 0; // CustomLeverage: Default custom leverage for Margin tab. +input int DefaultMagicNumber = 2022052714; // MagicNumber: Default magic number for Trading tab. +input string DefaultCommentary = ""; // Commentary: Default order comment for Trading tab. +input bool DefaultCommentAutoSuffix = false; // AutoSuffix: Automatic suffix for order comment in Trading tab. +input bool DefaultCommentBalance = false; // CommentBalance: Add current balance in front of order comment? +input bool DefaultDisableTradingWhenLinesAreHidden = false; // DisableTradingWhenLinesAreHidden: for Trading tab. +input int DefaultMaxSlippage = 0; // MaxSlippage: Maximum slippage for Trading tab. +input int DefaultMaxSpread = 0; // MaxSpread: Maximum spread for Trading tab. +input int DefaultMaxEntrySLDistance = 0; // MaxEntrySLDistance: Maximum entry/SL distance for Trading tab. +input int DefaultMinEntrySLDistance = 0; // MinEntrySLDistance: Minimum entry/SL distance for Trading tab. +input double DefaultMaxRiskPercentage = 0; // MaxRiskPercentage: Maximum risk % for Trading tab. +input double DefaultMaxMarginPerc = 0; // MaxMarginPerc: Maximum margin % for Trading tab. +input double DefaultMaxPositionSizeTotal = 0; // Maximum position size total for Trading tab. +input double DefaultMaxPositionSizePerSymbol = 0; // Maximum position size per symbol for Trading tab. +input bool DefaultSubtractOPV = false; // SubtractOPV: Subtract open positions volume (Trading tab). +input bool DefaultSubtractPOV = false; // SubtractPOV: Subtract pending orders volume (Trading tab). +input bool DefaultDoNotApplyStopLoss = false; // DoNotApplyStopLoss: Don't apply SL for Trading tab. +input bool DefaultDoNotApplyTakeProfit = false; // DoNotApplyTakeProfit: Don't apply TP for Trading tab. +input bool DefaultAskForConfirmation = true; // AskForConfirmation: Ask for confirmation for Trading tab. +input int DefaultPanelPositionX = 0; // PanelPositionX: Panel's X coordinate. +input int DefaultPanelPositionY = 15; // PanelPositionY: Panel's Y coordinate. +input ENUM_BASE_CORNER DefaultPanelPositionCorner = CORNER_LEFT_UPPER; // PanelPositionCorner: Panel's corner. +input bool DefaultTPLockedOnSL = false; // TPLockedOnSL: Lock TP to (multiplied) SL distance. +input int DefaultTrailingStop = 0; // TrailingStop: For the Trading tab. +input int DefaultBreakEven = 0; // BreakEven: For the Trading tab. +input int DefaultExpiryMinutes = 0; // ExpiryMinutes: Pending order expiration in minutes. Min = 10. +input int DefaultMaxNumberOfTradesTotal = 0; // MaxNumberOfTradesTotal: For the Trading tab. 0 - no limit. +input int DefaultMaxNumberOfTradesPerSymbol = 0; // MaxNumberOfTradesPerSymbol: For the Trading tab. 0 - no limit. +input double DefaultMaxRiskTotal = 0; // MaxRiskTotal: For the Trading tab. 0 - no limit. +input double DefaultMaxRiskPerSymbol = 0; // MaxRiskPerSymbol: For the Trading tab. 0 - no limit. +input double DefaultMaxMarginPercTotal = 0; // MaxMarginPercTotal: For the Trading tab. 0 - no limit. +input double DefaultMaxMarginPercPerSymbol = 0; // MaxMarginPercPerSymbol: For the Trading tab. 0 - no limit. +input bool DefaultSLDistanceInPoints = false; // SLDistanceInPoints: SL distance in points instead of a level. +input bool DefaultTPDistanceInPoints = false; // TPDistanceInPoints: TP distance in points instead of a level. +input MARGIN_UTILIZATION_BASE DefaultMarginUtilizationBase = MUB_BALANCE; // Margin utilization base. +input double DefaultMUBStartingBalance = 0; // Starting balance for margin utilization base. +input group "Keyboard shortcuts" +input string ____Keyboard_Shortcuts = "Case-insensitive hotkey. Supports Ctrl, Shift."; +input string TradeHotKey = "T"; // TradeHotKey: Execute a trade. +input string SwitchOrderTypeHotKey = "O"; // SwitchOrderTypeHotKey: Switch order type. +input string SwitchEntryDirectionHotKey = "TAB"; // SwitchEntryDirectionHotKey: Switch entry direction. +input string SwitchHideShowLinesHotKey = "H"; // SwitchHideShowLinesHotKey: Switch Hide/Show lines. +input string SetStopLossHotKey = "S"; // SetStopLossHotKey: Set SL to where mouse pointer is. +input string SetTakeProfitHotKey = "P"; // SetTakeProfitHotKey: Set TP to where mouse pointer is. +input string SetEntryHotKey = "E"; // SetEntryHotKey: Set Entry to where mouse pointer is. +input string MinimizeMaximizeHotkey = "`"; // MinimizeMaximizeHotkey: Minimize/maximize the panel. +input string SwitchSLPointsLevelHotKey = "Shift+S"; // SwitchSLPointsLevelHotKey: Switch SL between points and level. +input string SwitchTPPointsLevelHotKey = "Shift+P"; // SwitchTPPointsLevelHotKey: Switch TP between points and level. +input group "Miscellaneous" +input string ____Miscellaneous = ""; +input double TP_Multiplier = 1; // TP Multiplier for SL value (for take-profit button). +input bool UseCommissionToSetTPDistance = false; // UseCommissionToSetTPDistance: For TP button. +input SHOW_SPREAD ShowSpread = No; // ShowSpread: Show current spread in points or as an SL ratio. +input double AdditionalFunds = 0; // AdditionalFunds: Added to account balance for risk calculation. +input double CustomBalance = 0; // CustomBalance: Overrides AdditionalFunds value. +input CANDLE_NUMBER ATRCandle = Current_Candle; // ATRCandle: Candle to get ATR value from. +input bool CalculateUnadjustedPositionSize = false; // CalculateUnadjustedPositionSize: Ignore broker's restrictions. +input bool SurpassBrokerMaxPositionSize = false; // Surpass Broker Max Position Size with multiple trades. +input bool RoundDown = true; // RoundDown: Position size and potential reward are rounded down. +input double QuickRisk1 = 0; // QuickRisk1: First quick risk button, in percentage points. +input double QuickRisk2 = 0; // QuickRisk2: Second quick risk button, in percentage points. +input string ObjectPrefix = "PS_"; // ObjectPrefix: To prevent confusion with other indicators/EAs. +input SYMBOL_CHART_CHANGE_REACTION SymbolChange = SYMBOL_CHART_CHANGE_EACH_OWN; // SymbolChange: What to do with the panel on chart symbol change? +input bool DisableTradingSounds = false; // DisableTradingSounds: If true, sound will be off for trading actions. +input bool IgnoreMarketExecutionMode = true; // IgnoreMarketExecutionMode: If true, ignore Market execution. +input bool MarketModeApplySLTPAfterAllTradesExecuted = false; // Market Mode: Apply SL/TP after all trades executed. +input bool DarkMode = false; // DarkMode: Enable dark mode for a less bright panel. +input string SettingsFile = ""; // SettingsFile: Custom settings file from \Files\PS_Settings\ +input bool PrefillAdditionalTPsBasedOnMain = true; // Prefill additional TPs based on Main? +input bool AskBeforeClosing = false; // Ask for confirmation before closing the panel? +input bool CapMaxPositionSizeBasedOnMargin = false; // Cap position size based on available margin? +input bool LessRestrictiveMaxLimits = false; // Allow smaller trades when trading limits are exceeded? +input color LongButtonColor = CONTROLS_BUTTON_COLOR_BG; // Long Button Color +input color ShortButtonColor = CONTROLS_BUTTON_COLOR_BG; // Short Button Color +input color TradeButtonColor = CONTROLS_BUTTON_COLOR_BG; // Trade Button Color +input bool DoNotDeleteLinesLabels = false; // Do Not Delete Lines/Labels on deinitialization? + +CPositionSizeCalculator* ExtDialog; + +// Global variables: +bool Dont_Move_the_Panel_to_Default_Corner_X_Y; +uint LastRecalculationTime = 0; +bool StopLossLineIsBeingMoved = false; +bool TakeProfitLineIsBeingMoved[]; // Separate for each TP. +bool NeedToCheckToggleScaleOffOn; +int PrevChartWidth = -1; +int DeinitializationReason = -1; +string OldSymbol = ""; +int OldTakeProfitsNumber = -1; +int Mouse_Last_X = 0, Mouse_Last_Y = 0; // For SL/TP hotkeys. +color LongButtonColorAdjusted, ShortButtonColorAdjusted, TradeButtonColorAdjusted; // Based on the DarkMode setting. +HotkeyDef Hotkeys[HK_COUNT]; + +int OnInit() +{ + if (DarkMode) + { + CONTROLS_EDIT_COLOR_ENABLE = DARKMODE_EDIT_BG_COLOR; + CONTROLS_EDIT_COLOR_DISABLE = 0x999999; + CONTROLS_BUTTON_COLOR_ENABLE = DARKMODE_BUTTON_BG_COLOR; + CONTROLS_BUTTON_COLOR_DISABLE = 0x919999; + } + else + { + CONTROLS_EDIT_COLOR_ENABLE = C'255,255,255'; + CONTROLS_EDIT_COLOR_DISABLE = C'221,221,211'; + CONTROLS_BUTTON_COLOR_ENABLE = C'200,200,200'; + CONTROLS_BUTTON_COLOR_DISABLE = C'224,224,224'; + } + if (LongButtonColor == CONTROLS_BUTTON_COLOR_BG) // Default color is used. + { + if (DarkMode) LongButtonColorAdjusted = DARKMODE_BUTTON_BG_COLOR; + else LongButtonColorAdjusted = CONTROLS_BUTTON_COLOR_BG; + } + else + { + LongButtonColorAdjusted = LongButtonColor; + } + if (ShortButtonColor == CONTROLS_BUTTON_COLOR_BG) // Default color is used. + { + if (DarkMode) ShortButtonColorAdjusted = DARKMODE_BUTTON_BG_COLOR; + else ShortButtonColorAdjusted = CONTROLS_BUTTON_COLOR_BG; + } + else + { + ShortButtonColorAdjusted = ShortButtonColor; + } + if (TradeButtonColor == CONTROLS_BUTTON_COLOR_BG) // Default color is used. + { + if (DarkMode) TradeButtonColorAdjusted = DARKMODE_BUTTON_BG_COLOR; + else TradeButtonColorAdjusted = CONTROLS_BUTTON_COLOR_BG; + } + else + { + TradeButtonColorAdjusted = TradeButtonColor; + } + + TickSize = -1; + + if (DeinitializationReason != REASON_CHARTCHANGE) ExtDialog = new CPositionSizeCalculator; // Create the panel only if it is not a symbol/timeframe change. + else OldTakeProfitsNumber = sets.TakeProfitsNumber; // Will be used to resize the panel if needed when switching symbols in some modes. + + MathSrand(GetTickCount() + 293029); // Used by CreateInstanceId() in Dialog.mqh (standard library). Keep the second number unique across other panel indicators/EAs. + + if (SettingsFile != "") // Load a custom settings file if given via input parameters. + { + ExtDialog.SetFileName(SettingsFile); + } + + Dont_Move_the_Panel_to_Default_Corner_X_Y = true; + + PanelCaptionBase = "Position Sizer (ver. " + VERSION + ")"; + + // Symbol changed. + if ((DeinitializationReason == REASON_CHARTCHANGE) && (OldSymbol != _Symbol)) + { + ObjectsDeleteAll(0, ObjectPrefix, -1, OBJ_HLINE); // All lines should be deleted, so that they could be recreated at new sets. values. + if (SymbolChange == SYMBOL_CHART_CHANGE_EACH_OWN) + { + ExtDialog.SaveSettingsOnDisk(OldSymbol); // Save old symbol's settings. + } + ExtDialog.UpdateFileName(); // Update the filename. + + // Reset everything. + OutputPointValue = ""; OutputSwapsType = TRANSLATION_LABEL_UNKNOWN; SwapsTripleDay = "?"; + OutputSwapsDailyLongLot = "?"; OutputSwapsDailyShortLot = "?"; OutputSwapsDailyLongPS = "?"; OutputSwapsDailyShortPS = "?"; + OutputSwapsYearlyLongLot = "?"; OutputSwapsYearlyShortLot = "?"; OutputSwapsYearlyLongPS = "?"; OutputSwapsYearlyShortPS = "?"; + OutputSwapsCurrencyDailyLot = ""; OutputSwapsCurrencyDailyPS = ""; OutputSwapsCurrencyYearlyLot = ""; OutputSwapsCurrencyYearlyPS = ""; + ReferenceSymbol = NULL; SwapConversionSymbol = ""; AdditionalReferenceSymbol = NULL; + WarnedAboutZeroUnitCost = 0; + + NeedToCheckToggleScaleOffOn = true; + + if (SymbolChange == SYMBOL_CHART_CHANGE_HARD_RESET) + { + // Lines are treated as a part of the panel. + if (DefaultLinesSelected) LinesSelectedStatus = 1; // Flip lines to selected. + else LinesSelectedStatus = 2; // Flip lines to unselected. + } + } + bool is_InitControlsValues_required = false; + // Normal attempt to load settings fails (attempted in not chart change case and in chart change case with 'each pair own settings' case + if ((((DeinitializationReason != REASON_CHARTCHANGE) || ((DeinitializationReason == REASON_CHARTCHANGE) && (OldSymbol != _Symbol) && (SymbolChange == SYMBOL_CHART_CHANGE_EACH_OWN))) && (!ExtDialog.LoadSettingsFromDisk())) + // OR chart change with hard_reset configured and with symbol change. + || ((DeinitializationReason == REASON_CHARTCHANGE) && (SymbolChange == SYMBOL_CHART_CHANGE_HARD_RESET) && (OldSymbol != _Symbol))) + { + sets.TradeDirection = DefaultTradeDirection; + sets.EntryLevel = EntryLevel; + sets.StopLossLevel = StopLossLevel; + sets.TakeProfitLevel = TakeProfitLevel; // Optional + sets.TPMultiplier = TP_Multiplier; + sets.TakeProfitsNumber = DefaultTakeProfitsNumber; + if (sets.TakeProfitsNumber < 1) sets.TakeProfitsNumber = 1; // At least one TP. + ArrayResize(sets.TP, sets.TakeProfitsNumber); + ArrayResize(sets.TPShare, sets.TakeProfitsNumber); + ArrayResize(TakeProfitLineIsBeingMoved, sets.TakeProfitsNumber); + ArrayInitialize(sets.TP, 0); + ArrayInitialize(sets.TPShare, 100 / sets.TakeProfitsNumber); + ArrayResize(sets.WasSelectedAdditionalTakeProfitLine, sets.TakeProfitsNumber - 1); // -1 because the flag for the main TP is saved elsewhere. + sets.ATRPeriod = DefaultATRPeriod; + sets.ATRMultiplierSL = DefaultATRMultiplierSL; + sets.ATRMultiplierTP = DefaultATRMultiplierTP; + sets.ATRTimeframe = DefaultATRTimeframe; + sets.EntryType = DefaultEntryType; // If Instant, Entry level will be updated to current Ask/Bid price automatically; if Pending, Entry level will remain intact and StopLevel warning will be issued if needed. + sets.Risk = DefaultRisk; // Risk tolerance in percentage points + sets.MoneyRisk = DefaultMoneyRisk; // Risk tolerance in account currency + if (DefaultMoneyRisk > 0) sets.UseMoneyInsteadOfPercentage = true; + else sets.UseMoneyInsteadOfPercentage = false; + if (DefaultPositionSize > 0) + { + sets.RiskFromPositionSize = true; + sets.PositionSize = DefaultPositionSize; + OutputPositionSize = DefaultPositionSize; + } + else sets.RiskFromPositionSize = false; + sets.CommissionPerLot = DefaultCommission; // Commission charged per lot (one side) in account currency or %. + sets.CommissionType = DefaultCommissionType; + sets.CustomBalance = CustomBalance; + sets.AccountButton = DefaultAccountButton; + sets.IncludeOrders = DefaultIncludeOrders; // Will portfolio risk calculation include all orders? + sets.IgnoreOrdersWithoutSL = DefaultIgnoreOrdersWithoutSL; // If true, portfolio risk calculation will skip orders without stop-loss. + sets.IgnoreOrdersWithoutTP = DefaultIgnoreOrdersWithoutTP; // If true, portfolio risk calculation will skip orders without take-profit. + sets.IncludeSymbols = DefaultIncludeSymbols; // Include all symbols in portfolio risk calculation? + sets.IncludeDirections = DefaultIncludeDirections; // Include all trade directions in portfolio risk calculation? + sets.HideAccSize = HideAccSize; // If true, account size line will not be shown. + sets.ShowLines = DefaultShowLines; + sets.SelectedTab = MainTab; + sets.CustomLeverage = DefaultCustomLeverage; + sets.MagicNumber = DefaultMagicNumber; + sets.Commentary = DefaultCommentary; + sets.CommentAutoSuffix = DefaultCommentAutoSuffix; + sets.DisableTradingWhenLinesAreHidden = DefaultDisableTradingWhenLinesAreHidden; + if (sets.TakeProfitsNumber > 1) + { + for (int i = 0; i < sets.TakeProfitsNumber; i++) + { + sets.TP[i] = TakeProfitLevel; + sets.TPShare[i] = 100 / sets.TakeProfitsNumber; + } + } + sets.MaxSlippage = DefaultMaxSlippage; + sets.MaxSpread = DefaultMaxSpread; + sets.MaxEntrySLDistance = DefaultMaxEntrySLDistance; + sets.MinEntrySLDistance = DefaultMinEntrySLDistance; + sets.MaxPositionSizeTotal = DefaultMaxPositionSizeTotal; + sets.MaxPositionSizePerSymbol = DefaultMaxPositionSizePerSymbol; + sets.MaxRiskPercentage = DefaultMaxRiskPercentage; + sets.MaxMarginPerc = DefaultMaxMarginPerc; + if ((sets.MaxPositionSizeTotal < sets.MaxPositionSizePerSymbol) && (sets.MaxPositionSizeTotal != 0)) sets.MaxPositionSizeTotal = sets.MaxPositionSizePerSymbol; + sets.StopLoss = 0; + sets.TakeProfit = 0; + sets.SubtractPendingOrders = DefaultSubtractPOV; + sets.SubtractPositions = DefaultSubtractOPV; + sets.DoNotApplyStopLoss = DefaultDoNotApplyStopLoss; + sets.DoNotApplyTakeProfit = DefaultDoNotApplyTakeProfit; + sets.AskForConfirmation = DefaultAskForConfirmation; + sets.WasSelectedEntryLine = false; + sets.WasSelectedStopLossLine = false; + sets.WasSelectedTakeProfitLine = false; + sets.IsPanelMinimized = false; + sets.TPLockedOnSL = DefaultTPLockedOnSL; + sets.TrailingStopPoints = DefaultTrailingStop; + sets.BreakEvenPoints = DefaultBreakEven; + sets.ExpiryMinutes = DefaultExpiryMinutes; + if ((sets.ExpiryMinutes != 0) && (sets.ExpiryMinutes < 10)) sets.ExpiryMinutes = 0; + sets.MaxNumberOfTradesTotal = DefaultMaxNumberOfTradesTotal; + sets.MaxNumberOfTradesPerSymbol = DefaultMaxNumberOfTradesPerSymbol; + if ((sets.MaxNumberOfTradesTotal < sets.MaxNumberOfTradesPerSymbol) && (sets.MaxNumberOfTradesTotal != 0)) sets.MaxNumberOfTradesTotal = sets.MaxNumberOfTradesPerSymbol; + sets.MaxRiskTotal = DefaultMaxRiskTotal; + sets.MaxRiskPerSymbol = DefaultMaxRiskPerSymbol; + if ((sets.MaxRiskTotal < sets.MaxRiskPerSymbol) && (sets.MaxRiskTotal != 0)) sets.MaxRiskTotal = sets.MaxRiskPerSymbol; + sets.MaxMarginPercTotal = DefaultMaxMarginPercTotal; + sets.MaxMarginPercPerSymbol = DefaultMaxMarginPercPerSymbol; + if ((sets.MaxMarginPercTotal < sets.MaxMarginPercPerSymbol) && (sets.MaxMarginPercTotal != 0)) sets.MaxMarginPercTotal = sets.MaxMarginPercPerSymbol; + sets.ShareVolumeMode = Decreasing; + sets.TemplateChanged = false; + sets.SLDistanceInPoints = DefaultSLDistanceInPoints; + sets.TPDistanceInPoints = DefaultTPDistanceInPoints; + sets.MarginUtilizationBase = DefaultMarginUtilizationBase; + sets.MUBStartingBalance = DefaultMUBStartingBalance; + if (DeinitializationReason == REASON_CHARTCHANGE) is_InitControlsValues_required = true; + sets.LastAdditionalTPScheme = ADDITIONAL_TP_SCHEME_OUTWARD; + } + if (sets.TakeProfitsNumber < 1) // Read an old settings file with absent or bogus TakeProfitNumber parameter + { + sets.TakeProfitsNumber = 1; // At least one TP. + ArrayResize(sets.TP, sets.TakeProfitsNumber); + ArrayResize(sets.TPShare, sets.TakeProfitsNumber); + ArrayResize(TakeProfitLineIsBeingMoved, sets.TakeProfitsNumber); + ArrayInitialize(sets.TP, 0); + ArrayInitialize(sets.TPShare, 100 / sets.TakeProfitsNumber); + ArrayResize(sets.WasSelectedAdditionalTakeProfitLine, sets.TakeProfitsNumber - 1); // -1 because the flag for the main TP is saved elsewhere. + } + if (DeinitializationReason != REASON_CHARTCHANGE) + { + if (!ExtDialog.Create(0, "Position Sizer (ver. " + VERSION + ")", 0, DefaultPanelPositionX, DefaultPanelPositionY)) return INIT_FAILED; + ExtDialog.Run(); + + // No ini file - move the panel according to the inputs. + if (!FileIsExist(ExtDialog.IniFileName() + ExtDialog.IniFileExt())) + { + Dont_Move_the_Panel_to_Default_Corner_X_Y = false; + } + ExtDialog.IniFileLoad(); + + // If a hotkey is given, break up the string to check for hotkey presses in OnChartEvent(). + SetupHotkey(TradeHotKey, Hotkeys[HK_Trade]); + SetupHotkey(SwitchEntryDirectionHotKey, Hotkeys[HK_SwitchEntryDirection]); + SetupHotkey(SwitchOrderTypeHotKey, Hotkeys[HK_SwitchOrderType]); + SetupHotkey(SwitchHideShowLinesHotKey, Hotkeys[HK_SwitchHideShowLines]); + SetupHotkey(SetStopLossHotKey, Hotkeys[HK_SetStopLoss]); + SetupHotkey(SetTakeProfitHotKey, Hotkeys[HK_SetTakeProfit]); + SetupHotkey(SetEntryHotKey, Hotkeys[HK_SetEntry]); + SetupHotkey(SwitchSLPointsLevelHotKey, Hotkeys[HK_SwitchSLPointsLevel]); + SetupHotkey(SwitchTPPointsLevelHotKey, Hotkeys[HK_SwitchTPPointsLevel]); + SetupHotkey(MinimizeMaximizeHotkey, Hotkeys[HK_MinimizeMaximize]); + } + else if (OldSymbol != _Symbol) + { + if (SymbolChange == SYMBOL_CHART_CHANGE_HARD_RESET) // Reset Entry, SL, and all TPs if it was a symbol change and a hard reset is required. + { + sets.EntryLevel = 0; + sets.StopLossLevel = 0; + sets.StopLoss = 0; + sets.TakeProfitLevel = 0; + sets.TakeProfit = 0; + for (int i = 0; i < sets.TakeProfitsNumber; i++) + { + sets.TP[i] = 0; + } + Dont_Move_the_Panel_to_Default_Corner_X_Y = false; + } + } + // Avoid re-initialization on timeframe change and on symbol change with the 'keep panel' setting. + if ((DeinitializationReason != REASON_CHARTCHANGE) || ((DeinitializationReason == REASON_CHARTCHANGE) && (OldSymbol != _Symbol) && ((SymbolChange == SYMBOL_CHART_CHANGE_HARD_RESET) || (SymbolChange == SYMBOL_CHART_CHANGE_EACH_OWN)))) + { + if (DeinitializationReason == REASON_CHARTCHANGE) // Do not run if it is not the symbol change because 'CPositionSizeCalculator::Create()' takes care of that in other cases. + { + // Remove extra empty space on the panel when going from a panel with more TPs to a panel with fewer TPs. + if (sets.TakeProfitsNumber < OldTakeProfitsNumber) + { + Initialization(); + int NewTakeProfitsNumber = sets.TakeProfitsNumber; + sets.TakeProfitsNumber = OldTakeProfitsNumber; // Used and decremented inside OnClickBtnTakeProfitsNumberRemove(). + while (sets.TakeProfitsNumber > NewTakeProfitsNumber) + { + ExtDialog.OnClickBtnTakeProfitsNumberRemove(); + } + } + else// if (DeinitializationReason == REASON_CHARTCHANGE) // Do not run if it is not the symbol change because 'CPositionSizeCalculator::Create()' takes care of that in other cases. + { + // Create necessary panel elements if newly loaded symbol has more TPs. + int NewTakeProfitsNumber = sets.TakeProfitsNumber; + sets.TakeProfitsNumber = OldTakeProfitsNumber; // It will be increased inside OnClickBtnTakeProfitsNumberAdd(). + while (sets.TakeProfitsNumber < NewTakeProfitsNumber) + { + ExtDialog.OnClickBtnTakeProfitsNumberAdd(); + } + // These should be executed only after all TP arrays are properly resized. + Initialization(); + ExtDialog.IniFileLoad(); // InitObjects(); is skipped because it will be done after adding all the TP chart objects. + } + } + else Initialization(); + // Brings panel on top of other objects without actual maximization of the panel. + ExtDialog.HideShowMaximize(); + } + + if (!Dont_Move_the_Panel_to_Default_Corner_X_Y) + { + int new_x = DefaultPanelPositionX, new_y = DefaultPanelPositionY; + int chart_width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); + int chart_height = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); + int panel_width = ExtDialog.Width(); + int panel_height = ExtDialog.Height(); + + // Invert coordinate if necessary. + if (DefaultPanelPositionCorner == CORNER_LEFT_LOWER) + { + new_y = chart_height - panel_height - new_y; + } + else if (DefaultPanelPositionCorner == CORNER_RIGHT_UPPER) + { + new_x = chart_width - panel_width - new_x; + } + else if (DefaultPanelPositionCorner == CORNER_RIGHT_LOWER) + { + new_x = chart_width - panel_width - new_x; + new_y = chart_height - panel_height - new_y; + } + ExtDialog.remember_left = new_x; + ExtDialog.remember_top = new_y; + ExtDialog.Move(new_x, new_y); + ExtDialog.FixatePanelPosition(); // Remember the panel's new position for the INI file. + } + + if ((StartPanelMinimized) && (!ExtDialog.IsMinimized()) && (!Dont_Move_the_Panel_to_Default_Corner_X_Y)) // Minimize only if needs minimization. We check Dont_Move_the_Panel_to_Default_Corner_X_Y to make sure we didn't load an INI-file. An INI-file already contains a more preferred state for the panel. + { + // No access to the minmax button, no way to edit the chart height. + // Dummy variables for passing as references. + long lparam = 0; + double dparam = 0; + string sparam = ""; + // Increasing the height of the panel beyond that of the chart will trigger its minimization. + ExtDialog.Height((int)ChartGetInteger(ChartID(), CHART_HEIGHT_IN_PIXELS) + 1); + // Call the chart event processing function. + ExtDialog.ChartEvent(CHARTEVENT_CHART_CHANGE, lparam, dparam, sparam); + } + + if (!IsVisualMode()) // The timer doesn't work in MT4 Strategy Tester. + { + if (!EventSetTimer(1)) Print(TRANSLATION_MESSAGE_ERROR_SETTING_TIMER + ": ", GetLastError()); + } + + if (DarkMode) + { + int total = ObjectsTotal(ChartID()); + for (int i = 0; i < total; i++) + { + string obj_name = ObjectName(ChartID(), i); + if (StringSubstr(obj_name, 0, StringLen(ExtDialog.Name())) != ExtDialog.Name()) continue; // Skip non-panel objects. + if (obj_name == ExtDialog.Name() + "Back") + { + + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BGCOLOR, DARKMODE_BG_DARK_COLOR); + } + if (obj_name == ExtDialog.Name() + "Caption") + { + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BGCOLOR, DARKMODE_BG_DARK_COLOR); + ObjectSetInteger(ChartID(), obj_name, OBJPROP_COLOR, DARKMODE_CONTROL_BORDER_COLOR); + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BORDER_COLOR, DARKMODE_BG_DARK_COLOR); + } + else if (obj_name == ExtDialog.Name() + "ClientBack") + { + ObjectSetInteger(ChartID(), obj_name, OBJPROP_COLOR, DARKMODE_MAIN_AREA_BORDER_COLOR); + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BGCOLOR, DARKMODE_MAIN_AREA_BG_COLOR); + } + else if (obj_name == ExtDialog.Name() + "m_BtnEntry") // Long/Short + { + if (sets.TradeDirection == Long) + { + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BGCOLOR, LongButtonColorAdjusted); + } + else if (sets.TradeDirection == Short) + { + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BGCOLOR, ShortButtonColorAdjusted); + } + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BORDER_COLOR, DARKMODE_CONTROL_BORDER_COLOR); + } + else if ((obj_name == ExtDialog.Name() + "m_BtnMainTrade") || (obj_name == ExtDialog.Name() + "m_BtnTrade") || (obj_name == ExtDialog.Name() + "m_OutsideTradeButton")) // Any of the Trade buttons (Main tab, Trading tab, outside). + { + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BGCOLOR, TradeButtonColorAdjusted); + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BORDER_COLOR, DARKMODE_CONTROL_BORDER_COLOR); + } + else if (StringSubstr(obj_name, 0, StringLen(ExtDialog.Name() + "m_Edt")) == ExtDialog.Name() + "m_Edt") + { + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BGCOLOR, DARKMODE_EDIT_BG_COLOR); + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BORDER_COLOR, DARKMODE_CONTROL_BORDER_COLOR); + } + else if (StringSubstr(obj_name, 0, StringLen(ExtDialog.Name() + "m_Btn")) == ExtDialog.Name() + "m_Btn") + { + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BGCOLOR, DARKMODE_BUTTON_BG_COLOR); + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BORDER_COLOR, DARKMODE_CONTROL_BORDER_COLOR); + } + else if (StringSubstr(obj_name, 0, StringLen(ExtDialog.Name() + "m_Chk")) == ExtDialog.Name() + "m_Chk") + { + ObjectSetInteger(ChartID(), obj_name, OBJPROP_COLOR, DARKMODE_TEXT_COLOR); + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BGCOLOR, DARKMODE_MAIN_AREA_BG_COLOR); + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BORDER_COLOR, DARKMODE_MAIN_AREA_BG_COLOR); + } + else if (StringSubstr(obj_name, 0, StringLen(ExtDialog.Name() + "m_Rgp")) == ExtDialog.Name() + "m_Rgp") + { + if (ObjectGetInteger(ChartID(), obj_name, OBJPROP_TYPE) == OBJ_RECTANGLE_LABEL) ObjectSetInteger(ChartID(), obj_name, OBJPROP_COLOR, DARKMODE_MAIN_AREA_BG_COLOR); + else ObjectSetInteger(ChartID(), obj_name, OBJPROP_COLOR, DARKMODE_TEXT_COLOR); + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BGCOLOR, DARKMODE_MAIN_AREA_BG_COLOR); + ObjectSetInteger(ChartID(), obj_name, OBJPROP_BORDER_COLOR, DARKMODE_MAIN_AREA_BG_COLOR); + } + else + { + if (obj_name == ExtDialog.Name() + "m_LblURL") ObjectSetInteger(ChartID(), obj_name, OBJPROP_COLOR, 0x224400); + else ObjectSetInteger(ChartID(), obj_name, OBJPROP_COLOR, DARKMODE_TEXT_COLOR); + } + } + } + + // If symbol change with a reset was enacted. + if (is_InitControlsValues_required) ExtDialog.InitControlsValues(); + + return INIT_SUCCEEDED; +} + +void OnDeinit(const int reason) +{ + DeinitializationReason = reason; // Remember reason to avoid recreating the panel in the OnInit() if it is not deleted here. + + EventKillTimer(); + + if (reason == REASON_TEMPLATE) sets.TemplateChanged = true; // Will be used to select lines according to the DefaultLinesSelected input parameter. + + if ((reason == REASON_CLOSE) || (reason == REASON_REMOVE) || (reason == REASON_CHARTCLOSE) || (reason == REASON_PROGRAM)) + { + if (!DoNotDeleteLinesLabels) ObjectsDeleteAll(0, ObjectPrefix); // Delete all lines if platform was closed. + if ((reason == REASON_REMOVE) || (reason == REASON_PROGRAM)) + { + if (SettingsFile == "") ExtDialog.DeleteSettingsFile(); + if (!FileDelete(ExtDialog.IniFileName() + ExtDialog.IniFileExt())) Print(TRANSLATION_MESSAGE_FAILED_DELETE_INI + ": ", GetLastError()); + } + } + + // It is deinitialization due to input parameters change - save current parameters values (that are also changed via panel) to global variables. + if (reason == REASON_PARAMETERS) GlobalVariableSet("PS-" + IntegerToString(ChartID()) + "-Parameters", 1); + + if ((reason != REASON_CHARTCHANGE) && (reason != REASON_REMOVE) && (reason != REASON_PROGRAM)) + { + ExtDialog.SaveSettingsOnDisk(); + ExtDialog.IniFileSave(); + } + + if (reason == REASON_CHARTCHANGE) + { + OldSymbol = _Symbol; + PrevChartWidth = (int)ChartGetInteger(ChartID(), CHART_WIDTH_IN_PIXELS); // Chart width is used to detect going from wide-quote symbol to a narrower one. + } + else + { + if (!DoNotDeleteLinesLabels) + { + ObjectDelete(0, ObjectPrefix + "StopLossLabel"); + ObjectDelete(0, ObjectPrefix + "EntryLabel"); + ObjectsDeleteAll(0, ObjectPrefix + "TakeProfitLabel", -1, OBJ_LABEL); + ObjectsDeleteAll(0, ObjectPrefix + "TPAdditionalLabel", -1, OBJ_LABEL); + ObjectDelete(0, ObjectPrefix + "SLAdditionalLabel"); + ObjectDelete(0, ObjectPrefix + "EntryAdditionalLabel"); + } + ExtDialog.Destroy(); + delete ExtDialog; + } + + if (!DoNotDeleteLinesLabels) ObjectsDeleteAll(0, ObjectPrefix + "BE"); // Delete all BE lines and labels. +} + +void OnTick() +{ + if (IsVisualMode()) // Visual backtesting. + { + ListenToChartEvents(ExtDialog.Name()); // Check and generate chart events in Strategy Tester. + ExtDialog.UpdateStrategyTesterTrades(); + } + + ExtDialog.RefreshValues(); + + if (sets.TrailingStopPoints > 0) DoTrailingStop(); +} + +void OnChartEvent(const int id, + const long &lparam, + const double &dparam, + const string &sparam) +{ + if (id == CHARTEVENT_MOUSE_MOVE) + { + Mouse_Last_X = (int)lparam; + Mouse_Last_Y = (int)dparam; + if (((uint)sparam & 1) == 1) // While left mouse button is down. + { + if ((sets.SLDistanceInPoints) || ((ShowATROptions) && (sets.ATRMultiplierSL > 0))) + { + double current_line_price = NormalizeDouble(ObjectGetDouble(ChartID(), ObjectPrefix + "StopLossLine", OBJPROP_PRICE, 0), _Digits); + if (MathAbs(current_line_price - tStopLossLevel) > _Point / 2.0) // != for doubles. + { + StopLossLineIsBeingMoved = true; + } + else StopLossLineIsBeingMoved = false; + } + if ((sets.TPDistanceInPoints) || ((ShowATROptions) && (sets.ATRMultiplierTP > 0))) + { + ArrayInitialize(TakeProfitLineIsBeingMoved, false); + double current_line_price = NormalizeDouble(ObjectGetDouble(ChartID(), ObjectPrefix + "TakeProfitLine", OBJPROP_PRICE, 0), _Digits); + if (MathAbs(current_line_price - tTakeProfitLevel) > _Point / 2.0) // != for doubles. + { + TakeProfitLineIsBeingMoved[0] = true; + } + // Additional take-profits. + else + { + for (int i = 1; i < sets.TakeProfitsNumber; i++) // Will fire only if sets.TakeProfitsNumber > 1. + { + if (sets.TP[i] != 0) // With zero points TP, keep the TP lines at zero level - as with the main TP level. + { + current_line_price = NormalizeDouble(ObjectGetDouble(ChartID(), ObjectPrefix + "TakeProfitLine" + IntegerToString(i), OBJPROP_PRICE, 0), _Digits); + if (MathAbs(current_line_price - sets.TP[i]) > _Point / 2.0) // != for doubles. + { + TakeProfitLineIsBeingMoved[i] = true; + break; + } + } + } + } + } + } + } + + // Some buttons cannot be processed using the panel's event handler because they aren't added to the panel's list of controls. + if (id == CHARTEVENT_OBJECT_CLICK) + { + // Outside trade button: + if (sparam == ExtDialog.Name() + "m_OutsideTradeButton") + { + ExtDialog.m_OutsideTradeButton.Pressed(false); + Trade(); + } + else if (IsVisualMode()) + { + // Button to switch the corner of the outside close buttons. + if (sparam == ExtDialog.Name() + "m_BtnOutsideCloseButtonsSwitchButton") + { + ExtDialog.ProcessOutsideCloseButtonsSwitchClick(); + } + // Outside close button: + else if (StringSubstr(sparam, 0, StringLen(ExtDialog.Name() + "m_BtnOutsideClose")) == ExtDialog.Name() + "m_BtnOutsideClose") + { + int i = (int)StringToInteger(StringSubstr(sparam, StringLen(ExtDialog.Name() + "m_BtnOutsideClose"))); + ExtDialog.ProcessOutsideCloseButtonClick(i); + } + } + } + + if (id == CHARTEVENT_CLICK) // Avoid "sticking" of xxxLineIsBeingMoved variables. + { + StopLossLineIsBeingMoved = false; + ArrayInitialize(TakeProfitLineIsBeingMoved, false); + } + + // Remember the panel's location to have the same location for minimized and maximized states. + if ((id == CHARTEVENT_CUSTOM + ON_DRAG_END) && (lparam == -1)) + { + ExtDialog.remember_top = ExtDialog.Top(); + ExtDialog.remember_left = ExtDialog.Left(); + } + + // Catch multiple TP fields. + if (sets.TakeProfitsNumber > 1) + { + if (id == CHARTEVENT_CUSTOM + ON_END_EDIT) + { + // Additional take-profit field #N on Main tab. + if (StringSubstr(sparam, 0, StringLen(ExtDialog.Name() + "m_EdtAdditionalTPEdits")) == ExtDialog.Name() + "m_EdtAdditionalTPEdits") + { + int i = (int)StringToInteger(StringSubstr(sparam, StringLen(ExtDialog.Name() + "m_EdtAdditionalTPEdits"))) - 1; + ExtDialog.UpdateAdditionalTPEdit(i); + } + // Take-profit field #N on Trading tab. + else if (StringSubstr(sparam, 0, StringLen(ExtDialog.Name() + "m_EdtTradingTPEdit")) == ExtDialog.Name() + "m_EdtTradingTPEdit") + { + int i = (int)StringToInteger(StringSubstr(sparam, StringLen(ExtDialog.Name() + "m_EdtTradingTPEdit"))) - 1; + ExtDialog.UpdateTradingTPEdit(i); + } + // Trading take-profit share field #N on Trading tab. + else if (StringSubstr(sparam, 0, StringLen(ExtDialog.Name() + "m_EdtTradingTPShareEdit")) == ExtDialog.Name() + "m_EdtTradingTPShareEdit") + { + int i = (int)StringToInteger(StringSubstr(sparam, StringLen(ExtDialog.Name() + "m_EdtTradingTPShareEdit"))) - 1; + ExtDialog.UpdateTradingTPShareEdit(i); + } + } + else if (id == CHARTEVENT_CUSTOM + ON_CLICK) + { + // Additional take-profit increase button #N on Main tab. + if (StringSubstr(sparam, 0, StringLen(ExtDialog.Name() + "m_BtnAdditionalTPButtonsIncrease")) == ExtDialog.Name() + "m_BtnAdditionalTPButtonsIncrease") + { + int i = (int)StringToInteger(StringSubstr(sparam, StringLen(ExtDialog.Name() + "m_BtnAdditionalTPButtonsIncrease"))) - 1; + ExtDialog.ProcessAdditionalTPButtonsIncrease(i); + } + // Additional take-profit decrease button #N on Main tab. + else if (StringSubstr(sparam, 0, StringLen(ExtDialog.Name() + "m_BtnAdditionalTPButtonsDecrease")) == ExtDialog.Name() + "m_BtnAdditionalTPButtonsDecrease") + { + int i = (int)StringToInteger(StringSubstr(sparam, StringLen(ExtDialog.Name() + "m_BtnAdditionalTPButtonsDecrease"))) - 1; + ExtDialog.ProcessAdditionalTPButtonsDecrease(i); + } + // Because there is a bug that keeps a control's Id() = -1 if it is created after the panel is initialized. So, it cannot be processed with the panel's event processor. + else if (sparam == ExtDialog.Name() + "m_BtnTakeProfitsNumberRemove") + { + ExtDialog.OnClickBtnTakeProfitsNumberRemove(); + } + else if (sparam == ExtDialog.Name() + "m_BtnTPsInward") + { + ExtDialog.OnClickBtnTPsInward(); + } + else if (sparam == ExtDialog.Name() + "m_BtnTPsOutward") + { + ExtDialog.OnClickBtnTPsOutward(); + } + else if (sparam == ExtDialog.Name() + "m_BtnTradingTPShare") + { + ExtDialog.OnClickBtnTradingTPShare(); + } + } + } + + if (id == CHARTEVENT_KEYDOWN) + { + short key = (short)lparam; + if (key < 65 || (key > 90 && key < 97) || key > 122) // Not a capital or normal letter. + { + // Get Unicode key value. + key = TranslateKey((int)lparam); + // In case of failure, use raw value. + if (key == -1) key = (short)lparam; + } + + // Trade direction: + if (HotkeyPressed(Hotkeys[HK_SwitchEntryDirection], key)) + { + SwitchEntryDirection(); + } + // Order type: + else if (HotkeyPressed(Hotkeys[HK_SwitchOrderType], key)) + { + ExtDialog.OnClickBtnOrderType(); + } + // Hide/Show lines: + else if (HotkeyPressed(Hotkeys[HK_SwitchHideShowLines], key)) + { + ExtDialog.OnClickBtnLines(); + } + // Trade: + else if (HotkeyPressed(Hotkeys[HK_Trade], key)) + { + Trade(); + } + // Set stop-loss: + else if (HotkeyPressed(Hotkeys[HK_SetStopLoss], key)) + { + // Capture point price location. + int subwindow; + double price; + datetime time; // Dummy. + ChartXYToTimePrice(ChartID(), Mouse_Last_X, Mouse_Last_Y, subwindow, time, price); + // If valid, move the SL line there. + if ((subwindow == 0) && (price > 0)) + { + if (TickSize > 0) price = NormalizeDouble(MathRound(price / TickSize) * TickSize, _Digits); + ObjectSetDouble(ChartID(), ObjectPrefix + "StopLossLine", OBJPROP_PRICE, price); + if ((sets.SLDistanceInPoints) || (ShowATROptions)) ExtDialog.UpdateFixedSL(); + ExtDialog.RefreshValues(); + } + } + // Set take-profit: + else if (HotkeyPressed(Hotkeys[HK_SetTakeProfit], key)) + { + // Capture point price location. + int subwindow; + double price; + datetime time; // Dummy. + ChartXYToTimePrice(ChartID(), Mouse_Last_X, Mouse_Last_Y, subwindow, time, price); + // If valid, move the TP line there. + if ((subwindow == 0) && (price > 0)) + { + // If "TP locked on SL" mode was on, turn it off. + if (sets.TPLockedOnSL) + { + sets.TPLockedOnSL = false; + ObjectSetInteger(ChartID(), ObjectPrefix + "TakeProfitLine", OBJPROP_SELECTABLE, true); + ObjectSetInteger(ChartID(), ObjectPrefix + "TakeProfitLine", OBJPROP_SELECTED, sets.WasSelectedTakeProfitLine); + ExtDialog.ResetChkTPLockedOnSL(); + } + if (TickSize > 0) price = NormalizeDouble(MathRound(price / TickSize) * TickSize, _Digits); + ObjectSetDouble(ChartID(), ObjectPrefix + "TakeProfitLine", OBJPROP_PRICE, price); + if ((sets.TPDistanceInPoints) || (ShowATROptions)) ExtDialog.UpdateFixedTP(); + ExtDialog.ShowTPRelatedEdits(); + ExtDialog.RefreshValues(); + if ((PrefillAdditionalTPsBasedOnMain) && (sets.TakeProfitsNumber > 1)) + { + ExtDialog.DoPrefillAdditionalTPsBasedOnMain(); + } + ExtDialog.HideShowMaximize(); + ExtDialog.MoveAndResize(); + } + } + // Set entry: + else if (HotkeyPressed(Hotkeys[HK_SetEntry], key)) + { + // Capture point price location. + int subwindow; + double price; + datetime time; // Dummy. + ChartXYToTimePrice(ChartID(), Mouse_Last_X, Mouse_Last_Y, subwindow, time, price); + // If valid, move the Entry line there and switch from Instant to Pending if necessary. + if ((subwindow == 0) && (price > 0)) + { + if (TickSize > 0) price = NormalizeDouble(MathRound(price / TickSize) * TickSize, _Digits); + ObjectSetDouble(ChartID(), ObjectPrefix + "EntryLine", OBJPROP_PRICE, price); + if (sets.EntryType == Instant) + { + ExtDialog.OnClickBtnOrderType(); // Includes RefreshValues(). + } + else ExtDialog.RefreshValues(); + } + } + // Minimize/maximize: + else if (HotkeyPressed(Hotkeys[HK_MinimizeMaximize], key)) + { + ExtDialog.EmulateMinMaxClick(); + } + // Switch SL between points and level: + else if (HotkeyPressed(Hotkeys[HK_SwitchSLPointsLevel], key)) + { + if (sets.SLDistanceInPoints) sets.SLDistanceInPoints = false; // If was in points, set to level. + else + { + sets.SLDistanceInPoints = true; // If was in level, set to points. + sets.StopLoss = (int)MathRound(MathAbs(sets.StopLossLevel - sets.EntryLevel) / _Point); + } + ExtDialog.RefreshValues(); + } + // Switch TP between points and level: + else if (HotkeyPressed(Hotkeys[HK_SwitchTPPointsLevel], key)) + { + if (sets.TPDistanceInPoints) sets.TPDistanceInPoints = false; // If was in points, set to level. + else + { + sets.TPDistanceInPoints = true; // If was in level, set to points. + if (sets.TakeProfitLevel != 0) sets.TakeProfit = (int)MathRound(MathAbs(sets.TakeProfitLevel - sets.EntryLevel) / _Point); + // Additional take-profits. + if (sets.TakeProfitsNumber > 1) + { + for (int i = 1; i < sets.TakeProfitsNumber; i++) + { + if (sets.TP[i] != 0) // With zero points TP, keep the TP lines at zero level - as with the main TP level. + { + if (sets.TP[i] != 0) ExtDialog.AdditionalTPEdits[i - 1].Text(DoubleToString(MathAbs(MathRound((sets.TP[i] - sets.EntryLevel) / _Point)), 0)); + } + } + } + } + ExtDialog.RefreshValues(); + } + } + + // Call Panel's event handler only if it is not a CHARTEVENT_CHART_CHANGE - workaround for minimization bug on chart switch. + if (id != CHARTEVENT_CHART_CHANGE) ExtDialog.OnEvent(id, lparam, dparam, sparam); + + // Recalculate on chart changes, clicks, and certain object dragging. + if ((id == CHARTEVENT_CLICK) || (id == CHARTEVENT_CHART_CHANGE) || ((id == CHARTEVENT_OBJECT_DRAG) && + ((sparam == ObjectPrefix + "EntryLine") || (sparam == ObjectPrefix + "StopLossLine") || (StringFind(sparam, ObjectPrefix + "TakeProfitLine") != -1)))) + { + // Moving lines when fixed SL/TP distance is enabled. Should set a new fixed SL/TP distance. + if ((id == CHARTEVENT_OBJECT_DRAG) && ((sets.SLDistanceInPoints) || (sets.TPDistanceInPoints) || (ShowATROptions))) + { + if (sparam == ObjectPrefix + "StopLossLine") ExtDialog.UpdateFixedSL(); + else if (sparam == ObjectPrefix + "TakeProfitLine") ExtDialog.UpdateFixedTP(); + else if ((sets.TakeProfitsNumber > 1) && (StringFind(sparam, ObjectPrefix + "TakeProfitLine") != -1)) + { + int len = StringLen(ObjectPrefix + "TakeProfitLine"); + int i = (int)StringToInteger(StringSubstr(sparam, len)); + if (i >= 1) ExtDialog.UpdateAdditionalFixedTP(i); // Prevents accessing AdditionalTPEdits[] at -1 if a stray object with a similar name is found. + } + } + + if (sparam == ObjectPrefix + "StopLossLine") StopLossLineIsBeingMoved = false; // In any case ending moving state for the stop-loss line. + if (StringFind(sparam, ObjectPrefix + "TakeProfitLine") != -1) ArrayInitialize(TakeProfitLineIsBeingMoved, false); // In any case ending moving state for the take-profit line. + + if (id == CHARTEVENT_CHART_CHANGE) ChartWidth = ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); + else ExtDialog.RefreshValues(); + + static bool prev_chart_on_top = false; + // If this is an active chart, make sure the panel is visible (not behind the chart's borders). For inactive chart, this will work poorly, because inactive charts get minimized by MetaTrader. + if (ChartGetInteger(ChartID(), CHART_BRING_TO_TOP)) + { + if (ExtDialog.Top() < 0) ExtDialog.Move(ExtDialog.Left(), 0); + int chart_height = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); + if (ExtDialog.Top() > chart_height) ExtDialog.Move(ExtDialog.Left(), chart_height - ExtDialog.Height()); + int chart_width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); + if (ExtDialog.Left() > chart_width) ExtDialog.Move(chart_width - ExtDialog.Width(), ExtDialog.Top()); + // If chart was brought on top, refresh values to move labels. + if ((prev_chart_on_top == false) && ((ShowMainLineLabels) || (ShowAdditionalEntryLabel) || (ShowAdditionalTPLabel) || (ShowAdditionalSLLabel))) ExtDialog.RefreshValues(); + } + // Remember if the chart is on top or is minimized. + prev_chart_on_top = ChartGetInteger(ChartID(), CHART_BRING_TO_TOP); + ChartRedraw(); + } +} + +//+------------------------------------------------------------------+ +//| Trade event handler | +//+------------------------------------------------------------------+ +void OnTrade() +{ + ExtDialog.RefreshValues(); + ChartRedraw(); +} + +//+------------------------------------------------------------------+ +//| Timer event handler | +//+------------------------------------------------------------------+ +void OnTimer() +{ + /** + * Release resource 50ms to prevent freeze + * when change symbols or close Position Sizer + * */ + if (GetTickCount() - LastRecalculationTime < 50) return; + if (NeedToCheckToggleScaleOffOn) + { + if ((double)ChartGetInteger(ChartID(), CHART_WIDTH_IN_PIXELS) != PrevChartWidth) + { + // Toggle price scale off and then on to return it to its original size. + // This can be useful when switching from symbol with a wide price scale (index, BTC, etc.) to one with a narrow scale (EUR/USD). + ChartSetInteger(ChartID(), CHART_SHOW_PRICE_SCALE, false); + ChartSetInteger(ChartID(), CHART_SHOW_PRICE_SCALE, true); + } + NeedToCheckToggleScaleOffOn = false; + } + ExtDialog.CheckAndRestoreLines(); // Check if any lines should be restored. + if (GetTickCount() - LastRecalculationTime < 1000) return; // Do not recalculate on timer if less than 1 second passed. + ExtDialog.RefreshValues(); + ChartRedraw(); +} +//+------------------------------------------------------------------+ \ No newline at end of file diff --git a/57-Position-Sizer/Position-Sizer.pdf b/57-Position-Sizer/Position-Sizer.pdf new file mode 100644 index 0000000..33834f7 Binary files /dev/null and b/57-Position-Sizer/Position-Sizer.pdf differ diff --git a/58-Chart-Pattern-Helper/Chart-Pattern-Helper.mq4 b/58-Chart-Pattern-Helper/Chart-Pattern-Helper.mq4 new file mode 100644 index 0000000..0f2575e --- /dev/null +++ b/58-Chart-Pattern-Helper/Chart-Pattern-Helper.mq4 @@ -0,0 +1,1657 @@ +//+------------------------------------------------------------------+ +//| Chart Pattern Helper | +//| Copyright © 2024, EarnForex.com | +//| https://www.earnforex.com/ | +//+------------------------------------------------------------------+ +#property copyright "Copyright © 2024, EarnForex" +#property link "https://www.earnforex.com/metatrader-expert-advisors/ChartPatternHelper/" +#property version "1.15" +#property strict + +#include + +#property description "Uses graphic objects (horizontal/trend lines, channels) to enter trades." +#property description "Works in two modes:" +#property description "1. Price is below upper entry and above lower entry. Only one or two pending stop orders are used." +#property description "2. Price is above upper entry or below lower entry. Only one pending limit order is used." +#property description "If an object is deleted/renamed after the pending order was placed, order will be canceled." +#property description "Pending order is removed if opposite entry is triggered." +#property description "Generally, it is safe to turn off the EA at any point." + +input group "Objects" +input string UpperBorderLine = "UpperBorder"; +input string UpperEntryLine = "UpperEntry"; +input string UpperTPLine = "UpperTP"; +input string LowerBorderLine = "LowerBorder"; +input string LowerEntryLine = "LowerEntry"; +input string LowerTPLine = "LowerTP"; +// The pattern may be given as trend/horizontal lines or equidistant channels. +input string BorderChannel = "Border"; +input string EntryChannel = "Entry"; +input string TPChannel = "TP"; +input group "Order management" +// In case Channel is used for Entry, pending orders will be removed even if OneCancelsOther = false. +input bool OneCancelsOther = true; // OneCancelsOther: Remove opposite orders once position is open? +// If true, spread will be added to Buy entry level and Sell SL/TP levels. It compensates the difference when Ask price is used, while all chart objects are drawn at Bid level. +input bool UseSpreadAdjustment = false; // UseSpreadAdjustment: Add spread to Buy entry and Sell SL/TP? +// Not all brokers support expiration. +input bool UseExpiration = true; // UseExpiration: Use expiration on pending orders? +input bool DisableBuyOrders = false; // DisableBuyOrders: Disable new and ignore existing buy trades? +input bool DisableSellOrders = false; // DisableSellOrders: Disable new and ignore existing sell trades? +// If true, the EA will try to adjust SL after breakout candle is complete as it may no longer qualify for SL; it will make SL more precise but will mess up the money management a bit. +input bool PostEntrySLAdjustment = false; // PostEntrySLAdjustment: Adjust SL after entry? +input bool UseDistantSL = false; // UseDistantSL: If true, set SL to pattern's farthest point. +input group "Trendline trading" +input bool OpenOnCloseAboveBelowTrendline = false; // Open trade on close above/below trendline. +input string SLLine = "SL"; // Stop-loss line name for trendline trading. +input int ThresholdSpreads = 10; // Threshold Spreads: number of spreads for minimum distance. +input group "Position sizing" +input bool CalculatePositionSize = true; // CalculatePositionSize: Use money management module? +input bool UpdatePendingVolume = true; // UpdatePendingVolume: If true, recalculate pending order volume. +input double FixedPositionSize = 0.01; // FixedPositionSize: Used if CalculatePositionSize = false. +input double Risk = 1; // Risk: Risk tolerance in percentage points. +input double MoneyRisk = 0; // MoneyRisk: Risk tolerance in base currency. +input bool UseMoneyInsteadOfPercentage = false; +input bool UseEquityInsteadOfBalance = false; +input double FixedBalance = 0; // FixedBalance: If > 0, trade size calc. uses it as balance. +input group "Miscellaneous" +input int Magic = 20200530; +input int Slippage = 30; // Slippage: Maximum slippage in broker's pips. +input bool Silent = false; // Silent: If true, does not display any output via chart comment. +input bool ErrorLogging = true; // ErrorLogging: If true, errors will be logged to file. + +// Global variables: +bool UseUpper, UseLower; +double UpperSL, UpperEntry, UpperTP, LowerSL, LowerEntry, LowerTP; +int UpperTicket, LowerTicket; +bool HaveBuyPending = false; +bool HaveSellPending = false; +bool HaveBuy = false; +bool HaveSell = false; +bool TCBusy = false; +bool PostBuySLAdjustmentDone = false, PostSellSLAdjustmentDone = false; +// For tick value adjustment: +string ProfitCurrency = "", account_currency = "", BaseCurrency = "", ReferenceSymbol = NULL, AdditionalReferenceSymbol = NULL; +bool ReferenceSymbolMode, AdditionalReferenceSymbolMode; +int ProfitCalcMode; +double TickSize; + +// For error logging: +string filename; + +void OnInit() +{ + FindObjects(); + if (ErrorLogging) + { + datetime tl = TimeLocal(); + string mon = IntegerToString(TimeMonth(tl)); + if (StringLen(mon) == 1) mon = "0" + mon; + string day = IntegerToString(TimeDay(tl)); + if (StringLen(day) == 1) day = "0" + day; + string hour = IntegerToString(TimeHour(tl)); + if (StringLen(hour) == 1) hour = "0" + hour; + string min = IntegerToString(TimeMinute(tl)); + if (StringLen(min) == 1) min = "0" + min; + string sec = IntegerToString(TimeSeconds(tl)); + if (StringLen(sec) == 1) sec = "0" + sec; + filename = "CPH-Errors-" + IntegerToString(TimeYear(tl)) + mon + day + hour + min + sec + ".log"; + } +} + +void OnDeinit(const int reason) +{ + SetComment(""); +} + +void OnTick() +{ + FindOrders(); + FindObjects(); + AdjustOrders(); // And delete the ones no longer needed. +} + +// Finds Entry, Border and TP objects. Detects respective levels according to found objects. Outputs found values to chart comment. +void FindObjects() +{ + string c1 = FindUpperObjects(); + string c2 = FindLowerObjects(); + + SetComment(c1 + c2); +} + +// Adjustment for Ask/Bid spread is made for entry level as Long positions are entered at Ask, while all objects are drawn at Bid. +string FindUpperObjects() +{ + string c = ""; // Text for chart comment + + if (DisableBuyOrders) + { + UseUpper = false; + return "\nBuy orders disabled via input parameters."; + } + + UseUpper = true; + + // Entry + if (OpenOnCloseAboveBelowTrendline) // Simple trendline entry doesn't need an entry line. + { + c = c + "\nUpper entry unnecessary."; + } + else if (ObjectFind(UpperEntryLine) > -1) + { + if ((ObjectType(UpperEntryLine) != OBJ_HLINE) && (ObjectType(UpperEntryLine) != OBJ_TREND)) + { + Alert("Upper Entry Line should be either OBJ_HLINE or OBJ_TREND."); + return("\nWrong Upper Entry Line object type."); + } + if (ObjectType(UpperEntryLine) != OBJ_HLINE) UpperEntry = NormalizeDouble(ObjectGetValueByShift(UpperEntryLine, 0), Digits); + else UpperEntry = NormalizeDouble(ObjectGet(UpperEntryLine, OBJPROP_PRICE1), Digits); // Horizontal line value + if (UseSpreadAdjustment) UpperEntry = NormalizeDouble(UpperEntry + MarketInfo(Symbol(), MODE_SPREAD) * Point, Digits); + ObjectSet(UpperEntryLine, OBJPROP_RAY, true); + c = c + "\nUpper entry found. Level: " + DoubleToStr(UpperEntry, Digits); + } + else + { + if (ObjectFind(EntryChannel) > -1) + { + if (ObjectType(EntryChannel) != OBJ_CHANNEL) + { + Alert("Entry Channel should be OBJ_CHANNEL."); + return "\nWrong Entry Channel object type."; + } + UpperEntry = NormalizeDouble(FindUpperEntryViaChannel(), Digits); + if (UseSpreadAdjustment) UpperEntry = NormalizeDouble(UpperEntry + MarketInfo(Symbol(), MODE_SPREAD) * Point, Digits); + ObjectSet(EntryChannel, OBJPROP_RAY, true); + c = c + "\nUpper entry found (via channel). Level: " + DoubleToStr(UpperEntry, Digits); + } + else + { + c = c + "\nUpper entry not found. No new position will be entered."; + UseUpper = false; + } + } + + // Border + if (ObjectFind(UpperBorderLine) > -1) + { + if ((ObjectType(UpperBorderLine) != OBJ_HLINE) && (ObjectType(UpperBorderLine) != OBJ_TREND)) + { + Alert("Upper Border Line should be either OBJ_HLINE or OBJ_TREND."); + return "\nWrong Upper Border Line object type."; + } + // Find upper SL + UpperSL = FindUpperSL(); + ObjectSet(UpperBorderLine, OBJPROP_RAY, true); + c = c + "\nUpper border found. Upper stop-loss level: " + DoubleToStr(UpperSL, Digits); + } + else // Try to find a channel. + { + if (ObjectFind(BorderChannel) > -1) + { + if (ObjectType(BorderChannel) != OBJ_CHANNEL) + { + Alert("Border Channel should be OBJ_CHANNEL."); + return "\nWrong Border Channel object type."; + } + // Find upper SL + UpperSL = FindUpperSLViaChannel(); + ObjectSet(BorderChannel, OBJPROP_RAY, true); + c = c + "\nUpper border found (via channel). Upper stop-loss level: " + DoubleToStr(UpperSL, Digits); + } + else + { + c = c + "\nUpper border not found."; + if ((CalculatePositionSize) && (!HaveBuy)) + { + UseUpper = false; + c = c + " Cannot trade without stop-loss, while CalculatePositionSize set to true."; + } + else + { + c = c + " Stop-loss won\'t be applied to new positions."; + // Track current SL, possibly installed by user. + if ((OrderSelect(UpperTicket, SELECT_BY_TICKET)) && ((OrderType() == OP_BUYSTOP) || (OrderType() == OP_BUYLIMIT))) + { + UpperSL = OrderStopLoss(); + } + } + } + } + // Adjust upper SL for tick size granularity. + TickSize = MarketInfo(Symbol(), MODE_TICKSIZE); + UpperSL = NormalizeDouble(MathRound(UpperSL / TickSize) * TickSize, _Digits); + + // Take-profit. + if (ObjectFind(UpperTPLine) > -1) + { + if ((ObjectType(UpperTPLine) != OBJ_HLINE) && (ObjectType(UpperTPLine) != OBJ_TREND)) + { + Alert("Upper TP Line should be either OBJ_HLINE or OBJ_TREND."); + return "\nWrong Upper TP Line object type."; + } + if (ObjectType(UpperTPLine) != OBJ_HLINE) UpperTP = NormalizeDouble(ObjectGetValueByShift(UpperTPLine, 0), Digits); + else UpperTP = NormalizeDouble(ObjectGet(UpperTPLine, OBJPROP_PRICE1), Digits); // Horizontal line value + ObjectSet(UpperTPLine, OBJPROP_RAY, true); + c = c + "\nUpper take-profit found. Level: " + DoubleToStr(UpperTP, Digits); + } + else + { + if (ObjectFind(TPChannel) > -1) + { + if (ObjectType(TPChannel) != OBJ_CHANNEL) + { + Alert("TP Channel should be OBJ_CHANNEL."); + return "\nWrong TP Channel object type."; + } + UpperTP = FindUpperTPViaChannel(); + ObjectSet(TPChannel, OBJPROP_RAY, true); + c = c + "\nUpper TP found (via channel). Level: " + DoubleToStr(UpperTP, Digits); + } + else + { + c = c + "\nUpper take-profit not found. Take-profit won\'t be applied to new positions."; + // Track current TP, possibly installed by user + if ((OrderSelect(UpperTicket, SELECT_BY_TICKET)) && ((OrderType() == OP_BUYSTOP) || (OrderType() == OP_BUYLIMIT))) + { + UpperTP = OrderTakeProfit(); + } + } + } + // Adjust upper TP for tick size granularity. + UpperTP = NormalizeDouble(MathRound(UpperTP / TickSize) * TickSize, _Digits); + + return c; +} + +// Adjustment for Ask/Bid spread is made for exit levels (SL and TP) as Short positions are exited at Ask, while all objects are drawn at Bid. +string FindLowerObjects() +{ + string c = ""; // Text for chart comment + + if (DisableSellOrders) + { + UseLower = false; + return "\nSell orders disabled via input parameters."; + } + + UseLower = true; + + // Entry. + if (OpenOnCloseAboveBelowTrendline) // Simple trendline entry doesn't need an entry line. + { + c = c + "\nLower entry unnecessary."; + } + else if (ObjectFind(LowerEntryLine) > -1) + { + if ((ObjectType(LowerEntryLine) != OBJ_HLINE) && (ObjectType(LowerEntryLine) != OBJ_TREND)) + { + Alert("Lower Entry Line should be either OBJ_HLINE or OBJ_TREND."); + return "\nWrong Lower Entry Line object type."; + } + if (ObjectType(LowerEntryLine) != OBJ_HLINE) LowerEntry = NormalizeDouble(ObjectGetValueByShift(LowerEntryLine, 0), Digits); + else LowerEntry = NormalizeDouble(ObjectGet(LowerEntryLine, OBJPROP_PRICE1), Digits); // Horizontal line value + ObjectSet(LowerEntryLine, OBJPROP_RAY, true); + c = c + "\nLower entry found. Level: " + DoubleToStr(LowerEntry, Digits); + } + else + { + if (ObjectFind(EntryChannel) > -1) + { + if (ObjectType(EntryChannel) != OBJ_CHANNEL) + { + Alert("Entry Channel should be OBJ_CHANNEL."); + return "\nWrong Entry Channel object type."; + } + LowerEntry = FindLowerEntryViaChannel(); + ObjectSet(EntryChannel, OBJPROP_RAY, true); + c = c + "\nLower entry found (via channel). Level: " + DoubleToStr(LowerEntry, Digits); + } + else + { + c = c + "\nLower entry not found. No new position will be entered."; + UseLower = false; + } + } + + // Border. + if (ObjectFind(LowerBorderLine) > -1) + { + if ((ObjectType(LowerBorderLine) != OBJ_HLINE) && (ObjectType(LowerBorderLine) != OBJ_TREND)) + { + Alert("Lower Border Line should be either OBJ_HLINE or OBJ_TREND."); + return "\nWrong Lower Border Line object type."; + } + // Find Lower SL. + LowerSL = NormalizeDouble(FindLowerSL(), Digits); + if (UseSpreadAdjustment) LowerSL = NormalizeDouble(LowerSL + MarketInfo(Symbol(), MODE_SPREAD) * Point, Digits); + ObjectSet(LowerBorderLine, OBJPROP_RAY, true); + c = c + "\nLower border found. Lower stop-loss level: " + DoubleToStr(LowerSL, Digits); + } + else // Try to find a channel. + { + if (ObjectFind(BorderChannel) > -1) + { + if (ObjectType(BorderChannel) != OBJ_CHANNEL) + { + Alert("Border Channel should be OBJ_CHANNEL."); + return "\nWrong Border Channel object type."; + } + // Find Lower SL + LowerSL = NormalizeDouble(FindLowerSLViaChannel(), Digits); + if (UseSpreadAdjustment) LowerSL = NormalizeDouble(LowerSL + MarketInfo(Symbol(), MODE_SPREAD) * Point, Digits); + ObjectSet(BorderChannel, OBJPROP_RAY, true); + c = c + "\nLower border found (via channel). Lower stop-loss level: " + DoubleToStr(LowerSL, Digits); + } + else + { + c = c + "\nLower border not found."; + if ((CalculatePositionSize) && (!HaveSell)) + { + UseLower = false; + c = c + " Cannot trade without stop-loss, while CalculatePositionSize set to true."; + } + else + { + c = c + " Stop-loss won\'t be applied to new positions."; + // Track current SL, possibly installed by user. + if ((OrderSelect(LowerTicket, SELECT_BY_TICKET)) && ((OrderType() == OP_SELLSTOP) || (OrderType() == OP_SELLLIMIT))) + { + LowerSL = OrderStopLoss(); + } + } + } + } + // Adjust lower SL for tick size granularity. + TickSize = MarketInfo(Symbol(), MODE_TICKSIZE); + LowerSL = NormalizeDouble(MathRound(LowerSL / TickSize) * TickSize, _Digits); + + // Take-profit. + if (ObjectFind(LowerTPLine) > -1) + { + if ((ObjectType(LowerTPLine) != OBJ_HLINE) && (ObjectType(LowerTPLine) != OBJ_TREND)) + { + Alert("Lower TP Line should be either OBJ_HLINE or OBJ_TREND."); + return "\nWrong Lower TP Line object type."; + } + if (ObjectType(LowerTPLine) != OBJ_HLINE) LowerTP = NormalizeDouble(ObjectGetValueByShift(LowerTPLine, 0), Digits); + else LowerTP = NormalizeDouble(ObjectGet(LowerTPLine, OBJPROP_PRICE1), Digits); // Horizontal line value + if (UseSpreadAdjustment) LowerTP = NormalizeDouble(LowerTP + MarketInfo(Symbol(), MODE_SPREAD) * Point, Digits); + ObjectSet(LowerTPLine, OBJPROP_RAY, true); + c = c + "\nLower take-profit found. Level: " + DoubleToStr(LowerTP, Digits); + } + else + { + if (ObjectFind(TPChannel) > -1) + { + if (ObjectType(TPChannel) != OBJ_CHANNEL) + { + Alert("TP Channel should be OBJ_CHANNEL."); + return "\nWrong TP Channel object type."; + } + LowerTP = NormalizeDouble(FindLowerTPViaChannel(), Digits); + if (UseSpreadAdjustment) LowerTP = NormalizeDouble(LowerTP + MarketInfo(Symbol(), MODE_SPREAD) * Point, Digits); + ObjectSet(TPChannel, OBJPROP_RAY, true); + c = c + "\nLower TP found (via channel). Level: " + DoubleToStr(LowerTP, Digits); + } + else + { + c = c + "\nLower take-profit not found. Take-profit won\'t be applied to new positions."; + // Track current TP, possibly installed by user. + if ((OrderSelect(LowerTicket, SELECT_BY_TICKET)) && ((OrderType() == OP_SELLSTOP) || (OrderType() == OP_SELLLIMIT))) + { + LowerTP = OrderTakeProfit(); + } + } + } + // Adjust lower TP for tick size granularity. + LowerTP = NormalizeDouble(MathRound(LowerTP / TickSize) * TickSize, _Digits); + + return c; +} + +// Find SL using a border line - the low of the first bar with major part below border. +double FindUpperSL() +{ + // Invalid value will prevent order from executing in case something goes wrong. + double SL = -1; + + // Everything becomes much easier if the EA just needs to find the farthest opposite point of the pattern. + if (UseDistantSL) + { + // Horizontal line. + if (ObjectType(LowerBorderLine) == OBJ_HLINE) + { + return NormalizeDouble(ObjectGetDouble(0, LowerBorderLine, OBJPROP_PRICE1), Digits); + } + // Trend line. + else if (ObjectType(LowerBorderLine) == OBJ_TREND) + { + double price1 = ObjectGetDouble(0, LowerBorderLine, OBJPROP_PRICE1); + double price2 = ObjectGetDouble(0, LowerBorderLine, OBJPROP_PRICE2); + if (price1 < price2) return NormalizeDouble(price1, Digits); + else return NormalizeDouble(price2, Digits); + } + } + + // Easy stop-loss via a separate horizontal line when using trendline trading. + if (OpenOnCloseAboveBelowTrendline) + { + if (ObjectFind(0, SLLine) < 0) return -1; + return NormalizeDouble(ObjectGetDouble(0, SLLine, OBJPROP_PRICE1), Digits); + } + + for (int i = 0; i < Bars; i++) + { + double Border, Entry; + if (ObjectType(UpperBorderLine) != OBJ_HLINE) Border = ObjectGetValueByShift(UpperBorderLine, i); + else Border = ObjectGet(UpperBorderLine, OBJPROP_PRICE1); // Horizontal line value + if (ObjectType(UpperEntryLine) != OBJ_HLINE) Entry = ObjectGetValueByShift(UpperEntryLine, i); + else Entry = ObjectGet(UpperEntryLine, OBJPROP_PRICE1); // Horizontal line value + // Additional condition (Entry) checks whether _current_ candle may still have a bigger part within border before triggering entry. + // It is not possible if the current height inside border is not bigger than the distance from border to entry. + // It should not be checked for candles already completed. + // Additionally, if skipped the first bar because it could not potentially qualify, next bar's Low should be lower or equal to that of the first bar. + if ((Border - Low[i] > High[i] - Border) && ((Entry - Border < Border - Low[i]) || (i != 0)) && (Low[i] <= Low[0])) return NormalizeDouble(Low[i], Digits); + } + + return SL; +} + +// Find SL using a border line - the high of the first bar with major part above border. +double FindLowerSL() +{ + // Invalid value will prevent order from executing in case something goes wrong. + double SL = -1; + + // Everything becomes much easier if the EA just needs to find the farthest opposite point of the pattern. + if (UseDistantSL) + { + // Horizontal line. + if (ObjectType(UpperBorderLine) == OBJ_HLINE) + { + return NormalizeDouble(ObjectGetDouble(0, UpperBorderLine, OBJPROP_PRICE1), Digits); + } + // Trend line. + else if (ObjectType(UpperBorderLine) == OBJ_TREND) + { + double price1 = ObjectGetDouble(0, UpperBorderLine, OBJPROP_PRICE1); + double price2 = ObjectGetDouble(0, UpperBorderLine, OBJPROP_PRICE2); + if (price1 > price2) return NormalizeDouble(price1, Digits); + else return NormalizeDouble(price2, Digits); + } + } + + // Easy stop-loss via a separate horizontal line when using trendline trading. + if (OpenOnCloseAboveBelowTrendline) + { + if (ObjectFind(0, SLLine) < 0) return -1; + return NormalizeDouble(ObjectGetDouble(0, SLLine, OBJPROP_PRICE1), Digits); + } + + for (int i = 0; i < Bars; i++) + { + double Border, Entry; + if (ObjectType(LowerBorderLine) != OBJ_HLINE) Border = ObjectGetValueByShift(LowerBorderLine, i); + else Border = ObjectGet(LowerBorderLine, OBJPROP_PRICE1); // Horizontal line value + if (ObjectType(LowerEntryLine) != OBJ_HLINE) Entry = ObjectGetValueByShift(LowerEntryLine, i); + else Entry = ObjectGet(LowerEntryLine, OBJPROP_PRICE1); // Horizontal line value + // Additional condition (Entry) checks whether _current_ candle may still have a bigger part within border before triggering entry. + // It is not possible if the current height inside border is not bigger than the distance from border to entry. + // It should not be checked for candles already completed. + // Additionally, if skipped the first bar because it could not potentially qualify, next bar's High should be higher or equal to that of the first bar. + if ((High[i] - Border > Border - Low[i]) && ((Border - Entry < High[i] - Border) || (i != 0)) && (High[i] >= High[0])) + { + return NormalizeDouble(High[i], Digits); + } + } + + return SL; +} + +// Find SL using a border channel - the low of the first bar with major part below upper line. +double FindUpperSLViaChannel() +{ + // Invalid value will prevent order from executing in case something goes wrong. + double SL = -1; + + // Easy stop-loss via a separate horizontal line when using trendline trading. + if (OpenOnCloseAboveBelowTrendline) + { + if (ObjectFind(0, SLLine) < 0) return -1; + return NormalizeDouble(ObjectGetDouble(0, SLLine, OBJPROP_PRICE1), Digits); + } + + for (int i = 0; i < Bars(_Symbol, _Period); i++) + { + // Get the upper of main and auxiliary lines + double Border = MathMax(ObjectGetValueByTime(0, BorderChannel, Time[i], 0), ObjectGetValueByTime(0, BorderChannel, Time[i], 1)); + + // Additional condition (Entry) checks whether _current_ candle may still have a bigger part within border before triggering entry. + // It is not possible if the current height inside border is not bigger than the distance from border to entry. + // It should not be checked for candles already completed. + // Additionally, if skipped the first bar because it could not potentially qualify, next bar's Low should be lower or equal to that of the first bar. + if ((Border - Low[i] > High[i] - Border) && ((UpperEntry - Border < Border - Low[i]) || (i != 0)) && (Low[i] <= Low[0])) return(NormalizeDouble(Low[i], _Digits)); + } + + return(SL); +} + +// Find SL using a border channel - the high of the first bar with major part above upper line. +double FindLowerSLViaChannel() +{ + // Invalid value will prevent order from executing in case something goes wrong. + double SL = -1; + + // Easy stop-loss via a separate horizontal line when using trendline trading. + if (OpenOnCloseAboveBelowTrendline) + { + if (ObjectFind(0, SLLine) < 0) return(-1); + return NormalizeDouble(ObjectGetDouble(0, SLLine, OBJPROP_PRICE1), Digits); + } + + for (int i = 0; i < Bars(_Symbol, _Period); i++) + { + // Get the lower of main and auxiliary lines + double Border = MathMin(ObjectGetValueByTime(0, BorderChannel, Time[i], 0), ObjectGetValueByTime(0, BorderChannel, Time[i], 1)); + + // Additional condition (Entry) checks whether _current_ candle may still have a bigger part within border before triggering entry. + // It is not possible if the current height inside border is not bigger than the distance from border to entry. + // It should not be checked for candles already completed. + // Additionally, if skipped the first bar because it could not potentially qualify, next bar's High should be higher or equal to that of the first bar. + if ((High[i] - Border > Border - Low[i]) && ((Border - LowerEntry < High[i] - Border) || (i != 0)) && (High[i] >= High[0])) return(NormalizeDouble(High[i], _Digits)); + } + + return SL; +} + +// Find entry point using the entry channel. +double FindUpperEntryViaChannel() +{ + // Invalid value will prevent order from executing in case something goes wrong. + double Entry = -1; + + // Get the upper of main and auxiliary lines + Entry = MathMax(ObjectGetValueByTime(0, EntryChannel, Time[0], 0), ObjectGetValueByTime(0, EntryChannel, Time[0], 1)); + + return NormalizeDouble(Entry, _Digits); +} + +// Find entry point using the entry channel. +double FindLowerEntryViaChannel() +{ + // Invalid value will prevent order from executing in case something goes wrong. + double Entry = -1; + + // Get the lower of main and auxiliary lines + Entry = MathMin(ObjectGetValueByTime(0, EntryChannel, Time[0], 0), ObjectGetValueByTime(0, EntryChannel, Time[0], 1)); + + return NormalizeDouble(Entry, _Digits); +} + +// Find TP using the TP channel. +double FindUpperTPViaChannel() +{ + // Invalid value will prevent order from executing in case something goes wrong. + double TP = -1; + + // Get the upper of main and auxiliary lines. + TP = MathMax(ObjectGetValueByTime(0, TPChannel, Time[0], 0), ObjectGetValueByTime(0, TPChannel, Time[0], 1)); + + return NormalizeDouble(TP, _Digits); +} + +// Find TP using the TP channel. +double FindLowerTPViaChannel() +{ + // Invalid value will prevent order from executing in case something goes wrong. + double TP = -1; + + // Get the lower of main and auxiliary lines. + TP = MathMin(ObjectGetValueByTime(0, TPChannel, Time[0], 0), ObjectGetValueByTime(0, TPChannel, Time[0], 1)); + return NormalizeDouble(TP, _Digits); +} + +void AdjustOrders() +{ + AdjustObjects(); // Rename objects if pending orders got executed. + AdjustUpperAndLowerOrders(); +} + +// Sets flags according to found pending orders and positions. +void FindOrders() +{ + HaveBuyPending = false; + HaveSellPending = false; + HaveBuy = false; + HaveSell = false; + for (int i = 0; i < OrdersTotal(); i++) + { + if (OrderSelect(i, SELECT_BY_POS, MODE_TRADES) == false) continue; + if (((OrderType() == OP_BUYSTOP) || (OrderType() == OP_BUYLIMIT)) && (OrderSymbol() == Symbol()) && (OrderMagicNumber() == Magic)) + { + HaveBuyPending = true; + UpperTicket = OrderTicket(); + } + else if (((OrderType() == OP_SELLSTOP) || (OrderType() == OP_SELLLIMIT)) && (OrderSymbol() == Symbol()) && (OrderMagicNumber() == Magic)) + { + HaveSellPending = true; + LowerTicket = OrderTicket(); + } + else if ((OrderType() == OP_BUY) && (OrderSymbol() == Symbol()) && (OrderMagicNumber() == Magic)) HaveBuy = true; + else if ((OrderType() == OP_SELL) && (OrderSymbol() == Symbol()) && (OrderMagicNumber() == Magic)) HaveSell = true; + } +} + +// Renaming objects prevents new position opening. +void AdjustObjects() +{ + if (((HaveBuy) && (!HaveBuyPending))) + { + if ((ObjectFind(0, UpperBorderLine) >= 0) || (ObjectFind(0, EntryChannel) >= 0)) + { + Print("Buy position found, renaming chart objects..."); + RenameObject(UpperBorderLine); + RenameObject(UpperEntryLine); + RenameObject(EntryChannel); + } + if (OneCancelsOther) + { + if ((ObjectFind(0, LowerBorderLine) >= 0) || (ObjectFind(0, BorderChannel) >= 0)) + { + Print("OCO is on, renaming opposite chart objects..."); + RenameObject(LowerBorderLine); + RenameObject(LowerEntryLine); + RenameObject(BorderChannel); + } + } + } + if (((HaveSell) && (!HaveSellPending))) + { + if ((ObjectFind(0, LowerEntryLine) >= 0) || (ObjectFind(0, EntryChannel) >= 0)) + { + Print("Sell position found, renaming chart objects..."); + RenameObject(LowerBorderLine); + RenameObject(LowerEntryLine); + RenameObject(EntryChannel); + } + if (OneCancelsOther) + { + if ((ObjectFind(0, UpperBorderLine) >= 0) || (ObjectFind(0, BorderChannel) >= 0)) + { + Print("OCO is on, renaming opposite chart objects..."); + RenameObject(UpperBorderLine); + RenameObject(UpperEntryLine); + RenameObject(BorderChannel); + } + } + } +} + +void RenameObject(string Object) +{ + if (ObjectFind(0, Object) > -1) // If exists + { + Print("Renaming ", Object, "."); + // Get object's type, price/time coordinates, style properties. + ENUM_OBJECT OT = (ENUM_OBJECT)ObjectGetInteger(0, Object, OBJPROP_TYPE); + double Price1 = ObjectGetDouble(0, Object, OBJPROP_PRICE, 0); + datetime Time1 = 0; + double Price2 = 0; + datetime Time2 = 0; + double Price3 = 0; + datetime Time3 = 0; + if ((OT == OBJ_TREND) || (OT == OBJ_CHANNEL)) + { + Time1 = (datetime)ObjectGetInteger(0, Object, OBJPROP_TIME, 0); + Price2 = ObjectGetDouble(0, Object, OBJPROP_PRICE, 1); + Time2 = (datetime)ObjectGetInteger(0, Object, OBJPROP_TIME, 1); + if (OT == OBJ_CHANNEL) + { + Price3 = ObjectGetDouble(0, Object, OBJPROP_PRICE, 2); + Time3 = (datetime)ObjectGetInteger(0, Object, OBJPROP_TIME, 2); + } + } + color Color = (color)ObjectGetInteger(0, Object, OBJPROP_COLOR); + ENUM_LINE_STYLE Style = (ENUM_LINE_STYLE)ObjectGetInteger(0, Object, OBJPROP_STYLE); + int Width = (int)ObjectGetInteger(0, Object, OBJPROP_WIDTH); + + // Delete object. + ObjectDelete(0, Object); + string NewObject = Object + IntegerToString(Magic); + // Create the same object with new name and set the old style properties. + ObjectCreate(0, NewObject, OT, 0, Time1, Price1, Time2, Price2, Time3, Price3); + ObjectSetInteger(0, NewObject, OBJPROP_COLOR, Color); + ObjectSetInteger(0, NewObject, OBJPROP_STYLE, Style); + ObjectSetInteger(0, NewObject, OBJPROP_WIDTH, Width); + ObjectSetInteger(0, NewObject, OBJPROP_RAY, true); + } +} + +// The main trading procedure. Sends, Modifies and Deletes orders. +void AdjustUpperAndLowerOrders() +{ + double NewVolume; + int last_error; + datetime expiration; + int order_type; + string order_type_string; + + if ((!IsTradeAllowed()) || (IsTradeContextBusy()) || (!IsConnected()) || (!MarketInfo(Symbol(), MODE_TRADEALLOWED))) + { + if (!TCBusy) Output("Trading context is busy or disconnected."); + TCBusy = true; + return; + } + else if (TCBusy) + { + Output("Trading context is no longer busy or disconnected."); + TCBusy = false; + } + + double StopLevel = MarketInfo(Symbol(), MODE_STOPLEVEL) * Point; + double FreezeLevel = MarketInfo(Symbol(), MODE_FREEZELEVEL) * Point; + double LotStep = MarketInfo(Symbol(), MODE_LOTSTEP); + int LotStep_digits = CountDecimalPlaces(LotStep); + + if (UseExpiration) + { + // Set expiration to the end of the current bar. + expiration = Time[0] + Period() * 60; + // If expiration is less than 11 minutes from now, set it to at least 11 minutes from now. + // (Brokers have such limit.) + if (expiration - TimeCurrent() < 660) expiration = TimeCurrent() + 660; + } + else expiration = 0; + + if (OpenOnCloseAboveBelowTrendline) // Simple case. + { + double BorderLevel; + if ((LowerTP > 0) && (!HaveSell) && (UseLower)) // SELL. + { + if (ObjectFind(0, LowerBorderLine) >= 0) // Line. + { + if (ObjectGetInteger(ChartID(), LowerBorderLine, OBJPROP_TYPE) == OBJ_HLINE) BorderLevel = NormalizeDouble(ObjectGetDouble(0, LowerBorderLine, OBJPROP_PRICE1), _Digits); + else BorderLevel = NormalizeDouble(ObjectGetValueByShift(LowerBorderLine, 1), _Digits); + } + else // Channel + { + BorderLevel = MathMin(ObjectGetValueByTime(0, BorderChannel, Time[1], 0), ObjectGetValueByTime(0, BorderChannel, Time[1], 1)); + } + BorderLevel = NormalizeDouble(MathRound(BorderLevel / TickSize) * TickSize, _Digits); + + // Previous candle close significantly lower than the border line. + if (BorderLevel - Close[1] >= SymbolInfoInteger(Symbol(), SYMBOL_SPREAD) * _Point * ThresholdSpreads) + { + RefreshRates(); + NewVolume = GetPositionSize(Bid, LowerSL); + LowerTicket = ExecuteMarketOrder(OP_SELL, NewVolume, Bid, LowerSL, LowerTP); + } + } + else if ((UpperTP > 0) && (!HaveBuy) && (UseUpper)) // BUY. + { + if (ObjectFind(0, UpperBorderLine) >= 0) // Line. + { + if (ObjectGetInteger(ChartID(), UpperBorderLine, OBJPROP_TYPE) == OBJ_HLINE) BorderLevel = NormalizeDouble(ObjectGetDouble(0, UpperBorderLine, OBJPROP_PRICE1), _Digits); + else BorderLevel = NormalizeDouble(ObjectGetValueByShift(UpperBorderLine, 1), _Digits); + } + else // Channel + { + BorderLevel = MathMax(ObjectGetValueByTime(0, BorderChannel, Time[1], 0), ObjectGetValueByTime(0, BorderChannel, Time[1], 1)); + } + BorderLevel = NormalizeDouble(MathRound(BorderLevel / TickSize) * TickSize, _Digits); + + // Previous candle close significantly higher than the border line. + if (Close[1] - BorderLevel >= SymbolInfoInteger(Symbol(), SYMBOL_SPREAD) * _Point * ThresholdSpreads) + { + RefreshRates(); + NewVolume = GetPositionSize(Ask, UpperSL); + UpperTicket = ExecuteMarketOrder(OP_BUY, NewVolume, Ask, UpperSL, UpperTP); + } + } + return; + } + + int OT = OrdersTotal(); + for (int i = OT - 1; i >= 0; i--) + { + double prevOrderOpenPrice, prevOrderStopLoss, prevOrderTakeProfit; + double SL; + if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; + RefreshRates(); + // BUY + if (((OrderType() == OP_BUYSTOP) || (OrderType() == OP_BUYLIMIT)) && (OrderSymbol() == Symbol()) && (OrderMagicNumber() == Magic) && (!DisableBuyOrders)) + { + // Current price is below Sell entry - pending Sell Limit will be used instead of two stop orders. + if ((LowerEntry - Bid > StopLevel) && (UseLower)) continue; + + NewVolume = GetPositionSize(UpperEntry, UpperSL); + // Delete existing pending order + if ((HaveBuy) || ((HaveSell) && (OneCancelsOther)) || (!UseUpper)) + { + if (!OrderDelete(OrderTicket())) + { + last_error = GetLastError(); + Output("OrderDelete() error. Order ticket = " + IntegerToString(OrderTicket()) + ". Error = " + IntegerToString(last_error)); + } + } + // If volume needs to be updated - delete and recreate order with new volume. + // Also check if EA will be able to create new pending order at current price. + else if ((UpdatePendingVolume) && (MathAbs(OrderLots() - NewVolume) > LotStep / 2)) + { + if ((UpperEntry - Ask > StopLevel) || (Ask - UpperEntry > StopLevel)) // Order can be re-created. + { + if (!OrderDelete(OrderTicket())) + { + last_error = GetLastError(); + Output("OrderDelete() error. Order ticket = " + IntegerToString(OrderTicket()) + ". Error = " + IntegerToString(last_error)); + } + Sleep(5000); // Wait 5 seconds before opening a new order. + } + else continue; + // Ask could change after deletion, check if there is still no error 130 present. + RefreshRates(); + if (UpperEntry - Ask > StopLevel) // Current price below entry. + { + order_type = OP_BUYSTOP; + order_type_string = "Stop"; + } + else if (Ask - UpperEntry > StopLevel) // Current price above entry. + { + order_type = OP_BUYLIMIT; + order_type_string = "Limit"; + } + else continue; + if (UseExpiration) + { + // Set expiration to the end of the current bar. + expiration = Time[0] + Period() * 60; + // If expiration is less than 11 minutes extra seconds from now, set it to at least 11 minutes from now. + // (Brokers have such limit.) + if (expiration - TimeCurrent() < 660) expiration = TimeCurrent() + 661; + } + else expiration = 0; + UpperTicket = OrderSend(Symbol(), order_type, NewVolume, UpperEntry, Slippage, UpperSL, UpperTP, "ChartPatternHelper", Magic, expiration); + last_error = GetLastError(); + if ((UpperTicket == -1) && (last_error != 128)) // Ignore time-out errors. + { + Output("StopLevel = " + DoubleToStr(StopLevel, 8)); + Output("FreezeLevel = " + DoubleToStr(FreezeLevel, 8)); + Output("Error Recreating Buy " + order_type_string + ": " + IntegerToString(last_error) + " (" + ErrorDescription(last_error) + ")"); + Output("Volume = " + DoubleToStr(NewVolume, LotStep_digits) + " Entry = " + DoubleToStr(UpperEntry, Digits) + " SL = " + DoubleToStr(UpperSL, Digits) + " TP = " + DoubleToStr(UpperTP, Digits) + " Bid/Ask = " + DoubleToStr(Bid, Digits) + "/" + DoubleToStr(Ask, Digits) + " Exp: " + TimeToStr(expiration, TIME_DATE | TIME_SECONDS)); + } + continue; + } + // Otherwise, update entry/SL/TP if at least one of them has changed. + else if ((MathAbs(OrderOpenPrice() - UpperEntry) > _Point / 2) || (MathAbs(OrderStopLoss() - UpperSL) > _Point / 2) || (MathAbs(OrderTakeProfit() - UpperTP) > _Point / 2)) + { + // Avoid error 130 based on entry. + if (UpperEntry - Ask > StopLevel) // Current price below entry. + { + order_type_string = "Stop"; + } + else if (Ask - UpperEntry > StopLevel) // Current price above entry. + { + order_type_string = "Limit"; + } + else if (MathAbs(OrderOpenPrice() - UpperEntry) > _Point / 2) continue; + // Avoid error 130 based on stop-loss. + if (UpperEntry - UpperSL <= StopLevel) + { + Output("Skipping Modify Buy " + order_type_string + " because stop-loss is too close to entry. StopLevel = " + DoubleToStr(StopLevel, Digits) + " Entry = " + DoubleToStr(UpperEntry, Digits) + " SL = " + DoubleToStr(UpperSL, Digits)); + continue; + } + // Avoid frozen context. In all modification cases. + if ((FreezeLevel != 0) && (MathAbs(OrderOpenPrice() - Ask) <= FreezeLevel)) + { + Output("Skipping Modify Buy " + order_type_string + " because open price is too close to Ask. FreezeLevel = " + DoubleToStr(FreezeLevel, Digits) + " OpenPrice = " + DoubleToStr(OrderOpenPrice(), Digits) + " Ask = " + DoubleToStr(Ask, Digits)); + continue; + } + if (UseExpiration) + { + expiration = OrderExpiration(); + if (expiration - TimeCurrent() < 660) expiration = TimeCurrent() + 660; + } + else expiration = 0; + prevOrderOpenPrice = OrderOpenPrice(); + prevOrderStopLoss = OrderStopLoss(); + prevOrderTakeProfit = OrderTakeProfit(); + if (!OrderModify(OrderTicket(), UpperEntry, UpperSL, UpperTP, expiration)) + { + last_error = GetLastError(); + if (last_error != 128) // Ignore time out errors. + { + if (last_error == 1) + { + Output("PREV: Entry = " + DoubleToStr(OrderOpenPrice(), Digits) + " SL = " + DoubleToStr(OrderStopLoss(), Digits) + " TP = " + DoubleToStr(OrderTakeProfit(), Digits)); + } + Output("StopLevel = " + DoubleToStr(StopLevel, 8)); + Output("FreezeLevel = " + DoubleToStr(FreezeLevel, 8)); + Output("Error Modifying Buy " + order_type_string + ": " + IntegerToString(last_error) + " (" + ErrorDescription(last_error) + ")"); + Output("FROM: Entry = " + DoubleToStr(OrderOpenPrice(), Digits) + " SL = " + DoubleToStr(OrderStopLoss(), Digits) + " TP = " + DoubleToStr(OrderTakeProfit(), Digits) + " -> TO: Entry = " + DoubleToStr(UpperEntry, 8) + " SL = " + DoubleToStr(UpperSL, 8) + " TP = " + DoubleToStr(UpperTP, 8) + " Bid/Ask = " + DoubleToStr(Bid, Digits) + "/" + DoubleToStr(Ask, Digits) + " OrderTicket = " + IntegerToString(OrderTicket()) + " OrderExpiration = " + TimeToStr(OrderExpiration(), TIME_DATE | TIME_SECONDS) + " -> " + TimeToStr(expiration, TIME_DATE | TIME_SECONDS)); + } + } + } + } + else if ((OrderType() == OP_BUY) && (OrderSymbol() == Symbol()) && (OrderMagicNumber() == Magic) && (!DisableBuyOrders)) + { + // PostEntrySLAdjustment - a procedure to correct SL if breakout candle become too long and no longer qualifies for SL rule. + if ((OrderOpenTime() > Time[1]) && (OrderOpenTime() < Time[0]) && (PostEntrySLAdjustment) && (PostBuySLAdjustmentDone == false)) + { + SL = AdjustPostBuySL(); + if (SL != -1) + { + // Avoid frozen context. In all modification cases. + if ((FreezeLevel != 0) && (MathAbs(OrderOpenPrice() - Ask) <= FreezeLevel)) + { + Output("Skipping Modify Buy Stop SL because open price is too close to Ask. FreezeLevel = " + DoubleToStr(FreezeLevel, Digits) + " OpenPrice = " + DoubleToStr(OrderOpenPrice(), 8) + " Ask = " + DoubleToStr(Ask, Digits)); + continue; + } + if (NormalizeDouble(SL, Digits) == NormalizeDouble(OrderStopLoss(), Digits)) PostBuySLAdjustmentDone = true; + else + { + if (!OrderModify(OrderTicket(), OrderOpenPrice(), SL, OrderTakeProfit(), OrderExpiration())) + { + last_error = GetLastError(); + if (last_error != 128) // Ignore time out errors. + { + Output("Error Modifying Buy SL: " + IntegerToString(last_error) + " (" + ErrorDescription(last_error) + ")"); + Output("FROM: Entry = " + DoubleToStr(OrderOpenPrice(), Digits) + " SL = " + DoubleToStr(OrderStopLoss(), Digits) + " -> TO: Entry = " + DoubleToStr(OrderOpenPrice(), 8) + " SL = " + DoubleToStr(SL, 8) + " Ask = " + DoubleToStr(Ask, Digits)); + } + } + else PostBuySLAdjustmentDone = true; + } + } + } + // Adjust TP only. + if (MathAbs(OrderTakeProfit() - UpperTP) > _Point / 2) + { + // Avoid frozen context. In all modification cases. + if ((FreezeLevel != 0) && (MathAbs(OrderOpenPrice() - Ask) <= FreezeLevel)) + { + Output("Skipping Modify Buy Stop TP because open price is too close to Ask. FreezeLevel = " + DoubleToStr(FreezeLevel, Digits) + " OpenPrice = " + DoubleToStr(OrderOpenPrice(), 8) + " Ask = " + DoubleToStr(Ask, Digits)); + continue; + } + if (!OrderModify(OrderTicket(), OrderOpenPrice(), OrderStopLoss(), UpperTP, OrderExpiration())) + { + last_error = GetLastError(); + if (last_error != 128) // Ignore time out errors. + { + Output("Error Modifying Buy TP: " + IntegerToString(last_error) + " (" + ErrorDescription(last_error) + ")"); + Output("FROM: Entry = " + DoubleToStr(OrderOpenPrice(), Digits) + " TP = " + DoubleToStr(OrderTakeProfit(), Digits) + " -> TO: Entry = " + DoubleToStr(OrderOpenPrice(), 8) + " TP = " + DoubleToStr(UpperTP, 8) + " Ask = " + DoubleToStr(Ask, Digits)); + } + } + } + } + // SELL + else if (((OrderType() == OP_SELLSTOP) || (OrderType() == OP_SELLLIMIT)) && (OrderSymbol() == Symbol()) && (OrderMagicNumber() == Magic) && (!DisableSellOrders)) + { + // Current price is above Buy entry - pending Buy Limit will be used instead of two stop orders. + if ((Ask - UpperEntry > StopLevel) && (UseUpper)) continue; + + NewVolume = GetPositionSize(LowerEntry, LowerSL); + // Delete existing pending order. + if (((HaveBuy) && (OneCancelsOther)) || (HaveSell) || (!UseLower)) + { + if (!OrderDelete(OrderTicket())) + { + last_error = GetLastError(); + Output("OrderDelete() error. Order ticket = " + IntegerToString(OrderTicket()) + ". Error = " + IntegerToString(last_error)); + } + } + // If volume needs to be updated - delete and recreate order with new volume. Also check if EA will be able to create new pending order at current price. + else if ((UpdatePendingVolume) && (MathAbs(OrderLots() - NewVolume) > LotStep / 2)) + { + if ((Bid - LowerEntry > StopLevel) || (LowerEntry - Bid > StopLevel)) // Order can be re-created + { + if (!OrderDelete(OrderTicket())) + { + last_error = GetLastError(); + Output("OrderDelete() error. Order ticket = " + IntegerToString(OrderTicket()) + ". Error = " + IntegerToString(last_error)); + } + } + else continue; + + // Bid could change after deletion, check if there is still no error 130 present. + RefreshRates(); + if (Bid - LowerEntry > StopLevel) // Current price above entry. + { + order_type = OP_BUYSTOP; + order_type_string = "Stop"; + } + else if (LowerEntry - Bid > StopLevel) // Current price below entry. + { + order_type = OP_BUYLIMIT; + order_type_string = "Limit"; + } + else continue; + if (UseExpiration) + { + // Set expiration to the end of the current bar. + expiration = Time[0] + Period() * 60; + // If expiration is less than 11 minutes extra seconds from now, set it to at least 11 minutes from now. + // (Brokers have such limit.) + if (expiration - TimeCurrent() < 660) expiration = TimeCurrent() + 661; + } + else expiration = 0; + LowerTicket = OrderSend(Symbol(), order_type, NewVolume, LowerEntry, Slippage, LowerSL, LowerTP, "ChartPatternHelper", Magic, expiration); + last_error = GetLastError(); + if ((LowerTicket == -1) && (last_error != 128)) // Ignore time-out errors. + { + Output("StopLevel = " + DoubleToStr(StopLevel, 8)); + Output("FreezeLevel = " + DoubleToStr(FreezeLevel, 8)); + Output("Error Recreating Sell " + order_type_string + ": " + IntegerToString(last_error) + " (" + ErrorDescription(last_error) + ")"); + Output("Volume = " + DoubleToStr(NewVolume, LotStep_digits) + " Entry = " + DoubleToStr(LowerEntry, Digits) + " SL = " + DoubleToStr(LowerSL, Digits) + " TP = " + DoubleToStr(LowerTP, Digits) + " Bid/Ask = " + DoubleToStr(Bid, Digits) + "/" + DoubleToStr(Ask, Digits) + " Exp: " + TimeToStr(expiration, TIME_DATE | TIME_SECONDS)); + } + continue; + } + // Otherwise, just update what needs to be updated. + else if ((MathAbs(OrderOpenPrice() - LowerEntry) > _Point / 2) || (MathAbs(OrderStopLoss() - LowerSL) > _Point / 2) || (MathAbs(OrderTakeProfit() - LowerTP) > _Point / 2)) + { + // Avoid error 130 based on entry. + if (Bid - LowerEntry > StopLevel) // Current price above entry. + { + order_type_string = "Stop"; + } + else if (LowerEntry - Bid > StopLevel) // Current price below entry. + { + order_type_string = "Limit"; + } + else if (MathAbs(OrderOpenPrice() - LowerEntry) > _Point / 2) continue; + // Avoid error 130 based on stop-loss. + if (LowerSL - LowerEntry <= StopLevel) + { + Output("Skipping Modify Sell " + order_type_string + " because stop-loss is too close to entry. StopLevel = " + DoubleToStr(StopLevel, Digits) + " Entry = " + DoubleToStr(LowerEntry, Digits) + " SL = " + DoubleToStr(LowerSL, Digits)); + continue; + } + // Avoid frozen context. In all modification cases. + if ((FreezeLevel != 0) && (MathAbs(Bid - OrderOpenPrice()) <= FreezeLevel)) + { + Output("Skipping Modify Sell " + order_type_string + " because open price is too close to Bid. FreezeLevel = " + DoubleToStr(FreezeLevel, Digits) + " OpenPrice = " + DoubleToStr(OrderOpenPrice(), Digits) + " Bid = " + DoubleToStr(Bid, Digits)); + continue; + } + if (UseExpiration) + { + expiration = OrderExpiration(); + if (expiration - TimeCurrent() < 660) expiration = TimeCurrent() + 660; + } + else expiration = 0; + prevOrderOpenPrice = OrderOpenPrice(); + prevOrderStopLoss = OrderStopLoss(); + prevOrderTakeProfit = OrderTakeProfit(); + if (!OrderModify(OrderTicket(), LowerEntry, LowerSL, LowerTP, expiration)) + { + last_error = GetLastError(); + if (last_error != 128) // Ignore time out errors. + { + if (last_error == 1) + { + Output("PREV: Entry = " + DoubleToStr(OrderOpenPrice(), Digits) + " SL = " + DoubleToStr(OrderStopLoss(), Digits) + " TP = " + DoubleToStr(OrderTakeProfit(), Digits)); + } + Output("StopLevel = " + DoubleToStr(StopLevel, 8)); + Output("FreezeLevel = " + DoubleToStr(FreezeLevel, 8)); + Output("Error Modifying Sell " + order_type_string + ": " + IntegerToString(last_error) + " (" + ErrorDescription(last_error) + ")"); + Output("FROM: Entry = " + DoubleToStr(OrderOpenPrice(), Digits) + " SL = " + DoubleToStr(OrderStopLoss(), Digits) + " TP = " + DoubleToStr(OrderTakeProfit(), Digits) + " -> TO: Entry = " + DoubleToStr(LowerEntry, 8) + " SL = " + DoubleToStr(LowerSL, 8) + " TP = " + DoubleToStr(LowerTP, 8) + " Bid/Ask = " + DoubleToStr(Bid, Digits) + "/" + DoubleToStr(Ask, Digits) + " OrderTicket = " + IntegerToString(OrderTicket()) + " OrderExpiration = " + TimeToStr(OrderExpiration(), TIME_DATE | TIME_SECONDS) + " -> " + TimeToStr(expiration, TIME_DATE | TIME_SECONDS)); + } + } + } + } + else if ((OrderType() == OP_SELL) && (OrderSymbol() == Symbol()) && (OrderMagicNumber() == Magic) && (!DisableSellOrders)) + { + // PostEntrySLAdjustment - a procedure to correct SL if breakout candle become too long and no longer qualifies for SL rule. + if ((OrderOpenTime() > Time[1]) && (OrderOpenTime() < Time[0]) && (PostEntrySLAdjustment) && (PostSellSLAdjustmentDone == false)) + { + SL = AdjustPostSellSL(); + if (SL != -1) + { + // Avoid frozen context. In all modification cases. + if ((FreezeLevel != 0) && (MathAbs(Bid - OrderOpenPrice()) <= FreezeLevel)) + { + Output("Skipping Modify Sell Stop SL because open price is too close to Bid. FreezeLevel = " + DoubleToStr(FreezeLevel, Digits) + " OpenPrice = " + DoubleToStr(OrderOpenPrice(), 8) + " Bid = " + DoubleToStr(Bid, Digits)); + continue; + } + if (NormalizeDouble(SL, Digits) == NormalizeDouble(OrderStopLoss(), Digits)) PostSellSLAdjustmentDone = true; + else + { + if (!OrderModify(OrderTicket(), OrderOpenPrice(), SL, OrderTakeProfit(), OrderExpiration())) + { + last_error = GetLastError(); + if (last_error != 128) // Ignore time out errors. + { + Output("Error Modifying Sell SL: " + IntegerToString(last_error) + " (" + ErrorDescription(last_error) + ")"); + Output("FROM: Entry = " + DoubleToStr(OrderOpenPrice(), Digits) + " SL = " + DoubleToStr(OrderStopLoss(), Digits) + " -> TO: Entry = " + DoubleToStr(OrderOpenPrice(), 8) + " SL = " + DoubleToStr(SL, 8) + " Bid = " + DoubleToStr(Bid, Digits)); + } + } + else PostSellSLAdjustmentDone = true; + } + } + } + // Adjust TP only. + if (MathAbs(OrderTakeProfit() - LowerTP) > _Point / 2) + { + // Avoid frozen context. In all modification cases. + if ((FreezeLevel != 0) && (MathAbs(Bid - OrderOpenPrice()) <= FreezeLevel)) + { + Output("Skipping Modify Sell Stop TP because open price is too close to Bid. FreezeLevel = " + DoubleToStr(FreezeLevel, Digits) + " OpenPrice = " + DoubleToStr(OrderOpenPrice(), 8) + " Bid = " + DoubleToStr(Bid, Digits)); + continue; + } + if (!OrderModify(OrderTicket(), OrderOpenPrice(), OrderStopLoss(), LowerTP, OrderExpiration())) + { + last_error = GetLastError(); + if (last_error != 128) // Ignore time out errors. + { + Output("Error Modifying Sell Stop TP: " + IntegerToString(last_error) + " (" + ErrorDescription(last_error) + ")"); + Output("FROM: Entry = " + DoubleToStr(OrderOpenPrice(), Digits) + " TP = " + DoubleToStr(OrderTakeProfit(), Digits) + " -> TO: Entry = " + DoubleToStr(OrderOpenPrice(), 8) + " TP = " + DoubleToStr(LowerTP, 8) + " Bid = " + DoubleToStr(Bid, Digits)); + } + } + } + } + } + + // BUY + // If we do not already have Long position or Long pending order and if we can enter Long + // and the current price is not below the Sell entry (in that case, only pending Sell Limit order will be used). + if ((!HaveBuy) && (!HaveBuyPending) && (UseUpper) && ((LowerEntry - Bid <= StopLevel) || (!UseLower))) + { + // Avoid error 130 based on stop-loss. + if (UpperEntry - UpperSL <= StopLevel) + { + Output("Skipping Send Pending Buy because stop-loss is too close to entry. StopLevel = " + DoubleToStr(StopLevel, Digits) + " Entry = " + DoubleToStr(UpperEntry, Digits) + " SL = " + DoubleToStr(UpperSL, Digits)); + } + else + { + if (UpperEntry - Ask > StopLevel) // Current price below entry. + { + order_type = OP_BUYSTOP; + order_type_string = "Stop"; + } + else if (Ask - UpperEntry > StopLevel) // Current price above entry. + { + order_type = OP_BUYLIMIT; + order_type_string = "Limit"; + } + else + { + order_type = -1; + Output("Skipping Send Pending Buy because entry is too close to Ask. StopLevel = " + DoubleToStr(StopLevel, Digits) + " Entry = " + DoubleToStr(UpperEntry, Digits) + " Ask = " + DoubleToStr(Ask, Digits)); + } + if (order_type > -1) + { + NewVolume = GetPositionSize(UpperEntry, UpperSL); + UpperTicket = OrderSend(Symbol(), order_type, NewVolume, UpperEntry, Slippage, UpperSL, UpperTP, "ChartPatternHelper", Magic, expiration); + last_error = GetLastError(); + if ((UpperTicket == -1) && (last_error != 128)) // Ignore time-out errors + { + Output("Error Sending Buy " + order_type_string + ": " + IntegerToString(last_error) + " (" + ErrorDescription(last_error) + ")"); + Output("Volume = " + DoubleToStr(NewVolume, LotStep_digits) + " Entry = " + DoubleToStr(UpperEntry, Digits) + " SL = " + DoubleToStr(UpperSL, Digits) + " TP = " + DoubleToStr(UpperTP, Digits) + " Ask = " + DoubleToStr(Ask, Digits) + " Exp: " + TimeToStr(expiration, TIME_DATE | TIME_SECONDS)); + } + } + } + } + + // SELL + // If we do not already have Short position or Short pending order and if we can enter Short + // and the current price is not above the Buy entry (in that case, only pending Buy Limit order will be used). + if ((!HaveSell) && (!HaveSellPending) && (UseLower) && ((Ask - UpperEntry <= StopLevel) || (!UseUpper))) + { + // Avoid error 130 based on stop-loss. + if (LowerSL - LowerEntry <= StopLevel) + { + Output("Skipping Send Pending Sell because stop-loss is too close to entry. StopLevel = " + DoubleToStr(StopLevel, Digits) + " Entry = " + DoubleToStr(LowerEntry, Digits) + " SL = " + DoubleToStr(LowerSL, Digits)); + } + else + { + if (Bid - LowerEntry > StopLevel) // Current price above entry. + { + order_type = OP_SELLSTOP; + order_type_string = "Stop"; + } + else if (LowerEntry - Bid > StopLevel) // Current price below entry. + { + order_type = OP_SELLLIMIT; + order_type_string = "Limit"; + } + else + { + order_type = -1; + Output("Skipping Send Pending Sell because entry is too close to Bid. StopLevel = " + DoubleToStr(StopLevel, Digits) + " Entry = " + DoubleToStr(LowerEntry, Digits) + " Bid = " + DoubleToStr(Bid, Digits)); + } + if (order_type > -1) + { + NewVolume = GetPositionSize(LowerEntry, LowerSL); + LowerTicket = OrderSend(Symbol(), order_type, NewVolume, LowerEntry, Slippage, LowerSL, LowerTP, "ChartPatternHelper", Magic, expiration); + last_error = GetLastError(); + if ((LowerTicket == -1) && (last_error != 128)) // Ignore time-out errors + { + Output("Error Sending Sell " + order_type_string + ": " + IntegerToString(last_error) + " (" + ErrorDescription(last_error) + ")"); + Output("Volume = " + DoubleToStr(NewVolume, LotStep_digits) + " Entry = " + DoubleToStr(LowerEntry, Digits) + " SL = " + DoubleToStr(LowerSL, Digits) + " TP = " + DoubleToStr(LowerTP, Digits) + " Bid = " + DoubleToStr(Bid, Digits) + " Exp: " + TimeToStr(expiration, TIME_DATE | TIME_SECONDS)); + } + } + } + } +} + +void SetComment(string c) +{ + if (!Silent) Comment(c); +} + +//+-----------------------------------------------------------------------------------+ +//| Calculates necessary adjustments for cases when ProfitCurrency != AccountCurrency.| +//+-----------------------------------------------------------------------------------+ +#define FOREX_SYMBOLS_ONLY 0 +#define NONFOREX_SYMBOLS_ONLY 1 +double CalculateAdjustment() +{ + double add_coefficient = 1; // Might be necessary for correction coefficient calculation if two pairs are used for profit currency to account currency conversion. This is handled differently in MT5 version. + if (ReferenceSymbol == NULL) + { + ReferenceSymbol = GetSymbolByCurrencies(ProfitCurrency, account_currency, FOREX_SYMBOLS_ONLY); + if (ReferenceSymbol == NULL) ReferenceSymbol = GetSymbolByCurrencies(ProfitCurrency, account_currency, NONFOREX_SYMBOLS_ONLY); + ReferenceSymbolMode = true; + // Failed. + if (ReferenceSymbol == NULL) + { + // Reversing currencies. + ReferenceSymbol = GetSymbolByCurrencies(account_currency, ProfitCurrency, FOREX_SYMBOLS_ONLY); + if (ReferenceSymbol == NULL) ReferenceSymbol = GetSymbolByCurrencies(account_currency, ProfitCurrency, NONFOREX_SYMBOLS_ONLY); + ReferenceSymbolMode = false; + } + if (ReferenceSymbol == NULL) + { + // The condition checks whether we are caclulating conversion coefficient for the chart's symbol or for some other. + // The error output is OK for the current symbol only because it won't be repeated ad infinitum. + // It should be avoided for non-chart symbols because it will just flood the log. + Print("Couldn't detect proper currency pair for adjustment calculation. Profit currency: ", ProfitCurrency, ". Account currency: ", account_currency, ". Trying to find a possible two-symbol combination."); + if ((FindDoubleReferenceSymbol("USD")) // USD should work in 99.9% of cases. + || (FindDoubleReferenceSymbol("EUR")) // For very rare cases. + || (FindDoubleReferenceSymbol("GBP")) // For extremely rare cases. + || (FindDoubleReferenceSymbol("JPY"))) // For extremely rare cases. + { + Print("Converting via ", ReferenceSymbol, " and ", AdditionalReferenceSymbol, "."); + } + else + { + Print("Adjustment calculation critical failure. Failed both simple and two-pair conversion methods."); + return 1; + } + } + } + if (AdditionalReferenceSymbol != NULL) // If two reference pairs are used. + { + // Calculate just the additional symbol's coefficient and then use it in final return's multiplication. + MqlTick tick; + SymbolInfoTick(AdditionalReferenceSymbol, tick); + add_coefficient = GetCurrencyCorrectionCoefficient(tick, AdditionalReferenceSymbolMode); + } + MqlTick tick; + SymbolInfoTick(ReferenceSymbol, tick); + return GetCurrencyCorrectionCoefficient(tick, ReferenceSymbolMode) * add_coefficient; +} + +//+---------------------------------------------------------------------------+ +//| Returns a currency pair with specified base currency and profit currency. | +//+---------------------------------------------------------------------------+ +string GetSymbolByCurrencies(const string base_currency, const string profit_currency, const uint symbol_type) +{ + // Cycle through all symbols. + for (int s = 0; s < SymbolsTotal(false); s++) + { + // Get symbol name by number. + string symbolname = SymbolName(s, false); + string b_cur; + + // Normal case - Forex pairs: + if (MarketInfo(symbolname, MODE_PROFITCALCMODE) == 0) + { + if (symbol_type == NONFOREX_SYMBOLS_ONLY) continue; // Avoid checking symbols of a wrong type. + // Get its base currency. + b_cur = SymbolInfoString(symbolname, SYMBOL_CURRENCY_BASE); + if (b_cur == "RUR") b_cur = "RUB"; + } + else // Weird case for brokers that set conversion pairs as CFDs. + { + if (symbol_type == FOREX_SYMBOLS_ONLY) continue; // Avoid checking symbols of a wrong type. + // Get its base currency as the initial three letters - prone to huge errors! + b_cur = StringSubstr(symbolname, 0, 3); + } + + // Get its profit currency. + string p_cur = SymbolInfoString(symbolname, SYMBOL_CURRENCY_PROFIT); + if (p_cur == "RUR") p_cur = "RUB"; + + // If the currency pair matches both currencies, select it in Market Watch and return its name. + if ((b_cur == base_currency) && (p_cur == profit_currency)) + { + // Select if necessary. + if (!(bool)SymbolInfoInteger(symbolname, SYMBOL_SELECT)) SymbolSelect(symbolname, true); + + return symbolname; + } + } + return NULL; +} + +//+----------------------------------------------------------------------------+ +//| Finds reference symbols using 2-pair method. | +//| Results are returned via reference parameters. | +//| Returns true if found the pairs, false otherwise. | +//+----------------------------------------------------------------------------+ +bool FindDoubleReferenceSymbol(const string cross_currency) +{ + // A hypothetical example for better understanding: + // The trader buys CAD/CHF. + // account_currency is known = SEK. + // cross_currency = USD. + // profit_currency = CHF. + // I.e., we have to buy dollars with francs (using the Ask price) and then sell those for SEKs (using the Bid price). + + ReferenceSymbol = GetSymbolByCurrencies(cross_currency, account_currency, FOREX_SYMBOLS_ONLY); + if (ReferenceSymbol == NULL) ReferenceSymbol = GetSymbolByCurrencies(cross_currency, account_currency, NONFOREX_SYMBOLS_ONLY); + ReferenceSymbolMode = true; // If found, we've got USD/SEK. + + // Failed. + if (ReferenceSymbol == NULL) + { + // Reversing currencies. + ReferenceSymbol = GetSymbolByCurrencies(account_currency, cross_currency, FOREX_SYMBOLS_ONLY); + if (ReferenceSymbol == NULL) ReferenceSymbol = GetSymbolByCurrencies(account_currency, cross_currency, NONFOREX_SYMBOLS_ONLY); + ReferenceSymbolMode = false; // If found, we've got SEK/USD. + } + if (ReferenceSymbol == NULL) + { + Print("Error. Couldn't detect proper currency pair for 2-pair adjustment calculation. Cross currency: ", cross_currency, ". Account currency: ", account_currency, "."); + return false; + } + + AdditionalReferenceSymbol = GetSymbolByCurrencies(cross_currency, ProfitCurrency, FOREX_SYMBOLS_ONLY); + if (AdditionalReferenceSymbol == NULL) AdditionalReferenceSymbol = GetSymbolByCurrencies(cross_currency, ProfitCurrency, NONFOREX_SYMBOLS_ONLY); + AdditionalReferenceSymbolMode = false; // If found, we've got USD/CHF. Notice that mode is swapped for cross/profit compared to cross/acc, because it is used in the opposite way. + + // Failed. + if (AdditionalReferenceSymbol == NULL) + { + // Reversing currencies. + AdditionalReferenceSymbol = GetSymbolByCurrencies(ProfitCurrency, cross_currency, FOREX_SYMBOLS_ONLY); + if (AdditionalReferenceSymbol == NULL) AdditionalReferenceSymbol = GetSymbolByCurrencies(ProfitCurrency, cross_currency, NONFOREX_SYMBOLS_ONLY); + AdditionalReferenceSymbolMode = true; // If found, we've got CHF/USD. Notice that mode is swapped for profit/cross compared to acc/cross, because it is used in the opposite way. + } + if (AdditionalReferenceSymbol == NULL) + { + Print("Error. Couldn't detect proper currency pair for 2-pair adjustment calculation. Cross currency: ", cross_currency, ". Chart's pair currency: ", ProfitCurrency, "."); + return false; + } + + return true; +} + +//+------------------------------------------------------------------+ +//| Get profit correction coefficient based on current prices. | +//| Valid for loss calculation only. | +//+------------------------------------------------------------------+ +double GetCurrencyCorrectionCoefficient(MqlTick &tick, const bool ref_symbol_mode) +{ + if ((tick.ask == 0) || (tick.bid == 0)) return -1; // Data is not yet ready. + // Reverse quote. + if (ref_symbol_mode) + { + // Using Buy price for reverse quote. + return tick.ask; + } + // Direct quote. + else + { + // Using Sell price for direct quote. + return (1 / tick.bid); + } +} + +// Taken from PositionSizeCalculator indicator. +double GetPositionSize(double Entry, double StopLoss) +{ + double Size, RiskMoney, UnitCost, PositionSize = 0; + ProfitCurrency = SymbolInfoString(Symbol(), SYMBOL_CURRENCY_PROFIT); + BaseCurrency = SymbolInfoString(Symbol(), SYMBOL_CURRENCY_BASE); + ProfitCalcMode = (int)MarketInfo(Symbol(), MODE_PROFITCALCMODE); + account_currency = AccountCurrency(); + + // A rough patch for cases when account currency is set as RUR instead of RUB. + if (account_currency == "RUR") account_currency = "RUB"; + if (ProfitCurrency == "RUR") ProfitCurrency = "RUB"; + if (BaseCurrency == "RUR") BaseCurrency = "RUB"; + + double LotStep = MarketInfo(Symbol(), MODE_LOTSTEP); + int LotStep_digits = CountDecimalPlaces(LotStep); + + double SL = MathAbs(Entry - StopLoss); + + if (!CalculatePositionSize) return FixedPositionSize; + + if (AccountCurrency() == "") return 0; + + if (FixedBalance > 0) + { + Size = FixedBalance; + } + else if (UseEquityInsteadOfBalance) + { + Size = AccountEquity(); + } + else + { + Size = AccountBalance(); + } + + if (!UseMoneyInsteadOfPercentage) RiskMoney = Size * Risk / 100; + else RiskMoney = MoneyRisk; + + // If Symbol is CFD. + if (ProfitCalcMode == 1) + UnitCost = SymbolInfoDouble(Symbol(), SYMBOL_TRADE_TICK_SIZE) * SymbolInfoDouble(Symbol(), SYMBOL_TRADE_CONTRACT_SIZE); // Apparently, it is more accurate than taking TICKVALUE directly in some cases. + else UnitCost = MarketInfo(Symbol(), MODE_TICKVALUE); // Futures or Forex. + + if (ProfitCalcMode != 0) // Non-Forex might need to be adjusted. + { + // If profit currency is different from account currency. + if (ProfitCurrency != account_currency) + { + double CCC = CalculateAdjustment(); // Valid only for loss calculation. + // Adjust the unit cost. + UnitCost *= CCC; + } + } + + // If account currency == pair's base currency, adjust UnitCost to future rate (SL). Works only for Forex pairs. + if ((account_currency == BaseCurrency) && (ProfitCalcMode == 0)) + { + double current_rate = 1, future_rate = StopLoss; + RefreshRates(); + if (StopLoss < Entry) + { + current_rate = Ask; + } + else if (StopLoss > Entry) + { + current_rate = Bid; + } + UnitCost *= (current_rate / future_rate); + } + + if ((SL != 0) && (UnitCost != 0) && (TickSize != 0)) PositionSize = NormalizeDouble(RiskMoney / (SL * UnitCost / TickSize), LotStep_digits); + + if (PositionSize < MarketInfo(Symbol(), MODE_MINLOT)) PositionSize = MarketInfo(Symbol(), MODE_MINLOT); + else if (PositionSize > MarketInfo(Symbol(), MODE_MAXLOT)) PositionSize = MarketInfo(Symbol(), MODE_MAXLOT); + double steps = PositionSize / SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); + if (MathFloor(steps) < steps) PositionSize = MathFloor(steps) * MarketInfo(Symbol(), MODE_LOTSTEP); + return PositionSize; +} + +// Prints and writes to file error info and context data. +void Output(string s) +{ + Print(s); + if (!ErrorLogging) return; + int file = FileOpen(filename, FILE_CSV | FILE_READ | FILE_WRITE); + if (file == -1) Print("Failed to create an error log file: ", GetLastError(), "."); + else + { + FileSeek(file, 0, SEEK_END); + s = TimeToStr(TimeCurrent(), TIME_DATE | TIME_SECONDS) + " - " + s; + FileWrite(file, s); + FileClose(file); + } +} + +// Runs only one time to adjust SL to appropriate bar's Low if breakout bar's part outside the pattern turned out to be longer than the one inside. +// Works only if PostEntrySLAdjustment = true. +double AdjustPostBuySL() +{ + double SL = -1; + string smagic = IntegerToString(Magic); + double Border; + + // Border. + if (ObjectFind(UpperBorderLine + smagic) > -1) + { + if ((ObjectType(UpperBorderLine + smagic) != OBJ_HLINE) && (ObjectType(UpperBorderLine + smagic) != OBJ_TREND)) return SL; + // Starting from 1 because it is new bar after breakout bar. + for (int i = 1; i < Bars; i++) + { + if (ObjectType(UpperBorderLine + smagic) != OBJ_HLINE) Border = ObjectGetValueByShift(UpperBorderLine + smagic, i); + else Border = ObjectGet(UpperBorderLine + smagic, OBJPROP_PRICE1); // Horizontal line value + // Major part inside pattern but and SL not closer than breakout bar's SL. + if ((Border - Low[i] > High[i] - Border) && (Low[i] <= Low[1])) return NormalizeDouble(Low[i], Digits); + } + } + else // Try to find a channel. + { + if (ObjectFind(BorderChannel + smagic) > -1) + { + if (ObjectType(BorderChannel + smagic) != OBJ_CHANNEL) return SL; + for (int i = 1; i < Bars; i++) + { + // Get the upper of main and auxiliary lines. + Border = MathMax(ObjectGetValueByTime(0, BorderChannel + smagic, Time[i], 0), ObjectGetValueByTime(0, BorderChannel + smagic, Time[i], 1)); + // Major part inside pattern but and SL not closer than breakout bar's SL. + if ((Border - Low[i] > High[i] - Border) && (Low[i] <= Low[0])) return NormalizeDouble(Low[i], _Digits); + } + } + } + return SL; +} + +// Runs only one time to adjust SL to appropriate bar's High if breakout bar's part outside the pattern turned out to be longer than the one inside. +// Works only if PostEntrySLAdjustment = true. +double AdjustPostSellSL() +{ + double SL = -1; + string smagic = IntegerToString(Magic); + double Border; + + // Border. + if (ObjectFind(LowerBorderLine + smagic) > -1) + { + if ((ObjectType(LowerBorderLine + smagic) != OBJ_HLINE) && (ObjectType(LowerBorderLine + smagic) != OBJ_TREND)) return SL; + // Starting from 1 because it is new bar after breakout bar. + for (int i = 1; i < Bars; i++) + { + if (ObjectType(LowerBorderLine + smagic) != OBJ_HLINE) Border = ObjectGetValueByShift(LowerBorderLine + smagic, i); + else Border = ObjectGet(LowerBorderLine + smagic, OBJPROP_PRICE1); // Horizontal line value + // Major part inside pattern but and SL not closer than breakout bar's SL. + if ((High[i] - Border > Border - Low[i]) && (High[i] >= High[0])) return NormalizeDouble(High[i], Digits); + } + } + else // Try to find a channel. + { + if (ObjectFind(BorderChannel + smagic) > -1) + { + if (ObjectType(BorderChannel + smagic) != OBJ_CHANNEL) return SL; + for (int i = 1; i < Bars; i++) + { + // Get the lower of main and auxiliary lines. + Border = MathMin(ObjectGetValueByTime(0, BorderChannel + smagic, Time[i], 0), ObjectGetValueByTime(0, BorderChannel + smagic, Time[i], 1)); + // Major part inside pattern but and SL not closer than breakout bar's SL. + if ((High[i] - Border > Border - Low[i]) && (High[i] >= High[0])) return NormalizeDouble(High[i], _Digits); + } + } + } + return SL; +} + +//+------------------------------------------------------------------+ +//| Counts decimal places. | +//+------------------------------------------------------------------+ +int CountDecimalPlaces(double number) +{ + // 100 as maximum length of number. + for (int i = 0; i < 100; i++) + { + double pwr = MathPow(10, i); + if (MathRound(number * pwr) / pwr == number) return i; + } + return -1; +} + +//+------------------------------------------------------------------+ +//| Execute a markte order (depends on symbol's trade execution mode.| +//+------------------------------------------------------------------+ +int ExecuteMarketOrder(const int order_type, const double volume, const double price, const double sl, const double tp) +{ + double order_sl = sl; + double order_tp = tp; + + double StopLevel = MarketInfo(Symbol(), MODE_STOPLEVEL) * Point; + double FreezeLevel = MarketInfo(Symbol(), MODE_FREEZELEVEL) * Point; + double LotStep = MarketInfo(Symbol(), MODE_LOTSTEP); + int LotStep_digits = CountDecimalPlaces(LotStep); + + ENUM_SYMBOL_TRADE_EXECUTION Execution_Mode = (ENUM_SYMBOL_TRADE_EXECUTION)SymbolInfoInteger(Symbol(), SYMBOL_TRADE_EXEMODE); + + // Market execution mode - preparation. + if (Execution_Mode == SYMBOL_TRADE_EXECUTION_MARKET) + { + // No SL/TP allowed on instant orders. + order_sl = 0; + order_tp = 0; + } + + int ticket = OrderSend(Symbol(), order_type, volume, price, Slippage, order_sl, order_tp, "Chart Pattern Helper", Magic); + if (ticket == -1) + { + int last_error = GetLastError(); + string order_string = ""; + if (order_type == OP_BUY) order_string = "Buy"; + else if (order_type == OP_SELL) order_string = "Sell"; + Output("Error Sending " + order_string + ": " + IntegerToString(last_error) + " (" + ErrorDescription(last_error) + ")"); + Output("Volume = " + DoubleToStr(volume, LotStep_digits) + " Entry = " + DoubleToStr(price, Digits) + " SL = " + DoubleToStr(order_sl, Digits) + " TP = " + DoubleToStr(order_tp, Digits)); + } + else + { + Output("Order executed. Ticket: " + IntegerToString(ticket) + "."); + } + + // Market execution mode - applying SL/TP. + if (Execution_Mode == SYMBOL_TRADE_EXECUTION_MARKET) + { + if (!OrderSelect(ticket, SELECT_BY_TICKET)) + { + Output("Failed to find the order to apply SL/TP."); + return 0; + } + for (int i = 0; i < 10; i++) + { + bool result = OrderModify(ticket, OrderOpenPrice(), sl, tp, OrderExpiration()); + if (result) + { + break; + } + else + { + Output("Error modifying the order: " + IntegerToString(GetLastError())); + } + } + } + + return ticket; +} +//+------------------------------------------------------------------+ \ No newline at end of file diff --git a/58-Chart-Pattern-Helper/Chart-Pattern-Helper.pdf b/58-Chart-Pattern-Helper/Chart-Pattern-Helper.pdf new file mode 100644 index 0000000..943fae4 Binary files /dev/null and b/58-Chart-Pattern-Helper/Chart-Pattern-Helper.pdf differ diff --git a/59-SetFixed-SL-TP/SetFixed-SL-TP.mq4 b/59-SetFixed-SL-TP/SetFixed-SL-TP.mq4 new file mode 100644 index 0000000..d1da897 --- /dev/null +++ b/59-SetFixed-SL-TP/SetFixed-SL-TP.mq4 @@ -0,0 +1,579 @@ +#property link "https://www.earnforex.com/metatrader-expert-advisors/SetFixedSLandTPEA/" +#property version "1.00" + +#property copyright "EarnForex.com - 2025" +#property description "This EA constantly monitors your positions and pending orders and sets a stop-loss and, if required a take-profit, to all trades based on the given filters." +#property description "" +#property description "DISCLAIMER: This EA comes with no guarantee. Use it at your own risk." +#property description "It is best to test it on a demo account first." +#property description "" +#property description "Find more on EarnForex.com" +#property icon "\\Files\\EF-Icon-64x64px.ico" + +#include +#include + +enum ENUM_PRICE_TYPE +{ + PRICE_TYPE_OPEN, // Trade's open price + PRICE_TYPE_CURRENT // Current price +}; + +enum ENUM_ORDER_TYPES +{ + ALL_ORDERS = 1, // ALL TRADES + ONLY_BUY = 2, // BUY ONLY + ONLY_SELL = 3 // SELL ONLY +}; + +enum ENUM_TP_TYPE +{ + TP_TYPE_POINTS, // Points + TP_TYPE_LEVEL, // Level + TP_TYPE_PERCENTAGE, // Percentage of SL + TP_TYPE_UNCHANGED // Keep TP unchanged +}; + +enum ENUM_SL_TYPE +{ + SL_TYPE_POINTS, // Points + SL_TYPE_LEVEL, // Level + SL_TYPE_UNCHANGED // Keep SL unchanged +}; + +// Input parameters. +input string Group_1 = "===================="; // SL & TP +input double StopLoss = 200; // Stop-loss +input ENUM_SL_TYPE StopLossType = SL_TYPE_POINTS; // Stop-loss type +input bool OverwriteExistingSL = false; // Overwrite existing SL? +input double TakeProfit = 400; // Take-profit +input ENUM_TP_TYPE TakeProfitType = TP_TYPE_POINTS; // Take-profit type +input bool OverwriteExistingTP = false; // Overwrite existing TP? + +input string Group_2 = "===================="; // Filters +input bool CurrentSymbolOnly = true; // Current symbol only? +input ENUM_ORDER_TYPES OrderTypeFilter = ALL_ORDERS; // Type of trades to apply to +input bool OnlyMagicNumber = false; // Modify only trades matching the magic number +input int MagicNumber = 0; // Matching magic number +input bool OnlyWithComment = false; // Modify only trades with the following comment +input string MatchingComment = ""; // Matching comment +input bool ApplyToPending = false; // Apply to pending orders too? + +input string Group_3 = "===================="; // Execution +input ENUM_PRICE_TYPE PriceType = PRICE_TYPE_OPEN; // Price to use for SL/TP setting +input bool ProcessOnceOnly = true; // Process each position/order only once? +input int CheckIntervalSeconds = 1; // Check interval in seconds +input bool InputEnableExpert = false; // Enable EA + +input string Group_4 = "===================="; // Control panel +input bool ShowPanel = true; // Show graphical panel +input string ExpertName = "SLTP"; // Expert name (to name the objects) +input int Xoff = 20; // Horizontal spacing for the control panel +input int Yoff = 20; // Vertical spacing for the control panel +input ENUM_BASE_CORNER ChartCorner = CORNER_LEFT_UPPER; // Chart corner +input int FontSize = 10; // Font size + +// Global variables. +bool EnableExpert; // Main enable/disable flag. +int ProcessedOrders[]; // Array to store processed order tickets. +datetime LastCheckTime; // For processed orders cleanup. + +// Panel variables. +double DPIScale; // Scaling parameter for the panel based on the screen DPI. +int PanelMovY, PanelLabX, PanelLabY, PanelRecX; +string PanelBase = ""; +string PanelLabel = ""; +string PanelEnableDisable = ""; + +int OnInit() +{ + EnableExpert = InputEnableExpert; + + // Initialize arrays. + ArrayResize(ProcessedOrders, 0); + LastCheckTime = 0; + + // Initialize panel variables. + PanelBase = ExpertName + "-P-BAS"; + PanelLabel = ExpertName + "-P-LAB"; + PanelEnableDisable = ExpertName + "-P-ENADIS"; + + CleanPanel(); + + DPIScale = (double)TerminalInfoInteger(TERMINAL_SCREEN_DPI) / 96.0; + PanelMovY = (int)MathRound(20 * DPIScale); + PanelLabX = (int)MathRound(150 * DPIScale); + PanelLabY = PanelMovY; + PanelRecX = PanelLabX + 4; + + if (ShowPanel) DrawPanel(); + + EventSetTimer(CheckIntervalSeconds); + + return INIT_SUCCEEDED; +} + +void OnDeinit(const int reason) +{ + // Clean up panel. + CleanPanel(); +} + +void OnChartEvent(const int id, + const long &lparam, + const double &dparam, + const string &sparam) +{ + if (id == CHARTEVENT_OBJECT_CLICK) + { + if (sparam == PanelEnableDisable) + { + ChangeTrailingEnabled(); + } + } + else if (id == CHARTEVENT_KEYDOWN) + { + if (lparam == 27) // ESC key. + { + if (MessageBox("Are you sure you want to close the EA?", "EXIT ?", MB_YESNO) == IDYES) + { + ExpertRemove(); + } + } + } +} + +void OnTimer() +{ + // Update panel if enabled. + if (ShowPanel) DrawPanel(); + + // Only process if enabled. + if (!EnableExpert) return; + + // Check connection and trading status. + if (!TerminalInfoInteger(TERMINAL_CONNECTED)) + { + return; + } + + if (!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) + { + return; + } + + if (!MQLInfoInteger(MQL_TRADE_ALLOWED)) + { + return; + } + + // Process all orders (both positions and pending). + ProcessOrders(); + + // Clean up closed orders from the array periodically. + if (ProcessOnceOnly && ArraySize(ProcessedOrders) > 0) CleanupProcessedOrders(); +} + +void ProcessOrders() +{ + // Scan the orders backwards. + for (int i = OrdersTotal() - 1; i >= 0; i--) + { + // Select the order. If not selected print the error and continue with the next index. + if (OrderSelect(i, SELECT_BY_POS, MODE_TRADES) == false) + { + Print("ERROR - Unable to select the order - ", GetLastError()); + continue; + } + + int ticket = OrderTicket(); + + // Check if already processed. + if (ProcessOnceOnly && IsOrderProcessed(ticket)) continue; + + // Check if the order matches the filters. + if (!PassesOrderFilters()) continue; + + // Process the order. + if (ModifyOrder()) + { + // Mark as processed if successful. + if (ProcessOnceOnly) + { + AddProcessedOrder(ticket); + } + } + } +} + +bool PassesOrderFilters() +{ + // Check if pending order and if we should process pending. + if (!ApplyToPending && (OrderType() != OP_BUY) && (OrderType() != OP_SELL)) return false; + + // Check symbol filter. + if (CurrentSymbolOnly && (OrderSymbol() != Symbol())) return false; + + // Check magic number filter. + if (OnlyMagicNumber && (OrderMagicNumber() != MagicNumber)) return false; + + // Check comment filter. + if (OnlyWithComment && (StringCompare(OrderComment(), MatchingComment) != 0)) return false; + + // Check order type filter. + if (OrderTypeFilter == ONLY_SELL) + { + if ((OrderType() == OP_BUY) || (OrderType() == OP_BUYLIMIT) || (OrderType() == OP_BUYSTOP)) return false; + } + if (OrderTypeFilter == ONLY_BUY) + { + if ((OrderType() == OP_SELL) || (OrderType() == OP_SELLLIMIT) || (OrderType() == OP_SELLSTOP)) return false; + } + + return true; +} + +bool ModifyOrder() +{ + string symbol = OrderSymbol(); + double TakeProfitPrice = 0; + double StopLossPrice = 0; + double Price; + double point = SymbolInfoDouble(symbol, SYMBOL_POINT); + int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS); + + // Check if trading is enabled for symbol. + if (SymbolInfoInteger(symbol, SYMBOL_TRADE_MODE) == SYMBOL_TRADE_MODE_DISABLED) + { + return false; + } + + double tick_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE); + if (tick_size == 0) + { + return false; + } + + // Calculate SL/TP based on order type. + if ((OrderType() == OP_BUY) || (OrderType() == OP_BUYLIMIT) || (OrderType() == OP_BUYSTOP)) + { + CalculateBuySLTP(symbol, Price, StopLossPrice, TakeProfitPrice, digits, tick_size, point); + } + else if ((OrderType() == OP_SELL) || (OrderType() == OP_SELLLIMIT) || (OrderType() == OP_SELLSTOP)) + { + CalculateSellSLTP(symbol, Price, StopLossPrice, TakeProfitPrice, digits, tick_size, point); + } + + // Avoid modifying existing SL/TP if overwriting isn't allowed. + if (!OverwriteExistingSL && OrderStopLoss() > 0) StopLossPrice = OrderStopLoss(); + if (!OverwriteExistingTP && OrderTakeProfit() > 0) TakeProfitPrice = OrderTakeProfit(); + + // Check if modification is needed. + if ((MathAbs(StopLossPrice - OrderStopLoss()) < point / 2) && + (MathAbs(TakeProfitPrice - OrderTakeProfit()) < point / 2)) + { + return false; // No modification needed. + } + + if (OrderModify(OrderTicket(), OrderOpenPrice(), StopLossPrice, TakeProfitPrice, OrderExpiration())) + { + Print("Order #", OrderTicket(), " on ", OrderSymbol(), " modified: SL=", StopLossPrice, " TP=", TakeProfitPrice); + return true; + } + else + { + Print("Order #", OrderTicket(), " on ", OrderSymbol(), " failed to update SL to ", StopLossPrice, " and TP to ", TakeProfitPrice, " with error - ", GetLastError()); + return false; + } +} + +void CalculateBuySLTP(string symbol, double &Price, double &StopLossPrice, double &TakeProfitPrice, + int digits, double tick_size, double point) +{ + if (PriceType == PRICE_TYPE_CURRENT) + { + RefreshRates(); + // Should be Bid for Buy orders. + Price = SymbolInfoDouble(symbol, SYMBOL_BID); + } + else + { + Price = OrderOpenPrice(); + } + + // Calculate Stop-loss. + if (StopLossType == SL_TYPE_UNCHANGED) + { + StopLossPrice = OrderStopLoss(); + } + else if (StopLossType == SL_TYPE_LEVEL) + { + StopLossPrice = StopLoss; + } + else if (StopLossType == SL_TYPE_POINTS) + { + if (StopLoss > 0) + { + StopLossPrice = NormalizeDouble(Price - StopLoss * point, digits); + StopLossPrice = NormalizeDouble(MathRound(StopLossPrice / tick_size) * tick_size, digits); // Adjusting for tick size granularity. + } + } + + // Calculate Take-profit. + if (TakeProfitType == TP_TYPE_UNCHANGED) + { + TakeProfitPrice = OrderTakeProfit(); + } + else if (TakeProfitType == TP_TYPE_LEVEL) + { + TakeProfitPrice = TakeProfit; + } + else if (TakeProfitType == TP_TYPE_POINTS) + { + if (TakeProfit > 0) + { + TakeProfitPrice = NormalizeDouble(Price + TakeProfit * point, digits); + TakeProfitPrice = NormalizeDouble(MathRound(TakeProfitPrice / tick_size) * tick_size, digits); // Adjusting for tick size granularity. + } + } + else if (TakeProfitType == TP_TYPE_PERCENTAGE) + { + double sl_distance = 0; + if (StopLossPrice > 0) + sl_distance = OrderOpenPrice() - StopLossPrice; + else if (OrderStopLoss() > 0) + sl_distance = OrderOpenPrice() - OrderStopLoss(); + + if (sl_distance > 0) + { + TakeProfitPrice = NormalizeDouble(Price + sl_distance * TakeProfit / 100, digits); + TakeProfitPrice = NormalizeDouble(MathRound(TakeProfitPrice / tick_size) * tick_size, digits); // Adjusting for tick size granularity. + } + } +} + +void CalculateSellSLTP(string symbol, double &Price, double &StopLossPrice, double &TakeProfitPrice, + int digits, double tick_size, double point) +{ + if (PriceType == PRICE_TYPE_CURRENT) + { + RefreshRates(); + // Should be Ask for Sell orders. + Price = SymbolInfoDouble(symbol, SYMBOL_ASK); + } + else + { + Price = OrderOpenPrice(); + } + + // Calculate Stop-loss. + if (StopLossType == SL_TYPE_UNCHANGED) + { + StopLossPrice = OrderStopLoss(); + } + else if (StopLossType == SL_TYPE_LEVEL) + { + StopLossPrice = StopLoss; + } + else if (StopLossType == SL_TYPE_POINTS) + { + if (StopLoss > 0) + { + StopLossPrice = NormalizeDouble(Price + StopLoss * point, digits); + StopLossPrice = NormalizeDouble(MathRound(StopLossPrice / tick_size) * tick_size, digits); // Adjusting for tick size granularity. + } + } + + // Calculate Take-profit. + if (TakeProfitType == TP_TYPE_UNCHANGED) + { + TakeProfitPrice = OrderTakeProfit(); + } + else if (TakeProfitType == TP_TYPE_LEVEL) + { + TakeProfitPrice = TakeProfit; + } + else if (TakeProfitType == TP_TYPE_POINTS) + { + if (TakeProfit > 0) + { + TakeProfitPrice = NormalizeDouble(Price - TakeProfit * point, digits); + TakeProfitPrice = NormalizeDouble(MathRound(TakeProfitPrice / tick_size) * tick_size, digits); // Adjusting for tick size granularity. + } + } + else if (TakeProfitType == TP_TYPE_PERCENTAGE) + { + double sl_distance = 0; + if (StopLossPrice > 0) + sl_distance = StopLossPrice - OrderOpenPrice(); + else if (OrderStopLoss() > 0) + sl_distance = OrderStopLoss() - OrderOpenPrice(); + + if (sl_distance > 0) + { + TakeProfitPrice = NormalizeDouble(Price - sl_distance * TakeProfit / 100, digits); + TakeProfitPrice = NormalizeDouble(MathRound(TakeProfitPrice / tick_size) * tick_size, digits); // Adjusting for tick size granularity. + } + } +} + +bool IsOrderProcessed(int ticket) +{ + int size = ArraySize(ProcessedOrders); + + for (int i = 0; i < size; i++) + { + if (ProcessedOrders[i] == ticket) return true; + } + + return false; +} + +void AddProcessedOrder(int ticket) +{ + int size = ArraySize(ProcessedOrders); + ArrayResize(ProcessedOrders, size + 1, 10); + ProcessedOrders[size] = ticket; +} + +void CleanupProcessedOrders() +{ + if (TimeCurrent() - LastCheckTime < CheckIntervalSeconds * 10) return; // Check only once every 10 x CheckIntervalSeconds. + int size = ArraySize(ProcessedOrders); + for (int i = 0; i < size; i++) + { + int ticket = ProcessedOrders[i]; + if (!OrderSelect(ticket, SELECT_BY_TICKET)) + { + size--; // One element less. + for (int j = i; j < size; j++) // Shift all array elements left to remove the current one (i). + ProcessedOrders[j] = ProcessedOrders[j + 1]; + ArrayResize(ProcessedOrders, size); // New size. + } + } + LastCheckTime = TimeCurrent(); +} + +void DrawPanel() +{ + int SignX = 1; + int YAdjustment = 0; + if ((ChartCorner == CORNER_RIGHT_UPPER) || (ChartCorner == CORNER_RIGHT_LOWER)) + { + SignX = -1; // Correction for right-side panel position. + } + if ((ChartCorner == CORNER_RIGHT_LOWER) || (ChartCorner == CORNER_LEFT_LOWER)) + { + YAdjustment = (PanelMovY + 2) * 2 + 1 - PanelLabY; // Correction for lower side panel position. + } + + string PanelText = "FIXED SL/TP"; + string PanelToolTip = "Set fixed stop-loss and take-profit"; + int Rows = 1; + + // Create base rectangle. + ObjectCreate(ChartID(), PanelBase, OBJ_RECTANGLE_LABEL, 0, 0, 0); + ObjectSetInteger(ChartID(), PanelBase, OBJPROP_CORNER, ChartCorner); + ObjectSetInteger(ChartID(), PanelBase, OBJPROP_XDISTANCE, Xoff); + ObjectSetInteger(ChartID(), PanelBase, OBJPROP_YDISTANCE, Yoff + YAdjustment); + ObjectSetInteger(ChartID(), PanelBase, OBJPROP_XSIZE, PanelRecX); + ObjectSetInteger(ChartID(), PanelBase, OBJPROP_YSIZE, (PanelMovY + 1) * (Rows + 1) + 3); + ObjectSetInteger(ChartID(), PanelBase, OBJPROP_BGCOLOR, clrWhite); + ObjectSetInteger(ChartID(), PanelBase, OBJPROP_BORDER_TYPE, BORDER_FLAT); + ObjectSetInteger(ChartID(), PanelBase, OBJPROP_HIDDEN, true); + ObjectSetInteger(ChartID(), PanelBase, OBJPROP_SELECTABLE, false); + ObjectSetInteger(ChartID(), PanelBase, OBJPROP_COLOR, clrBlack); + + // Create main label. + DrawEdit(PanelLabel, + Xoff + 2 * SignX, + Yoff + 2, + PanelLabX, + PanelLabY, + true, + FontSize, + PanelToolTip, + ALIGN_CENTER, + "Consolas", + PanelText, + false, + clrNavy, + clrKhaki, + clrBlack); + ObjectSetInteger(ChartID(), PanelLabel, OBJPROP_CORNER, ChartCorner); + + // Create enable/disable button. + string EnableDisabledText = ""; + color EnableDisabledColor = clrNavy; + color EnableDisabledBack = clrKhaki; + + if (EnableExpert) + { + EnableDisabledText = "EXPERT ENABLED"; + EnableDisabledColor = clrWhite; + EnableDisabledBack = clrDarkGreen; + } + else + { + EnableDisabledText = "EXPERT DISABLED"; + EnableDisabledColor = clrWhite; + EnableDisabledBack = clrDarkRed; + } + + if (ObjectFind(ChartID(), PanelEnableDisable) >= 0) + { + ObjectSetString(ChartID(), PanelEnableDisable, OBJPROP_TEXT, EnableDisabledText); + ObjectSetInteger(ChartID(), PanelEnableDisable, OBJPROP_COLOR, EnableDisabledColor); + ObjectSetInteger(ChartID(), PanelEnableDisable, OBJPROP_BGCOLOR, EnableDisabledBack); + } + else + { + DrawEdit(PanelEnableDisable, + Xoff + 2 * SignX, + Yoff + (PanelMovY + 1) * Rows + 2, + PanelLabX, + PanelLabY, + true, + FontSize, + "Click to enable or disable the SL/TP modification feature.", + ALIGN_CENTER, + "Consolas", + EnableDisabledText, + false, + EnableDisabledColor, + EnableDisabledBack, + clrBlack); + } + ObjectSetInteger(ChartID(), PanelEnableDisable, OBJPROP_CORNER, ChartCorner); +} + +void CleanPanel() +{ + ObjectsDeleteAll(ChartID(), ExpertName + "-P-"); +} + +void ChangeTrailingEnabled() +{ + if (EnableExpert == false) + { + if (!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) + { + MessageBox("Algorithmic trading is disabled in the platform's options! Please enable it via Tools->Options->Expert Advisors.", "WARNING", MB_OK); + return; + } + if (!MQLInfoInteger(MQL_TRADE_ALLOWED)) + { + MessageBox("Algo Trading is disabled in the EA's settings! Please tick the Allow Algo Trading checkbox on the Common tab.", "WARNING", MB_OK); + return; + } + EnableExpert = true; + Print("SetFixedSLTP EA enabled."); + } + else + { + EnableExpert = false; + Print("SetFixedSLTP EA disabled."); + } + DrawPanel(); +} +//+------------------------------------------------------------------+ \ No newline at end of file diff --git a/59-SetFixed-SL-TP/SetFixed-SL-TP.pdf b/59-SetFixed-SL-TP/SetFixed-SL-TP.pdf new file mode 100644 index 0000000..e76feab Binary files /dev/null and b/59-SetFixed-SL-TP/SetFixed-SL-TP.pdf differ diff --git a/60-Trailing-Stop/Trailing-Stop.mq4 b/60-Trailing-Stop/Trailing-Stop.mq4 new file mode 100644 index 0000000..45fa722 --- /dev/null +++ b/60-Trailing-Stop/Trailing-Stop.mq4 @@ -0,0 +1,663 @@ +#property link "https://www.earnforex.com/metatrader-expert-advisors/Trailing-Stop-on-Profit/" +#property version "1.04" +#property strict +#property copyright "EarnForex.com - 2023-2026" +#property description "This expert advisor will start trailing the stop-loss after a given profit is reached." +#property description " " +#property description "WARNING: No warranty. This EA is offered \"as is\". Use at your own risk.\r\n" +#property icon "\\Files\\EF-Icon-64x64px.ico" + +#include +#include + +enum ENUM_CONSIDER +{ + All = -1, // All orders + Buy = OP_BUY, // Buy only + Sell = OP_SELL, // Sell only +}; + +input string Comment_1 = "===================="; // Expert Advisor Settings +input int TrailingStop = 50; // Trailing Stop, points +input int Profit = 100; // Profit in points when TS should kick in +input bool DisableTPonTSL = false; // Disable take-profit when TS kicks in? +input string Comment_2 = "===================="; // Orders Filtering Options +input bool OnlyCurrentSymbol = true; // Apply to current symbol only +input ENUM_CONSIDER OnlyType = All; // Apply to +input bool UseMagic = false; // Filter by magic number +input int MagicNumber = 0; // Magic number (if above is true) +input bool UseComment = false; // Filter by comment +input string CommentFilter = ""; // Comment (if above is true) +input bool EnableTrailingParam = false; // Enable trailing stop +input string Comment_3 = "===================="; // Notification Options +input bool EnableNotify = false; // Enable notifications feature +input bool SendAlert = true; // Send alert notification +input bool SendApp = false; // Send notification to mobile +input bool SendEmail = false; // Send notification via email +input string Comment_3a = "===================="; // Graphical Window +input bool ShowPanel = true; // Show graphical panel +input string ExpertName = "TSOP"; // Expert name (to name the objects) +input int Xoff = 20; // Horizontal spacing for the control panel +input int Yoff = 20; // Vertical spacing for the control panel +input ENUM_BASE_CORNER ChartCorner = CORNER_LEFT_UPPER; // Chart Corner +input int FontSize = 10; // Font Size +input string Comment_4 = "===================="; // Potential TSL Lines +input bool ShowPotentialSLLines = false; // Show potential TSL lines +input color PotentialSLBuyColor = clrDodgerBlue; // Potential Buy TSL line color +input color PotentialSLSellColor = clrOrangeRed; // Potential Sell TSL line color +input ENUM_LINE_STYLE PotentialSLStyle = STYLE_DOT; // Potential TSL line style +input int PotentialSLWidth = 1; // Potential TSL line width +input int PotentialSLLabelFontSize = 8; // Potential TSL label font size +input string Comment_5 = "===================="; // Activation Lines +input bool ShowActivationLines = false; // Show activation lines +input color ActivationBuyColor = clrDarkGray; // Activation Buy line color +input color ActivationSellColor = clrDarkSlateGray; // Activation Sell line color +input ENUM_LINE_STYLE ActivationStyle = STYLE_DASH; // Activation line style +input int ActivationWidth = 1; // Activation line width +input int ActivationFontSize = 8; // Activation label font size + +int OrderOpRetry = 5; // Number of order modification attempts. +double DPIScale; // Scaling parameter for the panel based on the screen DPI. +int PanelMovY, PanelLabX, PanelLabY, PanelRecX; +bool EnableTrailing = EnableTrailingParam; + +void OnInit() +{ + if (TrailingStop <= 0) + { + Alert("Trailing Stop should be > 0."); + } + + EnableTrailing = EnableTrailingParam; + + DPIScale = (double)TerminalInfoInteger(TERMINAL_SCREEN_DPI) / 96.0; + + PanelMovY = (int)MathRound(20 * DPIScale); + PanelLabX = (int)MathRound(150 * DPIScale); + PanelLabY = PanelMovY; + PanelRecX = PanelLabX + 4; + + if (ShowPanel) DrawPanel(); + if (ShowPotentialSLLines || ShowActivationLines) EventSetTimer(1); // If lines are required, use timer to refresh the lines for off-market hours. +} + +void OnDeinit(const int reason) +{ + CleanPanel(); + CleanPotentialSLLines(); + CleanActivationLines(); +} + +void OnTick() +{ + if (EnableTrailing) DoTrailingStop(); + DrawPotentialSLLines(); + DrawActivationLines(); +} + +void OnTimer() +{ + DrawPotentialSLLines(); + DrawActivationLines(); +} + +void OnChartEvent(const int id, + const long &lparam, + const double &dparam, + const string &sparam) +{ + if (id == CHARTEVENT_OBJECT_CLICK) + { + if (sparam == PanelEnableDisable) // Click on the enable/disable button. + { + ChangeTrailingEnabled(); + } + } + else if (id == CHARTEVENT_KEYDOWN) + { + if (lparam == 27) // Escape key. + { + if (MessageBox("Are you sure you want to close the EA?", "Terminate?", MB_YESNO) == IDYES) + { + ExpertRemove(); + } + } + } + else if (id == CHARTEVENT_CHART_CHANGE) + { + RepositionLabels(); + } +} + +void DoTrailingStop() +{ + for (int i = OrdersTotal() - 1; i >= 0; i--) + { + if (OrderSelect(i, SELECT_BY_POS, MODE_TRADES) == false) + { + int Error = GetLastError(); + string ErrorText = ErrorDescription(Error); + Print("ERROR - Unable to select the order - ", Error); + Print("ERROR - ", ErrorText); + break; + } + if (OnlyCurrentSymbol && OrderSymbol() != Symbol()) continue; + if (UseMagic && OrderMagicNumber() != MagicNumber) continue; + if (UseComment && StringFind(OrderComment(), CommentFilter) < 0) continue; + if (OnlyType != All && OrderType() != OnlyType) continue; + + int eDigits = (int)SymbolInfoInteger(OrderSymbol(), SYMBOL_DIGITS); + double point = SymbolInfoDouble(OrderSymbol(), SYMBOL_POINT); + double ask = SymbolInfoDouble(OrderSymbol(), SYMBOL_ASK); + double bid = SymbolInfoDouble(OrderSymbol(), SYMBOL_BID); + double TickSize = SymbolInfoDouble(OrderSymbol(), SYMBOL_TRADE_TICK_SIZE); + + // Normalize trailing stop value to the point value. + double TSTP = TrailingStop * point; + double P = Profit * point; + + if (OrderType() == OP_BUY) + { + if (NormalizeDouble(bid - OrderOpenPrice(), eDigits) >= NormalizeDouble(P, eDigits)) + { + double new_sl = NormalizeDouble(bid - TSTP, eDigits); + if (TickSize > 0) // Adjust for tick size granularity. + { + new_sl = NormalizeDouble(MathRound(new_sl / TickSize) * TickSize, eDigits); + } + if (TSTP != 0 && OrderStopLoss() < new_sl) + { + double new_tp = OrderTakeProfit(); + if (DisableTPonTSL) new_tp = 0; + ModifyOrder(OrderTicket(), OrderOpenPrice(), new_sl, new_tp, OrderSymbol()); + } + } + } + else if (OrderType() == OP_SELL) + { + if (NormalizeDouble(OrderOpenPrice() - ask, eDigits) >= NormalizeDouble(P, eDigits)) + { + double new_sl = NormalizeDouble(ask + TSTP, eDigits); + if (TickSize > 0) // Adjust for tick size granularity. + { + new_sl = NormalizeDouble(MathRound(new_sl / TickSize) * TickSize, eDigits); + } + if ((TSTP != 0 && OrderStopLoss() > new_sl) || OrderStopLoss() == 0) + { + double new_tp = OrderTakeProfit(); + if (DisableTPonTSL) new_tp = 0; + ModifyOrder(OrderTicket(), OrderOpenPrice(), new_sl, new_tp, OrderSymbol()); + } + } + } + } +} + +void ModifyOrder(int Ticket, double OpenPrice, double SLPrice, double TPPrice, string symbol) +{ + string TP_text = ""; + if (DisableTPonTSL && OrderTakeProfit() > 0) TP_text = ". TP set to zero."; + for (int i = 1; i <= OrderOpRetry; i++) // Several attempts to modify the order. + { + bool result = OrderModify(Ticket, OpenPrice, SLPrice, TPPrice, 0); + if (result) + { + Print("TRADE - UPDATE SUCCESS - Order ", Ticket, " new stop-loss ", SLPrice, TP_text); + NotifyStopLossUpdate(Ticket, SLPrice, symbol, TP_text); + break; + } + else + { + int Error = GetLastError(); + string ErrorText = ErrorDescription(Error); + Print("ERROR - UPDATE FAILED - error modifying order ", Ticket, " return error: ", Error, " Open=", OpenPrice, + " Old SL=", OrderStopLoss(), + " New SL=", SLPrice, " Bid=", SymbolInfoDouble(symbol, SYMBOL_BID), " Ask=", SymbolInfoDouble(symbol, SYMBOL_ASK)); + Print("ERROR - ", ErrorText); + } + } +} + +void NotifyStopLossUpdate(int Ticket, double SLPrice, string symbol, string TP_text) +{ + if (!EnableNotify) return; + if (!SendAlert && !SendApp && !SendEmail) return; + int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS); + string EmailSubject = ExpertName + " " + symbol + " Notification"; + string EmailBody = AccountCompany() + " - " + AccountName() + " - " + IntegerToString(AccountNumber()) + "\r\n\r\n" + ExpertName + " Notification for " + symbol + "\r\n\r\n"; + EmailBody += "Stop-loss for order " + IntegerToString(Ticket) + " moved to " + DoubleToString(SLPrice, digits) + TP_text; + string AlertText = ExpertName + " - " + Symbol() + " Notification: "; + AlertText += "Stop-loss for order " + IntegerToString(Ticket) + " moved to " + DoubleToString(SLPrice, digits) + TP_text; + string AppText = AccountCompany() + " - " + AccountName() + " - " + IntegerToString(AccountNumber()) + " - " + ExpertName + " - " + symbol + " - "; + AppText += "Stop-loss for order " + IntegerToString(Ticket) + " moved to " + DoubleToString(SLPrice, digits) + TP_text; + if (SendAlert) Alert(AlertText); + if (SendEmail) + { + if (!SendMail(EmailSubject, EmailBody)) Print("Error sending email " + IntegerToString(GetLastError())); + } + if (SendApp) + { + if (!SendNotification(AppText)) Print("Error sending notification " + IntegerToString(GetLastError())); + } + Print(ExpertName + " - last notification sent on " + TimeToString(TimeCurrent())); +} + +string PanelBase = ExpertName + "-P-BAS"; +string PanelLabel = ExpertName + "-P-LAB"; +string PanelEnableDisable = ExpertName + "-P-ENADIS"; +string PotentialSLLinePrefix = ExpertName + "-SL-"; +string PotentialSLLabelPrefix = ExpertName + "-SLL-"; +string ActivationLinePrefix = ExpertName + "-ACT-"; +string ActivationLabelPrefix = ExpertName + "-ACTL-"; + +void DrawPanel() +{ + int SignX = 1; + int YAdjustment = 0; + if ((ChartCorner == CORNER_RIGHT_UPPER) || (ChartCorner == CORNER_RIGHT_LOWER)) + { + SignX = -1; // Correction for right-side panel position. + } + if ((ChartCorner == CORNER_RIGHT_LOWER) || (ChartCorner == CORNER_LEFT_LOWER)) + { + YAdjustment = (PanelMovY + 2) * 2 + 1 - PanelLabY; // Correction for upper side panel position. + } + + string PanelText = "TSL on Profit"; + string PanelToolTip = "Trailing Stop on Profit by EarnForex"; + int Rows = 1; + ObjectCreate(ChartID(), PanelBase, OBJ_RECTANGLE_LABEL, 0, 0, 0); + ObjectSetInteger(ChartID(), PanelBase, OBJPROP_CORNER, ChartCorner); + ObjectSetInteger(ChartID(), PanelBase, OBJPROP_XDISTANCE, Xoff); + ObjectSetInteger(ChartID(), PanelBase, OBJPROP_YDISTANCE, Yoff + YAdjustment); + ObjectSetInteger(ChartID(), PanelBase, OBJPROP_XSIZE, PanelRecX); + ObjectSetInteger(ChartID(), PanelBase, OBJPROP_YSIZE, (PanelMovY + 1) * (Rows + 1) + 3); + ObjectSetInteger(ChartID(), PanelBase, OBJPROP_BGCOLOR, clrWhite); + ObjectSetInteger(ChartID(), PanelBase, OBJPROP_BORDER_TYPE, BORDER_FLAT); + ObjectSetInteger(ChartID(), PanelBase, OBJPROP_STATE, false); + ObjectSetInteger(ChartID(), PanelBase, OBJPROP_HIDDEN, true); + ObjectSetInteger(ChartID(), PanelBase, OBJPROP_SELECTABLE, false); + ObjectSetInteger(ChartID(), PanelBase, OBJPROP_COLOR, clrBlack); + + DrawEdit(PanelLabel, + Xoff + 2 * SignX, + Yoff + 2, + PanelLabX, + PanelLabY, + true, + FontSize, + PanelToolTip, + ALIGN_CENTER, + "Consolas", + PanelText, + false, + clrNavy, + clrKhaki, + clrBlack); + ObjectSetInteger(ChartID(), PanelLabel, OBJPROP_CORNER, ChartCorner); + + string EnableDisabledText = ""; + color EnableDisabledColor = clrNavy; + color EnableDisabledBack = clrKhaki; + if (EnableTrailing) + { + EnableDisabledText = "TRAILING ENABLED"; + EnableDisabledColor = clrWhite; + EnableDisabledBack = clrDarkGreen; + } + else + { + EnableDisabledText = "TRAILING DISABLED"; + EnableDisabledColor = clrWhite; + EnableDisabledBack = clrDarkRed; + } + + DrawEdit(PanelEnableDisable, + Xoff + 2 * SignX, + Yoff + (PanelMovY + 1) * Rows + 2, + PanelLabX, + PanelLabY, + true, + FontSize, + "Click to enable or disable the trailing stop feature", + ALIGN_CENTER, + "Consolas", + EnableDisabledText, + false, + EnableDisabledColor, + EnableDisabledBack, + clrBlack); + ObjectSetInteger(ChartID(), PanelEnableDisable, OBJPROP_CORNER, ChartCorner); +} + +void CleanPanel() +{ + ObjectsDeleteAll(ChartID(), ExpertName); +} + +void ChangeTrailingEnabled() +{ + if (EnableTrailing == false) + { + if (!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) + { + MessageBox("Automated trading is disabled in the platform's options! Please enable it via Tools->Options->Expert Advisors.", "WARNING", MB_OK); + return; + } + if (!MQLInfoInteger(MQL_TRADE_ALLOWED)) + { + MessageBox("Live Trading is disabled in the expert advisors's settings! Please tick the Allow Live Trading checkbox on the Common tab.", "WARNING", MB_OK); + return; + } + EnableTrailing = true; + } + else EnableTrailing = false; + DrawPanel(); + DrawActivationLines(); +} + +void DrawPotentialSLLines() +{ + if (!ShowPotentialSLLines) + { + CleanPotentialSLLines(); + return; + } + + datetime leftTime = GetLeftVisibleBarTime(); + + // Collect tickets that should currently have a potential TSL line. + ulong activeTickets[]; + int activeCount = 0; + + for (int i = 0; i < OrdersTotal(); i++) + { + if (OrderSelect(i, SELECT_BY_POS, MODE_TRADES) == false) + { + int Error = GetLastError(); + string ErrorText = ErrorDescription(Error); + Print("ERROR - Unable to select the order - ", Error); + Print("ERROR - ", ErrorText); + continue; + } + int ticket = OrderTicket(); + if (OnlyCurrentSymbol && OrderSymbol() != Symbol()) continue; + if (UseMagic && OrderMagicNumber() != MagicNumber) continue; + if (UseComment && StringFind(OrderComment(), CommentFilter) < 0) continue; + if (OnlyType != All && OrderType() != OnlyType) continue; + + string Instrument = OrderSymbol(); + double openPrice = OrderOpenPrice(); + int eDigits = (int)SymbolInfoInteger(Instrument, SYMBOL_DIGITS); + double point = SymbolInfoDouble(Instrument, SYMBOL_POINT); + + // Initial TSL = OpenPrice +/- (Profit - TrailingStop) * point. + // Rationale: trailing activates after price moves Profit*point in favour; + // at that instant the EA places SL TrailingStop*point on the safe side, + // so the level depends only on OpenPrice and the EA inputs (i.e. fixed). + double slDist = (Profit - TrailingStop) * point; + + double slLevel = 0; + color lineColor = clrNONE; + string dirLabel = ""; + if (OrderType() == OP_BUY) + { + slLevel = NormalizeDouble(openPrice + slDist, eDigits); + // Skip if the position's SL has already been moved to (or beyond) the Initial TSL. + // OrderStopLoss() == 0 (no SL set) naturally fails this check, so the line is kept. + if (OrderStopLoss() >= slLevel) continue; + lineColor = PotentialSLBuyColor; + dirLabel = "Buy"; + } + else if (OrderType() == OP_SELL) + { + slLevel = NormalizeDouble(openPrice - slDist, eDigits); + // For sells, "beyond" means a lower SL. Exclude OrderStopLoss() == 0 (no SL set). + if (OrderStopLoss() > 0 && OrderStopLoss() <= slLevel) continue; + lineColor = PotentialSLSellColor; + dirLabel = "Sell"; + } + else continue; // Pending orders: no potential TSL line. + + string lineName = PotentialSLLinePrefix + IntegerToString(ticket); + string tooltip = "Initial TSL #" + IntegerToString(ticket) + " " + dirLabel + ": " + DoubleToString(slLevel, eDigits); + CreateOrMoveHLine(lineName, slLevel, lineColor, PotentialSLStyle, PotentialSLWidth, tooltip); + + string labelName = PotentialSLLabelPrefix + IntegerToString(ticket); + string labelText = "#" + IntegerToString(ticket) + " " + dirLabel + " Initial TSL"; + CreateOrMoveLabel(labelName, leftTime, slLevel, labelText, lineColor, PotentialSLLabelFontSize); + + ArrayResize(activeTickets, activeCount + 1); + activeTickets[activeCount] = ticket; + activeCount++; + } + + // Remove potential TSL lines + labels for orders that no longer exist or are now filtered out. + int totalObjects = ObjectsTotal(0, 0, OBJ_HLINE); + for (int i = totalObjects - 1; i >= 0; i--) + { + string objName = ObjectName(0, i, 0, OBJ_HLINE); + if (StringFind(objName, PotentialSLLinePrefix) != 0) continue; + string ticketStr = StringSubstr(objName, StringLen(PotentialSLLinePrefix)); + ulong objTicket = (ulong)StringToInteger(ticketStr); + bool found = false; + for (int j = 0; j < activeCount; j++) + { + if (activeTickets[j] == objTicket) + { + found = true; + break; + } + } + if (!found) + { + ObjectDelete(0, objName); + ObjectDelete(0, PotentialSLLabelPrefix + ticketStr); + } + } + // Also clean orphaned labels (line was already removed). + int totalText = ObjectsTotal(0, 0, OBJ_TEXT); + for (int i = totalText - 1; i >= 0; i--) + { + string objName = ObjectName(0, i, 0, OBJ_TEXT); + if (StringFind(objName, PotentialSLLabelPrefix) != 0) continue; + string ticketStr = StringSubstr(objName, StringLen(PotentialSLLabelPrefix)); + ulong objTicket = (ulong)StringToInteger(ticketStr); + bool found = false; + for (int j = 0; j < activeCount; j++) + { + if (activeTickets[j] == objTicket) + { + found = true; + break; + } + } + if (!found) ObjectDelete(0, objName); + } +} + +void CleanPotentialSLLines() +{ + ObjectsDeleteAll(0, PotentialSLLinePrefix); + ObjectsDeleteAll(0, PotentialSLLabelPrefix); +} + +void DrawActivationLines() +{ + if (!ShowActivationLines || !EnableTrailing) + { + CleanActivationLines(); + return; + } + + if (Profit <= 0) + { + CleanActivationLines(); + return; + } + + datetime leftTime = GetLeftVisibleBarTime(); + + // Collect tickets that should currently have an activation line. + ulong activeTickets[]; + int activeCount = 0; + + for (int i = 0; i < OrdersTotal(); i++) + { + if (OrderSelect(i, SELECT_BY_POS, MODE_TRADES) == false) + { + int Error = GetLastError(); + string ErrorText = ErrorDescription(Error); + Print("ERROR - Unable to select the order - ", Error); + Print("ERROR - ", ErrorText); + continue; + } + int ticket = OrderTicket(); + if (OnlyCurrentSymbol && OrderSymbol() != Symbol()) continue; + if (UseMagic && OrderMagicNumber() != MagicNumber) continue; + if (UseComment && StringFind(OrderComment(), CommentFilter) < 0) continue; + if (OnlyType != All && OrderType() != OnlyType) continue; + + string Instrument = OrderSymbol(); + double openPrice = OrderOpenPrice(); + int eDigits = (int)SymbolInfoInteger(Instrument, SYMBOL_DIGITS); + double point = SymbolInfoDouble(Instrument, SYMBOL_POINT); + double activationDist = Profit * point; + if (activationDist == 0) continue; + + double activationLevel = 0; + color lineColor = clrNONE; + string dirLabel = ""; + if (OrderType() == OP_BUY) + { + activationLevel = NormalizeDouble(openPrice + activationDist, eDigits); + // Skip activation lines that have already been crossed - trailing is active. + if (SymbolInfoDouble(Instrument, SYMBOL_BID) >= activationLevel) continue; + lineColor = ActivationBuyColor; + dirLabel = "Buy"; + } + else if (OrderType() == OP_SELL) + { + activationLevel = NormalizeDouble(openPrice - activationDist, eDigits); + if (SymbolInfoDouble(Instrument, SYMBOL_ASK) <= activationLevel) continue; + lineColor = ActivationSellColor; + dirLabel = "Sell"; + } + else continue; // Pending orders: no activation line. + + string lineName = ActivationLinePrefix + IntegerToString(ticket); + string tooltip = "TSL Activation #" + IntegerToString(ticket) + " " + dirLabel + ": " + DoubleToString(activationLevel, eDigits); + CreateOrMoveHLine(lineName, activationLevel, lineColor, ActivationStyle, ActivationWidth, tooltip); + + string labelName = ActivationLabelPrefix + IntegerToString(ticket); + string labelText = "#" + IntegerToString(ticket) + " " + dirLabel + " TSL Activation"; + CreateOrMoveLabel(labelName, leftTime, activationLevel, labelText, lineColor, ActivationFontSize); + + ArrayResize(activeTickets, activeCount + 1); + activeTickets[activeCount] = ticket; + activeCount++; + } + + // Remove activation lines + labels for orders that no longer exist or have activated. + int totalObjects = ObjectsTotal(0, 0, OBJ_HLINE); + for (int i = totalObjects - 1; i >= 0; i--) + { + string objName = ObjectName(0, i, 0, OBJ_HLINE); + if (StringFind(objName, ActivationLinePrefix) != 0) continue; + string ticketStr = StringSubstr(objName, StringLen(ActivationLinePrefix)); + ulong objTicket = (ulong)StringToInteger(ticketStr); + bool found = false; + for (int j = 0; j < activeCount; j++) + { + if (activeTickets[j] == objTicket) + { + found = true; + break; + } + } + if (!found) + { + ObjectDelete(0, objName); + ObjectDelete(0, ActivationLabelPrefix + ticketStr); + } + } + // Also clean orphaned labels (line was already removed). + int totalText = ObjectsTotal(0, 0, OBJ_TEXT); + for (int i = totalText - 1; i >= 0; i--) + { + string objName = ObjectName(0, i, 0, OBJ_TEXT); + if (StringFind(objName, ActivationLabelPrefix) != 0) continue; + string ticketStr = StringSubstr(objName, StringLen(ActivationLabelPrefix)); + ulong objTicket = (ulong)StringToInteger(ticketStr); + bool found = false; + for (int j = 0; j < activeCount; j++) + { + if (activeTickets[j] == objTicket) + { + found = true; + break; + } + } + if (!found) ObjectDelete(0, objName); + } +} + +void CleanActivationLines() +{ + ObjectsDeleteAll(0, ActivationLinePrefix); + ObjectsDeleteAll(0, ActivationLabelPrefix); +} + +// Time of the leftmost visible bar - used to anchor activation text labels. +datetime GetLeftVisibleBarTime() +{ + int firstVisibleBar = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR); + datetime barTime = iTime(Symbol(), PERIOD_CURRENT, firstVisibleBar); + if (barTime == 0) barTime = TimeCurrent(); // Fallback. + return barTime; +} + +void CreateOrMoveHLine(string name, double price, color clr, ENUM_LINE_STYLE style, int width, string tooltip) +{ + ObjectCreate(0, name, OBJ_HLINE, 0, 0, price); + ObjectSetDouble(0, name, OBJPROP_PRICE, price); + ObjectSetInteger(0, name, OBJPROP_COLOR, clr); + ObjectSetInteger(0, name, OBJPROP_STYLE, style); + ObjectSetInteger(0, name, OBJPROP_WIDTH, width); + ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); + ObjectSetInteger(0, name, OBJPROP_HIDDEN, true); + ObjectSetInteger(0, name, OBJPROP_BACK, true); + ObjectSetString(0, name, OBJPROP_TOOLTIP, tooltip); +} + +void CreateOrMoveLabel(string name, datetime time, double price, string text, color clr, int fontSize) +{ + ObjectCreate(0, name, OBJ_TEXT, 0, time, price); + ObjectSetInteger(0, name, OBJPROP_TIME, time); + ObjectSetDouble(0, name, OBJPROP_PRICE, price); + ObjectSetString(0, name, OBJPROP_TEXT, text); + ObjectSetString(0, name, OBJPROP_FONT, "Consolas"); + ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize); + ObjectSetInteger(0, name, OBJPROP_COLOR, clr); + ObjectSetInteger(0, name, OBJPROP_ANCHOR, ANCHOR_LEFT_LOWER); + ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); + ObjectSetInteger(0, name, OBJPROP_HIDDEN, true); +} + +// Move EA-owned labels to the current leftmost visible bar so they follow the chart when the user scrolls or zooms. +void RepositionLabels() +{ + if (!ShowActivationLines && !ShowPotentialSLLines) return; + + datetime leftTime = GetLeftVisibleBarTime(); + int totalText = ObjectsTotal(0, 0, OBJ_TEXT); + for (int i = totalText - 1; i >= 0; i--) + { + string objName = ObjectName(0, i, 0, OBJ_TEXT); + if (StringFind(objName, ActivationLabelPrefix) != 0 && + StringFind(objName, PotentialSLLabelPrefix) != 0) continue; + ObjectSetInteger(0, objName, OBJPROP_TIME, leftTime); + } + ChartRedraw(); +} +//+------------------------------------------------------------------+ \ No newline at end of file diff --git a/60-Trailing-Stop/Trailing-Stop.pdf b/60-Trailing-Stop/Trailing-Stop.pdf new file mode 100644 index 0000000..14d372c Binary files /dev/null and b/60-Trailing-Stop/Trailing-Stop.pdf differ diff --git a/61-Telegram-Bot-EA/Telegram-Bot-EA.mq5 b/61-Telegram-Bot-EA/Telegram-Bot-EA.mq5 new file mode 100644 index 0000000..13fe18b Binary files /dev/null and b/61-Telegram-Bot-EA/Telegram-Bot-EA.mq5 differ diff --git a/61-Telegram-Bot-EA/Telegram-Bot-EA.pdf b/61-Telegram-Bot-EA/Telegram-Bot-EA.pdf new file mode 100644 index 0000000..2952d82 Binary files /dev/null and b/61-Telegram-Bot-EA/Telegram-Bot-EA.pdf differ diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..1684f2e --- /dev/null +++ b/README.txt @@ -0,0 +1,34 @@ +================================================================= + ФОРЕКС АРСЕНАЛ — СОДЕРЖИМОЕ ПАПКИ +================================================================= + +📁 indicators_mt4/ — сюда скачать MT4 индикаторы (исходники .mq4) +📁 indicators_mt5/ — сюда скачать MT5 индикаторы (исходники .mq5) +📁 experts_mt4/ — сюда скачать MT4 советники (исходники .mq4) +📁 experts_mt5/ — сюда скачать MT5 советники (исходники .mq5) +📁 scripts/ — скрипты для MT4/MT5 +📁 libraries/ — библиотеки для MT4/MT5 +📁 telegram_bridges/ — решения для связи Telegram ↔ MT4/MT5 +📁 neural_software/ — нейросетевой софт для анализа сделок +📁 screenshots/ — скриншоты индикаторов +📁 website/ — HTML-сайт-каталог (index.html) + +📄 СПИСОК_ИСТОЧНИКОВ.txt — все ссылки для скачивания + +================================================================= +КАК СКАЧАТЬ ИНДИКАТОРЫ: + +1. Открой MQL5 Code Base: https://www.mql5.com/en/code/mt4/indicators +2. Нажми "Download" у любого индикатора +3. Сохрани файл .mq4 или .mq5 в соответствующую папку +4. Помести файл в папку Indicators вашего MetaTrader +5. Перезапусти терминал — индикатор появится в списке + +GitHub коллекции — скачать ZIP: +1. Открой репозиторий на GitHub +2. Нажми зелёную кнопку "Code" → "Download ZIP" +3. Распакуй в соответствующую папку + +================================================================= +ГОТОВЫЕ СТРАТЕГИИ — смотри в файле СПИСОК_ИСТОЧНИКОВ.txt +================================================================= diff --git a/experts_mt4/EA31337_Libre.zip b/experts_mt4/EA31337_Libre.zip new file mode 100644 index 0000000..8a208aa Binary files /dev/null and b/experts_mt4/EA31337_Libre.zip differ diff --git a/experts_mt4/nkanven_EA_Collection.zip b/experts_mt4/nkanven_EA_Collection.zip new file mode 100644 index 0000000..359648a Binary files /dev/null and b/experts_mt4/nkanven_EA_Collection.zip differ diff --git a/experts_mt5/GOLD_ORB.zip b/experts_mt5/GOLD_ORB.zip new file mode 100644 index 0000000..54b740a Binary files /dev/null and b/experts_mt5/GOLD_ORB.zip differ diff --git a/experts_mt5/Gold_EA_OpenAI.zip b/experts_mt5/Gold_EA_OpenAI.zip new file mode 100644 index 0000000..543c44a Binary files /dev/null and b/experts_mt5/Gold_EA_OpenAI.zip differ diff --git a/experts_mt5/nyao_scalper_mt5.zip b/experts_mt5/nyao_scalper_mt5.zip new file mode 100644 index 0000000..6d02059 Binary files /dev/null and b/experts_mt5/nyao_scalper_mt5.zip differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..c031565 --- /dev/null +++ b/index.html @@ -0,0 +1,600 @@ + + + + + +Forex Navigator — 61 EA + 11 AI Tools + + + + +
+ +
+ +

Коллекция Expert Advisors для MT4/MT5 и AI/ML инструментов для автоматической торговли

+
+ +
+
61
Советников EA
+
40
MT4
+
21
MT5
+
43
Стратегий
+
11
AI/ML проектов
+
+ +
+
+ + + +
+ +
+ +
+ + +
+ +

😕 Ничего не найдено

Попробуйте изменить параметры поиска или фильтры

+ + +
+ + +
+ + +
+ +
+
+ + + + +
+Forex Navigator · 61 Expert Advisors · 11 AI/ML Tools · Собрано и организовано Hermes AI +
+ + + + + \ No newline at end of file diff --git a/indicators_mt4/GeneralTradingSarl_MT4_Part1.zip b/indicators_mt4/GeneralTradingSarl_MT4_Part1.zip new file mode 100644 index 0000000..d814da7 Binary files /dev/null and b/indicators_mt4/GeneralTradingSarl_MT4_Part1.zip differ diff --git a/indicators_mt4/GeneralTradingSarl_MT4_Part2.zip b/indicators_mt4/GeneralTradingSarl_MT4_Part2.zip new file mode 100644 index 0000000..133003e Binary files /dev/null and b/indicators_mt4/GeneralTradingSarl_MT4_Part2.zip differ diff --git a/indicators_mt4/RoundPriceLevels.zip b/indicators_mt4/RoundPriceLevels.zip new file mode 100644 index 0000000..f64f9a9 Binary files /dev/null and b/indicators_mt4/RoundPriceLevels.zip differ diff --git a/indicators_mt5/GeneralTradingSarl_MT5_Part1.zip b/indicators_mt5/GeneralTradingSarl_MT5_Part1.zip new file mode 100644 index 0000000..39c61ab Binary files /dev/null and b/indicators_mt5/GeneralTradingSarl_MT5_Part1.zip differ diff --git a/indicators_mt5/GeneralTradingSarl_MT5_Part2.zip b/indicators_mt5/GeneralTradingSarl_MT5_Part2.zip new file mode 100644 index 0000000..f918ba0 Binary files /dev/null and b/indicators_mt5/GeneralTradingSarl_MT5_Part2.zip differ diff --git a/website/index.html b/website/index.html new file mode 100644 index 0000000..2df0760 --- /dev/null +++ b/website/index.html @@ -0,0 +1,1071 @@ + + + + + +Форекс Арсенал — Полный каталог индикаторов, советников и софта + + + +
+

🏆 ФОРЕКС АРСЕНАЛ

+

Полный каталог индикаторов, советников, телеграм-мостов и нейросетевого софта для Forex и Gold (XAUUSD)

+
+ +
+ +
+

📊 Общая статистика каталога

+
+
300+
Индикаторов с исходниками
+
150+
Советников (EA)
+
50+
Скриптов и библиотек
+
20+
Телеграм-решений
+
15+
Нейросетевых инструментов
+
10+
Порталов-источников
+
+
+ℹ️ Важно: Все индикаторы и советники с пометкой ✅ Исходник имеют открытый код mq4/mq5 и доступны для бесплатного скачивания. +Перед использованием на реальном счёте — обязательно тестируйте на демо! +
+
+ +
+

📈 Индикаторы по категориям 300+

+ + +

🎯 Трендовые индикаторы

+
+
+

SuperTrend_Enhanced MT5

+

Динамический трендовый индикатор на основе ATR. Рисует цветную линию выше/ниже цены для обозначения бычьего/медвежьего тренда. Стрелки при пересечении.

+
ATR-basedСтрелкиМетки✅ Исходник
+ +
+
+

SuperTrend Quant Pro Elite MT5

+

Институциональный движок с Z-Score адаптивной волатильностью, фильтрами объёма Smart Money и MTF дашбордом в реальном времени.

+
Z-ScoreSmart MoneyMTF✅ Исходник
+ +
+
+

Self-Aware Trend System MT5

+

Адаптивная система SuperTrend + TQI (Trend Quality Index). Динамическое обнаружение сигналов, уровни риск-менеджмента, самообучающаяся калибровка.

+
АдаптивныйСамообучениеРиск-менеджмент✅ Исходник
+ +
+
+

GlowTrend Pro MT4/MT5

+

Трендовый индикатор с изменением цвета на основе адаптивных скользящих средних. Сигналы разворота в реальном времени.

+
Адаптивные МАСигналы разворота✅ Исходник
+ +
+
+

M4HA MT4

+

Трендовый индикатор на основе пересечения адаптивной HMA со сглаженной линией. Рекомендуется использовать с индикатором флэта для раннего обнаружения тренда.

+
HMAАдаптивный✅ Исходник
+ +
+
+

Trend Vision MT4 MT4

+

Комбинация моментума MACD + волатильности Bollinger Bands. Фильтрует ложные движения — сигнал только при согласии моментума И волатильности.

+
MACD + BBФильтр ложных✅ Исходник
+ +
+
+

Aura Heiken Ashi MT4/MT5

+

Продвинутый сглаженный Heiken Ashi с автоматическими зонами спроса/предложения, EMA-фильтром тренда и мульти-индикаторным подтверждением моментума.

+
Heiken AshiSupply/DemandEMA фильтр✅ Исходник
+ +
+
+

Parabolic SAR (встроенный) MT4/MT5

+

Классический индикатор параболического разворота. Точки SAR показывают потенциальные точки разворота тренда.

+
РазворотТрендВстроенный
+
+
+

ADX / ADXm MT4/MT5

+

Индекс среднего направленного движения. Измеряет силу тренда независимо от направления. ADXm — экспериментальная версия с VHF адаптацией.

+
Сила трендаVHF адаптивный✅ Исходник
+ +
+
+

Ichimoku Signals Cloud MT4/MT5

+

Облако Ишимоку с торговыми сигналами. Tenkan-sen, Kijun-sen, Senkou Span A/B, Chikou Span — полная система в одном индикаторе.

+
ИшимокуОблакоСигналы✅ Исходник
+ +
+
+

Gann HiLo Activator MT4/MT5

+

Активатор Ганна на основе высших максимумов и низших минимумов. Простой трендовый индикатор для определения направления.

+
ГаннТренд✅ Исходник
+
+
+

Keltner Channel MT4/MT5

+

Канал Кельтнера на основе ATR. Центральная линия — EMA, верх/низ — EMA ± ATR×множитель. Для определения тренда и перекупленности.

+
ATR каналEMA✅ Исходник
+
+
+

Donchian Channel MT4/MT5

+

Канал Дончиана — максимумы/минимумы за N периодов. Классический индикатор для пробойных стратегий (система Черепах).

+
ПробойМаксимумы/минимумы✅ Исходник
+
+
+

NRTR (Nick Rypock Trailing Reverse) MT4/MT5

+

Трейлинг-стоп индикатор с разворотом. Показывает уровни для trailing stop с автоматическим переключением при развороте.

+
Trailing StopРазворот✅ Исходник
+
+
+

Center of Gravity MT4/MT5

+

Индикатор центра тяжести Джона Эйлерса. Рассчитывает статистический центр ценового диапазона с каналами.

+
СтатистическийКанал✅ Исходник
+
+
+ +

📊 Осцилляторы

+
+
+

RSI Timeframe Analyzer MT5

+

Мульти-таймфрейм RSI дашборд. Показывает перекупленность/перепроданность/нейтралитет сразу на 9 таймфреймах (M1–MN1).

+
MTFДашборд9 ТФ✅ Исходник
+ +
+
+

Reverse Engineered RSI MT4

+

Проецирует уровни RSI 30, 50, 70 прямо на ценовой график. Показывает, где зоны перекупленности/перепроданности совпадают с реальными ценами.

+
RSI на ценеУровни 30/50/70✅ Исходник
+ +
+
+

MACD-v MT4

+

Волатильностно-нормализованный MACD. Повышенная стабильность при разных рыночных условиях. Сигналы моментума, пересечения, расширения/сжатия.

+
НормализованныйСтабильность✅ Исходник
+ +
+
+

MACD on Chart MT4/MT5

+

Отображает MACD прямо на основном ценовом графике, а не в отдельном окне. Удобно для совмещения с ценовым анализом.

+
На графикеMACD✅ Исходник
+ +
+
+

CCI Arrows MT4 MT4/MT5

+

Отмечает пересечения CCI нулевой линии красными/синими стрелками. Минимальная задержка, высокая точность. Все типы алертов.

+
СтрелкиCCIАлерты✅ Исходник
+ +
+
+

Stochastic (встроенный) MT4/MT5

+

Классический стохастический осциллятор. %K и %D линии для определения перекупленности/перепроданности. 80/20 уровни.

+
%K/%DПерекупленностьВстроенный
+
+
+

Awesome Oscillator MT4/MT5

+

Удивительный осциллятор Билла Вильямса. Разница между 5-периодной и 34-периодной простой скользящей средней по медианным ценам.

+
Билл ВильямсГистограммаВстроенный
+
+
+

Wave Trend Oscillator MT4/MT5

+

Волновой трендовый осциллятор. Для определения зон перекупленности/перепроданности и входов. Популярен для скальпинга.

+
Wave TrendСкальпинг✅ Исходник
+ +
+
+

SMI Ergodic Oscillator MT4/MT5

+

Сглаженный моментумный осциллятор. Для поиска разворотов с плавными сигналами. Лучше классического стохастика на трендовых рынках.

+
SMIРазворотПлавный✅ Исходник
+
+
+

Market Structure Oscillator MT4/MT5

+

Детектирует BOS (Break of Structure) и CHoCH (Change of Character) для Smart Money Concepts. Осциллятор рыночной структуры.

+
BOS/CHoCHSmart Money✅ Исходник
+ +
+
+

Bulls Power / Bears Power MT4/MT5

+

Сила быков и сила медведей по Элдеру. Разница между максимумом/минимумом и EMA. Для определения доминирования покупателей/продавцов.

+
ЭлдерСила быков/медведейВстроенный
+
+
+

Relative Vigor Index (RVI) MT4/MT5

+

Индекс относительной бодрости. Сравнивает цену закрытия с ценой открытия и масштабирует диапазоном. Для подтверждения тренда.

+
RVIПодтверждение тренда✅ Исходник
+
+
+ +

📉 Волатильность и каналы

+
+
+

Bollinger Bands (встроенные) MT4/MT5

+

Классические полосы Боллинджера. Средняя линия — SMA 20, верх/низ — ±2 стандартных отклонения. Для определения волатильности и перекупленности.

+
SMA 20Встроенный
+
+
+

Bollinger Trend MT4/MT5

+

Переосмысленные полосы Боллинджера. Неперерисовывающийся индикатор с сигналами выхода. Трендовая интерпретация классических BB.

+
НеперерисовывающийсяСигналы выхода✅ Исходник
+ +
+
+

ATR Channels / ATR Bands MT4/MT5

+

Каналы на основе Average True Range. Центральная линия + верх/низ по ATR×множитель. 30+ вариантов в коллекциях.

+
ATRКанал30+ вариантов✅ Исходник
+
+
+

Volatility Stop MT4/MT5

+

Стоп-уровни на основе волатильности. Динамические уровни для защиты позиций, адаптирующиеся к текущей волатильности рынка.

+
ВолатильностьСтоп-уровни✅ Исходник
+ +
+
+

Volatility Oscillator MT4

+

Измеряет волатильность текущего бара относительно предыдущих 100 баров в стандартных отклонениях. Показывает дивергенции.

+
Стандартные отклоненияДивергенции✅ Исходник
+ +
+
+

Keltner Channel MT4/MT5

+

Канал Кельтнера. EMA + ATR×множитель. Для определения тренда и торговли откатами внутри канала.

+
EMAATR✅ Исходник
+
+
+

APZ (Adaptive Price Zone) MT4/MT5

+

Адаптивная ценовая зона. Динамические полосы, адаптирующиеся к волатильности. Для определения экстремумов и разворотов.

+
АдаптивныйЗона✅ Исходник
+
+
+

Standard Deviation MT4/MT5

+

Стандартное отклонение цены от скользящей средней. Базовый индикатор волатильности, используемый в Bollinger Bands.

+
СигмаВолатильностьВстроенный
+
+
+ +

📦 Объёмные индикаторы

+
+
+

Volume Profile MT4/MT5

+

Профиль объёма — показывает, где проходил основной объём торгов по ценовым уровням. Критично для определения зон ликвидности.

+
ПрофильЛиквидность✅ Исходник
+ +
+
+

VWAP / Full VWAP MT4/MT5

+

Volume Weighted Average Price — средневзвешенная по объёму цена. Институциональный бенчмарк. Для определения "справедливой" цены.

+
VWAPИнституциональный✅ Исходник
+
+
+

OBV (On Balance Volume) MT4/MT5

+

Балансовый объём. Накапливает объём при росте и вычитает при падении. Для подтверждения тренда динамикой объёма.

+
НакоплениеПодтверждение тренда✅ Исходник
+
+
+

Market Facilitation Index MT4/MT5

+

Индекс содействия рынку Билла Вильямса. Соотношение диапазона бара к объёму. Зелёный/красный/синий/коричневый бары.

+
Билл ВильямсMFIВстроенный
+
+
+

Volume Divergence MT4/MT5

+

Дивергенции объёма. Ищет расхождения между ценой и объёмом — ранний сигнал потенциального разворота.

+
ДивергенцияРазворот✅ Исходник
+
+
+

Aliev Fx Volumes MT4

+

Модифицированный индикатор объёма от Aliev. Улучшенная визуализация объёмных импульсов для MetaTrader 4.

+
ОбъёмИмпульс✅ Исходник
+
+
+ +

🎯 Уровни поддержки/сопротивления

+
+
+

Support and Resistance Lines MT4/MT5

+

Автоматические горизонтальные линии поддержки и сопротивления. Алерт на пробой/отскок. Несколько методов расчёта.

+
Авто-уровниАлерты✅ Исходник
+ +
+
+

Session Range Boxes MT4/MT5

+

Цветные боксы для сессий Азия/Лондон/Нью-Йорк с линиями расширения диапазона high/low. Статистика средних диапазонов в пипсах.

+
СессииДиапазонСтатистика✅ Исходник
+ +
+
+

Pivot Points (все виды) MT4/MT5

+

Пивот-точки: классические, Камарилья, Фибоначчи, Вуди, ДеМарк. Автоматический расчёт уровней поддержки/сопротивления на день/неделю/месяц.

+
ПивотыУровниНесколько методов✅ Исходник
+
+
+

Fibonacci Confluence Zone Finder MT4

+

Рисует Фибоначчи от нескольких точек разворота. Находит зоны конфлюэнса, где пересекаются уровни нескольких Фибо — "зоны dramatically stronger".

+
КонфлюэнсФибоначчи✅ Исходник
+ +
+
+

Auto Fibonacci Retracement MT4

+

Авто-рисование Фибоначчи ретрейсмент/экстеншн на основе последних точек ZigZag. Динамическое обновление.

+
АвтоZigZagДинамический✅ Исходник
+ +
+
+

Draw on Liquidity (DOL) Mapper MT4

+

Отображает все цели ликвидности: предыдущие день/неделя/месяц high-low, равные уровни, непротестированные FVG. "Показывает, КУДА движется цена".

+
ЛиквидностьSmart Money✅ Исходник
+ +
+
+

Breaker Block Detector MT4

+

Детектирует breaker blocks — ордер-блоки, которые пробиты и сменили полярность. "Бычий OB, пробитый вниз, становится медвежьим breaker".

+
Breaker BlockSmart Money✅ Исходник
+ +
+
+

ZigZag HH HL LH LL Pattern Label MT4

+

Улучшенный ZigZag с авто-определением Higher High, Higher Low, Lower High, Lower Low. Рисует трендовые линии. Оптимизированный MQL4 код.

+
СтруктураZigZagТрендовые линии✅ Исходник
+ +
+
+ +

🕯️ Паттерны и ценовое действие

+
+
+

Find Pin Bars MT5

+

Автоматический поиск пин-баров. Иконки на квалифицирующихся барах. Для price action трейдеров.

+
Пин-барPrice Action✅ Исходник
+ +
+
+

Inside Bar MT4/MT5

+

Авто-обнаружение Inside Bar паттернов. Рисует прямоугольники проекции, алерты в реальном времени.

+
Inside BarПроекцияАлерты✅ Исходник
+ +
+
+

Marubozu MT5

+

Длинные тела свечей без теней. Японская свечная модель с сильным импульсом.

+
МарубозуИмпульс✅ Исходник
+ +
+
+

Candle Replay Magnifier MT5

+

Исторический оверлей анализа. Воспроизводит прошлые свечи на живом графике. Динамическое выделение диапазона, тултипы OHLCV.

+
РеплейOHLCVОбучение✅ Исходник
+ +
+
+

Flag and Pennant Patterns MT4

+

Автоматическое обнаружение паттернов флаг и вымпел. Продолжение тренда после консолидации.

+
ФлагВымпелПродолжение✅ Исходник
+
+
+

Doji Reader MT4

+

Распознаёт дожи-свечи и их разновидности. Сигнал нерешительности рынка и потенциального разворота.

+
ДожиРазворот✅ Исходник
+
+
+ +

🏛️ Институциональные / Smart Money

+
+
+

ZigZag BOS CHoCH Detection MT5

+

Автоматически детектирует Break of Structure (BOS) и Change of Character (CHoCH) по точкам ZigZag. Помечает горизонтальными линиями с подписями.

+
BOS/CHoCHSmart MoneyZigZag✅ Исходник
+ +
+
+

Dynamic Fair Value Gap (FVG) MT4

+

Авто-обнаружение имбалансов цены (Fair Value Gap) — 3-барные паттерны. Для Smart Money Concepts стратегии.

+
FVGИмбалансSMC✅ Исходник
+ +
+
+

Institutional ICT Killzones MT4

+

Для SMC/ICT трейдеров. Подсвечивает Asian Range, London Killzone, New York Killzone. Встроенная настройка Broker GMT Offset.

+
KillzonesICTGMT Offset✅ Исходник
+ +
+
+

Institutional DXY Overlay MT5

+

Профессиональный межрыночный инструмент. Накладывает USD Index (DXY), идентифицирует SMT Divergences, институциональные сдвиги корреляции.

+
DXYSMTКорреляция✅ Исходник
+ +
+
+

Institutional Ornstein-Uhlenbeck MT5

+

Эконометрический индикатор. Оценивает истинное дрейфовое равновесие актива и скорость возврата к среднему через стохастический процесс Орнштейна-Уленбека.

+
ЭконометрикаMean Reversion✅ Исходник
+ +
+
+

Institutional Markov Chain MT5

+

Матрица переходов Маркова. Прогнозирует % вероятности бычьего/медвежьего продолжения через стохастические матрицы.

+
МарковВероятность✅ Исходник
+ +
+
+

Institutional Kalman Filter MT5

+

Аэрокосмический фильтр Калмана. Нулевая статическая фазовая задержка при фильтрации рыночного шума и манипуляционных фитилей.

+
КалманНулевая задержка✅ Исходник
+ +
+
+

Institutional Fourier Transform MT5

+

Дискретное преобразование Фурье. Выделяет доминирующую циклическую частоту, устраняет фазовую задержку.

+
ФурьеЦиклыDSP✅ Исходник
+ +
+
+

Institutional GARCH(1,1) MT5

+

Нобелевская модель GARCH(1,1) для прогнозирования волатильности. Заменяет запаздывающий ATR.

+
GARCHПрогнозВолатильность✅ Исходник
+ +
+
+

Institutional K-Means Liquidity MT5

+

Кластеризация K-Means для ликвидности. Находит институциональные кластеры ликвидности на основе объёма.

+
K-MeansMLЛиквидность✅ Исходник
+ +
+
+ +

⏰ Сессионные и временные

+
+
+

Session Range Boxes MT4/MT5

+

Боксы для сессий Азия/Лондон/Нью-Йорк со статистикой средних диапазонов. Опциональные алерты на пробой.

+
СессииСтатистикаПробой✅ Исходник
+ +
+
+

iForexSessions MT5

+

Подсветка сессий форекс: Сидней, Токио, Лондон, Нью-Йорк. Визуальное разделение торговых сессий.

+
СиднейТокиоЛондонНью-Йорк✅ Исходник
+ +
+
+

Economic Calendar Monitor MT5

+

Монитор экономического календаря + кэш для бэктестинга. Экспортирует календарь в архивы, исправляет временные расхождения.

+
НовостиКалендарьБэктест✅ Исходник
+ +
+
+

Candle Timer / Bar Time Countdown MT4/MT5

+

Таймер до закрытия свечи. Для точного входа в конце бара.

+
ТаймерЗакрытие бара✅ Исходник
+
+
+ +

🔄 Корреляция и межрыночный анализ

+
+
+

Correlation Coefficient MT5

+

Стандартный коэффициент корреляции Пирсона между двумя инструментами. Для парного трейдинга и хеджирования.

+
КорреляцияПирсонПарный трейдинг✅ Исходник
+ +
+
+

Currency Strength Meter MT4/MT5

+

Измеритель силы валют. Показывает относительную силу всех основных валют для выбора лучших пар.

+
Сила валютОтносительная✅ Исходник
+
+
+

HTF Reversal Divergences MT5

+

Мульти-таймфрейм дивергенции RSI. Сигналы Buy/Sell, вдохновлён TradingView.

+
MTFДивергенцияRSI✅ Исходник
+ +
+
+
+ +
+

🤖 Советники (Expert Advisors) 150+

+ + +

📈 Трендовые советники

+
+
+

MA Crossover EA MT4/MT5

+

Классический советник на пересечении скользящих средних. Настраиваемые параметры, сессионные фильтры, опциональный мартингейл.

+
МАПересечениеСессии✅ Исходник
+ +
+
+

Super Trend EA MT4/MT5

+

Следование тренду на основе SuperTrend. Контроль сессий/таймфреймов, риск-менеджмент, опции мартингейла.

+
SuperTrendТрендРиск✅ Исходник
+ +
+
+

Ichimoku EA MT4/MT5

+

Торговля по системе Ишимоку. Настраиваемая логика входа, риск-менеджмент, сессионные фильтры.

+
ИшимокуТренд✅ Исходник
+ +
+
+

Turtle Trading EA MT4/MT5

+

Классическая система Черепах. Пробойные входы/выходы по каналу Дончиана. Правила входа/выхода из оригинальной системы.

+
ЧерепахиПробойДончиан✅ Исходник
+ +
+
+

Heiken Ashi EA MT4

+

Торговля на основе Heiken Ashi. Несколько режимов входа, сессионные фильтры, опциональный мартингейл.

+
Heiken AshiТренд✅ Исходник
+ +
+
+

EA31337 Libre MT4/MT5

+

Бесплатный open-source мультистратегический робот. 35+ встроенных стратегий на основе популярных индикаторов. Мульти-таймфрейм.

+
35+ стратегийМульти-ТФOpen Source✅ Исходник
+ +
+
+

VR RSI Robot MT4

+

Мульти-таймфрейм RSI. Синхронизация H1 + D1. Развороты RSI только из экстремальных зон.

+
RSIMTF H1/D1Экстремумы✅ Исходник
+ +
+
+

Stochastic Eclipse MT4

+

Ловит ранние развороты из перекупленности/перепроданности, едет по моментуму, фильтрует ложные сигналы.

+
СтохастикРазворотФильтр✅ Исходник
+ +
+
+ +

⚡ Скальпинговые советники

+
+
+

Goldfinch EA MT4/MT5

+

"Прыгает на внезапных всплесках цены". Простые настройки оптимизации. Для любых инструментов.

+
ВсплескиПростой✅ Исходник
+ +
+
+

ExMachina SafeScalping MT5

+

Консервативный скальпинг пробоев. Gold, Silver, Forex majors. Безопасный подход к скальпингу.

+
КонсервативныйПробой✅ Исходник
+ +
+
+

MSNR v5.31Plus AEU MT5

+

Malaysian SNR + Smart Money. XAUUSD M5. Сканирует W1/D1/H4/H1 для S/R. Ликвидность, sweep, engulfing, trendline, QML, CRT, DOL.

+
Smart MoneyXAUUSD M5MTF✅ Исходник
+ +
+
+

KSQ Fair Value Gap EA MT5

+

Институциональная торговля FVG зонами. 3-барные паттерны. EMA+ADX фильтр режима. Двойной SL/TP.

+
FVGИнституциональныйEMA+ADX✅ Исходник
+ +
+
+

OHLCMTF Scalper MT5

+

Мульти-таймфрейм price action. Без запаздывающих индикаторов. Отложенные входы, role reversals.

+
Price ActionMTFОтложенные✅ Исходник
+ +
+
+

Price Action Intraday MT5

+

Трендовая внутридневная торговля. Pin Bars, Engulfing, Inside Bar Breakouts. Двойной MA фильтр.

+
Пин-барEngulfingInside Bar✅ Исходник
+ +
+
+ +

📊 Сеточные системы

+
+
+

XANDER Grid XAUUSD MT5

+

Двунаправленная сетка специально для золота. Взвешенная средняя TP для группы. Дневная цель прибыли, лимит просадки.

+
СеткаXAUUSDПросадка-лимит✅ Исходник
+ +
+
+

BGC Grid EA MT5

+

Режимно-адаптивная сетка на основе научных исследований Taranto & Khan (2020–2022). BGT/TGT/MGT режимы. CUSUM детекция. CSV диагностика.

+
НаучныйBGT/TGT/MGTCUSUM✅ Исходник
+ +
+
+

RSI Grid EA Pro MT5

+

RSI + адаптивная сетка рекавери. Интеллектуальное управление перекрывающимися ордерами. Виртуальный trailing stop. 3 режима лота.

+
RSIРекавери3 режима лота✅ Исходник
+ +
+
+

MASTER-WINNERFX-Asim MT5

+

Трендовая сетка. EMA + RSI. Динамический лот от баланса. Контролируемый мультипликатор. Управление корзиной. Фильтр новостей.

+
EMA+RSIДинамический лотНовости✅ Исходник
+ +
+
+ +

🧠 AI и нейросетевые советники

+
+
+

Prime Quantum AI MT5

+

Классический префильтр + AI vision-подтверждение. ADX + Alligator → подтверждение через Claude/GPT/Gemini/DeepSeek/Grok. Требует WebRequest + API key.

+
Claude/GPTVision AIAPI✅ Исходник
+ +
+
+

ONNX Trader MT5

+

Бот со встроенной ML-моделью, обученной в Python, сохранённой в ONNX формате. Машинное обучение в MQL5.

+
ONNXPython MLНейросеть✅ Исходник
+ +
+
+

Sideways Martingale MT5

+

Мартингейл-детектор тренда с использованием ONNX AI. Нейросеть определяет, когда рынок в боковике.

+
ONNX AIБоковикМартингейл✅ Исходник
+ +
+
+

Easy Neural Network EA MT5

+

Продвинутый EA с использованием технологии искусственных нейронных сетей. Полный исходник mq5 для изучения.

+
НейросетьИсходник mq5Обучение✅ Исходник
+ +
+
+ +

🛡️ Управление рисками и утилиты

+
+
+

RiskSizer Panel Lite MT5

+

Калькулятор лота по риску %. Перетаскиваемые линии SL/TP. One-click BUY/SELL.

+
Риск %ПанельOne-click✅ Исходник
+ +
+
+

Position Size Pro Lite MT5

+

Интерактивный он-чарт калькулятор риска. Необходим для строгого управления капиталом.

+
КалькуляторОн-чартРиск✅ Исходник
+ +
+
+

Stealth Trade Manager MT5

+

Скрытые SL/TP от брокера. Spread Protector для новостей/ролловеров.

+
Скрытые SL/TPЗащита спреда✅ Исходник
+ +
+
+

XPro Trade Panel MT4/MT5

+

Полное управление сделками. One-click вход/выход. SL, TP, отложенные ордера, частичное закрытие.

+
ПанельOne-clickЧастичное закрытие✅ Исходник
+ +
+
+

AutoCloseOnProfitLoss MT5

+

Автоматическое закрытие всех позиций при достижении цели прибыли/убытка.

+
Авто-закрытиеПрибыль/убыток✅ Исходник
+ +
+
+

Professional Close All Positions MT5

+

6 умных фильтров: все, по типу, по символу, по прибыли/убытку. Реальное время P&L.

+
6 фильтровP&L✅ Исходник
+ +
+
+
+ +
+

🥇 Специально для Золота (XAUUSD) 20+

+
+⚠️ Важно: Золото (XAUUSD) — высоковолатильный инструмент. Все советники для золота требуют тщательного бэктеста и демо-тестирования. +Рекомендуется использовать ProCent/центовые счета для снижения риска. +
+
+
+

Quantum XAUUSD Silver Trader MT5 GOLD

+

Мульти-индикаторный EA для золота и серебра. RSI + ADX + MA. Адаптивное взвешивание. Отдельные пресеты для XAUUSD и XAGUSD. ATR-based SL/TP и trailing stop.

+
RSI+ADX+MAАдаптивныйATR SL/TP✅ Исходник
+ +
+
+

XANDER Gold Recovery MT5 GOLD

+

Стратегия Keltner Channel. Опциональная прогрессивная система рекавери. Контроль риска корзины. Образовательный исходник.

+
KeltnerРекавериОбразовательный✅ Исходник
+ +
+
+

XANDER Grid XAUUSD MT5 GOLD

+

Двунаправленная сетка для золота. Взвешенная средняя цена группы. Дневная цель прибыли. Макс. плавающая просадка. Для ProCent счетов.

+
СеткаProCentПросадка-лимит✅ Исходник
+ +
+
+

ExMachina SafeScalping MT5 GOLD

+

Консервативный скальпинг пробоев. Золото, серебро, Forex majors. Безопасный подход.

+
КонсервативныйПробойМульти-актив✅ Исходник
+ +
+
+

MSNR v5.31Plus AEU MT5 GOLD

+

Malaysian SNR + Smart Money. XAUUSD M5. Ликвидность, sweep, engulfing, trendline, QML, CRT, DOL. Частичное закрытие на R-множителях.

+
Smart MoneyM5Частичное закрытие✅ Исходник
+ +
+
+

The Impossible Gold v2.0 MT5 GOLD

+

Сессионный пробойной скальпер для XAUUSD на M5. Торговля на пробое сессионных диапазонов.

+
СессионныйПробойM5✅ Исходник
+ +
+
+

GOLD_ORB MT5 GOLD

+

Open Range Breakout для XAUUSD на H1. Пробой диапазона первых часов торгов. Исходник на GitHub.

+
ORBH1Пробой✅ Исходник
+ +
+
+

Gold 1 Minute EA MT5 GOLD

+

Скальпинг золота на M1. Быстрые сделки с минимальным удержанием. Доступен в MQL5 Market.

+
M1СкальпингБыстрые сделки
+ +
+
+

Launcher Gold Indicator MT5 GOLD

+

Мульти-подтверждающий торговый сигнальный индикатор. Высокая вероятность сделок с чёткими входами.

+
Мульти-сигналЗолотоБесплатно
+ +
+
+

News Filter for XAUUSD MT5 GOLD

+

Простой фильтр новостей для торговли XAUUSD. Пауза во время высоковолатильных экономических релизов.

+
Фильтр новостейПауза✅ Исходник
+ +
+
+
+ +
+

📋 Стратегии для торговли

+ +

🥇 Стратегии для золота (XAUUSD)

+
+
+

Скальпинг золота M1-M5

+

Таймфрейм: M1 или M5
Индикаторы: SuperTrend_Enhanced + Session Range Boxes + ATR
Вход: Пробой сессионного диапазона в направлении тренда SuperTrend
SL: 1.5×ATR | TP: 1:1.5 риск/прибыль
Фильтр: Не торговать во время новостей (Economic Calendar Monitor)

+
M1-M5СкальпингПробой
+
+
+

Smart Money Concepts (SMC) для золота

+

Таймфрейм: M5 вход, H4 структура
Индикаторы: ZigZag BOS CHoCH + FVG + Order Blocks + Liquidity Sweeps
Вход: CHoCH на M5 после sweep ликвидности на H4
SL: За последний swing low/high
TP: Противоположный order block или 1:2 RR

+
SMCICTM5/H4
+
+
+

Трендовая стратегия H1

+

Таймфрейм: H1
Индикаторы: Ichimoku + ADX(14) + Volume Profile
Вход: Цена выше облака Ишимоку + ADX > 25 + объём выше среднего
SL: За Senkou Span B
TP: Trailing stop по Kijun-sen

+
H1ТрендИшимоку
+
+
+

Сеточная стратегия (только для опытных)

+

Инструмент: XANDER Grid или BGC Grid EA
Счёт: ProCent (центовый)
Настройки: Макс. лот 0.04, дневная цель прибыли $10, макс. просадка 20%
Важно: Регулярный вывод прибыли. Сетки опасны — могут слить депозит!

+
СеткаProCentВысокий риск
+
+
+ +

📈 Общие стратегии

+
+
+

Пробой + тренд (Breakout Trend)

+

Индикаторы: Donchian Channel(20) + ADX(14) + Volume
Вход: Пробой верхней/нижней линии Дончиана при ADX > 20
SL: Средняя линия Дончиана
TP: 2× расстояние до SL

+
ПробойДончианADX
+
+
+

RSI + Уровни (Mean Reversion)

+

Индикаторы: RSI(14) + Support/Resistance Lines + Volume
Вход: RSI < 30 + цена у поддержки + рост объёма = BUY
Вход: RSI > 70 + цена у сопротивления + рост объёма = SELL
SL: За ближайший уровень
TP: Средняя линия RSI (50) или противоположный уровень

+
RSIУровниВозврат к среднему
+
+
+

MACD + Bollinger (Trend Vision)

+

Индикаторы: MACD(12,26,9) + Bollinger Bands(20,2) + Trend Vision
Вход: MACD пересёк нулевую линию ВНУТРИ полос Боллинджера + цена отскочила от средней линии
SL: За противоположную полосу BB
TP: 1.5× расстояние до SL

+
MACDBBТренд
+
+
+

Мульти-таймфрейм (MTF Confluence)

+

Индикаторы: RSI Timeframe Analyzer + MTF Moving Average + Session Boxes
Вход: Все 9 таймфреймов RSI показывают одно направление + цена выше/ниже MTF MA
SL: Ближайший уровень сессии
TP: Следующий ключевой уровень

+
MTFКонфлюэнс9 ТФ
+
+
+
+ +
+

🔗 Телеграм-мосты и автоматизация 20+

+ +

💚 Бесплатные решения

+
+
+

ogunjobiFX Signal Copier MT4/MT5

+

Python бот для копирования сигналов из Telegram в MT4/MT5 через MetaAPI cloud. Расчёт лота, SL/TP, P/L. ⚠️ Архивирован (2024), но код рабочий.

+
PythonMetaAPIOpen Source✅ Исходник
+ +
+
+

SignalTrader MT5

+

Автоматический торговый бот. Мониторит сообщения от провайдеров сигналов, исполняет сделки в MetaTrader 5.

+
PythonMT5Авто-исполнение✅ Исходник
+ +
+
+

MT4-Telegram-Bot-Recon MT4

+

EA для MT4, коммуницирующий с Telegram ботом. Запрос ордеров, баланса, информации об аккаунте из Telegram.

+
EA MT4Запросы✅ Исходник
+ +
+
+

EZ MT5 to Telegram Signals Provider MT5

+

EA отправляет сигналы из MT5 в Telegram. Бесплатный провайдер сигналов через бота.

+
EA MT5ОтправкаБесплатно✅ Исходник
+ +
+
+

MtApi (MetaTrader API) MT4/MT5

+

.NET API для работы с MetaTrader. Не прямое API к серверам, а bridge через терминал. Для создания собственных решений.

+
.NET APIBridgeРазработка✅ Исходник
+ +
+
+

Telegram to MT4/MT5 Copier (MQL5 Free) MT4/MT5

+

Бесплатные версии копировщиков в MQL5 Code Base и Market. Базовая функциональность копирования сигналов.

+
MQL5КопировщикБесплатно
+ +
+
+ +

💛 Платные решения

+
+
+

TelegramFxCopier MT4/MT5

+

AI-распознавание сигналов из Telegram. Автоматическое копирование в MT4/MT5. Распознаёт разные форматы сигналов.

+
AIРаспознавание~$30/мес
+ +
+
+

Telegram To MT4 Copier (MQL5) MT4

+

Копирует все сигналы из Telegram в MT4. Работает как remote copier. Лёгкая настройка.

+
RemoteMT4~$99
+ +
+
+

MT5 to Telegram Trade Copier (Zefinx) MT5

+

Двусторонний мост MT5 ↔ Telegram. Копирование сделок, уведомления, управление.

+
ДвустороннийMT5~$49
+ +
+
+

Copygram MT4/MT5

+

Копирование сделок из любого Telegram канала прямо в MetaTrader. Реальное время.

+
Любой каналРеальное времяПодписка
+ +
+
+

TSCopier MT4/MT5

+

Telegram Signals Copier. MT4, MT5, cTrader, TradeLocker. AI-распознавание, cloud routing, дашборд.

+
AICloudМульти-платформа
+ +
+
+

TelegramTradeCopier MT4/MT5

+

Копирование сигналов в миллисекундах. Реальное автоматическое исполнение.

+
МиллисекундыАвтоПодписка
+ +
+
+
+ +
+

🧠 Нейросетевой софт для анализа сделок 15+

+ +

📓 AI-журналы сделок

+
+
+

TradeZella Web

+

AI торговой партнёр и журнал #1. Zella AI анализирует каждую сделку, тегирует сетапы, ревьюит сессии автоматически. 500+ брокеров, 50+ аналитических отчётов.

+
Zella AI500+ брокеровРеплей
+ +
+
+

TradesViz Web

+

AI торговой журнал. 600+ статистик для поиска преимущества, устранения ошибок. Симулятор сделок, бэктестер.

+
600+ статистикСимуляторБесплатный план
+ +
+
+

TraderSync Web

+

AI торговой журнал. AI анализирует каждую сделку, находит прибыльные паттерны. Elite план с полным AI ($79.95/мес).

+
AI паттерныАнализElite $79.95
+ +
+
+

Tradervue Web

+

Индустриальный стандарт торгового журнала. Автоимпорт из 80+ брокеров. Анализ для stocks, options, futures, forex.

+
80+ брокеровИмпортИндустрия
+ +
+
+

TradeMetria Web

+

Интегрированный AI анализирует сделки, находит инсайты, идентифицирует ошибки. Персональный торговый коуч 24/7.

+
AI коуч24/7Инсайты
+ +
+
+ +

🤖 Нейросетевые EA для торговли

+
+
+

Perceptrader AI MT4/MT5

+

AI-powered робот на основе перцептрона и deep learning. +329% verified gain. Использует нейросеть для прогнозирования движений.

+
ПерцептронDeep Learning+329%
+ +
+
+

NODE Neural EA MT5

+

Бесплатный нейросетевой EA, #1 в MQL5 Market. Обучен на исторических данных. Для нескольких пар.

+
#1 MQL5БесплатныйНейросеть
+ +
+
+

Aura Neuron EA MT5

+

Нейросетевой EA из серии Aura. Для EURUSD и GOLD. Автоматизированная торговля с AI-логикой.

+
AuraEURUSD/GOLDAI
+ +
+
+

GOLD-EA-MT5-WITH-OPENAI MT5

+

AI-guided M1 скальпинг на XAUUSD. Дашборд, телеметрия, тестовый режим, retry-safe исполнение, tracked trailing stop.

+
OpenAIM1XAUUSD✅ Исходник
+ +
+
+ +

📊 AI-анализ графиков

+
+
+

FXCharts AI Chart Analyzer iOS/Android

+

AI-powered идентификация графиков. Дает институциональное преимущество для Forex и Crypto. MT4/MT5/TradingView.

+
AIПаттерныМобильное
+ +
+
+

Chart AI — Trading Analysis Android/iOS

+

ML-движок для анализа графиков. Сканирование визуальных паттернов на stocks, forex, crypto.

+
MLСканированиеМобильное
+ +
+
+

NeuroShell Trader Windows

+

Платформа с нейросетевым прогнозированием. Анализирует индикаторы, распознаёт многомерные паттерны, прогнозирует движения.

+
НейросетиПрогнозПлатформа
+ +
+
+
+ +
+

📚 Источники и порталы

+
+ + + + + + + + + + + + + + +
ПорталСсылкаMT4MT5ИсходникиОсобенности
MQL5 Code Basemql5.com/en/code✅ ПолныеКрупнейшая библиотека, 5000+ индикаторов
GitHub — GeneralTradingSarl MT5GitHub✅ 845+Коллекция 845+ MT5 индикаторов
GitHub — GeneralTradingSarl MT4GitHub✅ 800+×33 части по 800+ MT4 индикаторов
ForexCrackedforexcracked.com660+ индикаторов, verified non-repaint
ForexMT4Indicatorsforexmt4indicators.com500+ индикаторов, стратегии
EarnForexearnforex.com100+ индикаторов, открытый код
FXCodeBasefxcodebase.comФорум, 50+ популярных
Point Zero Tradingpointzero-trading.com20+ бесплатных, полная функциональность
Forex Stationforex-station.comФорум, ежедневные обновления
TradingFindertradingfinder.com500+ индикаторов
GitHub — EA31337GitHub35+ стратегий, open source
GitHub — nkanvenGitHubКоллекция EA для кастомизации
+
+
+ + + +
+ + + diff --git a/СПИСОК_ИСТОЧНИКОВ.txt b/СПИСОК_ИСТОЧНИКОВ.txt new file mode 100644 index 0000000..e77c0d1 --- /dev/null +++ b/СПИСОК_ИСТОЧНИКОВ.txt @@ -0,0 +1,81 @@ +================================================================= + ФОРЕКС АРСЕНАЛ + Полный каталог индикаторов, советников и софта +================================================================= + +📊 ИСТОЧНИКИ (все ссылки для скачивания): + +1️⃣ MQL5 CODE BASE — 5000+ индикаторов и советников + MT4: https://www.mql5.com/en/code/mt4 + MT5: https://www.mql5.com/en/code/mt5 + +2️⃣ GITHUB КОЛЛЕКЦИИ (исходники!): + GeneralTradingSarl MT5 (845+ индикаторов): + https://github.com/GeneralTradingSarl/mql5_indicators_mt5_part1 + GeneralTradingSarl MT4 (800+ × 3 части): + https://github.com/GeneralTradingSarl/mql4_indicators_part1 + https://github.com/GeneralTradingSarl/mql4_indicators_part2 + https://github.com/GeneralTradingSarl/mql4_indicators_part3 + EA31337 Libre (35+ стратегий, open source): + https://github.com/EA31337/EA31337-Libre + PeterThomet MT Tools (448⭐): + https://github.com/peterthomet/MetaTrader-5-and-4-Tools + +3️⃣ FOREXCRACKED — 660+ индикаторов: + https://www.forexcracked.com + +4️⃣ EARN FOREX — 100+ индикаторов: + https://www.earnforex.com/indicators/ + +5️⃣ POINT ZERO TRADING: + https://www.pointzero-trading.com/products/free + +6️⃣ FOREXMT4 INDICATORS: + https://forexmt4indicators.com + +🥇 ТОП-10 ИНДИКАТОРОВ ДЛЯ ЗОЛОТА (XAUUSD): + 1. SuperTrend_Enhanced (MT5 #61652) + 2. Session Range Boxes (MT5 #69586) + 3. Institutional DXY Overlay (MT5 #71100) + 4. ZigZag BOS CHoCH Detection (MT5 #65980) + 5. Dynamic Fair Value Gap (MT4 #73458) + 6. Institutional ICT Killzones (MT4 #71073) + 7. HTF Reversal Divergences (MT5 #71611) + 8. Volume Profile (EarnForex) + 9. Currency Strength Meter + 10. Fibonacci Confluence Zone Finder (MT4 #72219) + +🥇 ТОП-10 СОВЕТНИКОВ ДЛЯ ЗОЛОТА: + 1. Quantum XAUUSD Silver Trader (MT5 #73622) + 2. MSNR v5.31Plus AEU (MT5 #73680) + 3. XANDER Grid XAUUSD (MT5 #71776) + 4. ExMachina SafeScalping (MT5 #70052) + 5. GOLD_ORB (GitHub: yulz008) + 6. Nyao Scalper v42 (GitHub: elrizwiraswara) + 7. Gold 1 Minute EA (MQL5 Market) + 8. EA31337 Libre (GitHub) + 9. RSI Grid EA Pro (MT5 #71700) + 10. KSQ Fair Value Gap EA (MT5 #71467) + +🔗 ТЕЛЕГРАМ-МОСТЫ (бесплатные): + - ogunjobiFX Signal Copier (GitHub, Python+MetaAPI) + - SignalTrader (GitHub: ebrahimkhodadadi) + - MT4-Telegram-Bot-Recon (GitHub: dennislwm) + - EZ MT5 to Telegram (ForexFactory) + +🔗 ТЕЛЕГРАМ-МОСТЫ (платные): + - TelegramFxCopier (~$30/мес) + - Telegram To MT4 Copier (MQL5 Market, ~$99) + - MT5 to Telegram Trade Copier (Zefinx, ~$49) + - Copygram (подписка) + - TSCopier (AI + Cloud) + +🧠 НЕЙРОСЕТЕВОЙ СОФТ: + - TradeZella (AI журнал, 500+ брокеров) + - TradesViz (600+ статистик) + - Perceptrader AI (Deep Learning, +329%) + - NODE Neural EA (#1 в MQL5 Market, бесплатный) + - GOLD-EA-MT5-WITH-OPENAI (GitHub) + - Prime Quantum AI (MT5 #72527, Claude/GPT) + - NeuroShell Trader + - FXCharts AI Chart Analyzer